@7n/rules-lang-js 0.12.0 → 0.13.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.0] - 2026-07-21
4
+
5
+ ### Added
6
+
7
+ - storybook: новий concern `ci` (ADR Кластер 5, CI-частина) — канонічний composite action `setup-playwright-chromium` (кеш Playwright-браузерів, лише chromium) і `.github/workflows/lint-storybook.yml` (швидкий PR-прогін `vitest --project=storybook`), гейтований `requires.capability: ci:github`
8
+
9
+ ### Fixed
10
+
11
+ - storybook: viteFinal-фільтр стійкий до VueMacros-стека (Promise/масив-резолв, сімейний фільтр vite:*/vue-macros), vitest@^4 provider-factory (@vitest/browser-playwright) замість застарілого рядка `'playwright'`, flat-root layout у detectStoriesGlob (components без src/), точковий alias-мок одного модуля в mocking.mdc, і STORIES_RE false positive на `storybookTest({ configDir })` без явного include — усе за результатами пілота adopt-діагностики на nitra/components. Заодно governance package_json.rego: allowlist доповнено `@vitest/browser-playwright`.
12
+ - canon Storybook: viteConfigPath-обхід (empty-vite.config.js), валідний iconSet-імпорт, mswLoader замість mswDecorator, повний .storybook/**-glob у CI/lint, вирівняні governance-піни (storybook ^10.5.3, root Vite build-tooling deps), knip-виключення для .storybook-артефактів
13
+ - storybook: скоуп-детекція більше не вимагає vite.config.* пакета (source-only Vue-бібліотеки, tauri-components/npm rollout) — hasStandardBuild прибрано, vitest-config fix толерує відсутній vite.config
14
+
3
15
  ## [0.12.0] - 2026-07-21
4
16
 
5
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules-lang-js",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Плагін @7n/rules: JS/npm/bun-екосистема — lint-правила (js/bun/vue/js-run/npm-module/db), taze-провайдер (package.json, bunx taze) і doc-files-екстрактори (oxc AST)",
5
5
  "keywords": [
6
6
  "javascript",
@@ -15,13 +15,17 @@ alwaysApply: false
15
15
 
16
16
  ## Скоуп
17
17
 
18
- Тільки Vue-компонентні бібліотеки — пакет із `vue` у `peerDependencies` (маркер `isVueComponentLibraryPkg`, той самий що й у `vue.mdc`, не дублюється) і не менше **3** `.vue`-файлів. Поріг відсікає пакети з одним-двома допоміжними компонентами, для яких повний Storybook-скафолд — зайві накладні витрати. Пакети без розпізнаваного `vite.config.{js,ts,mjs}` (нестандартний build) пропускаються мовчки — автоматичний скафолд спирається на `viteConfigPath` пакета.
18
+ Тільки Vue-компонентні бібліотеки — пакет із `vue` у `peerDependencies` (маркер `isVueComponentLibraryPkg`, той самий що й у `vue.mdc`, не дублюється) і не менше **3** `.vue`-файлів. Поріг відсікає пакети з одним-двома допоміжними компонентами, для яких повний Storybook-скафолд — зайві накладні витрати.
19
+
20
+ **Наявність `vite.config.{js,ts,mjs}` пакета — НЕ умова скоупу** (хвиля 1.4, фікс за результатами rollout-у на `tauri-components/npm`). До хвилі 1.4 тут була функція `hasStandardBuild` — вона мовчки виключала зі скоупу пакети без власного `vite.config.*` ("skip нестандартного build", ADR Кластер 1). На практиці це виключало легітимні source-only Vue-бібліотеки (peerDependencies.vue + достатньо `.vue`-файлів, просто без окремого Vite-білду) — а канонічний скафолд для них працює без жодних змін: `viteConfigPath` у `.storybook/main.js` завжди вказує на власний `empty-vite.config.js` скафолда (не на `vite.config.*` пакета), а `viteFinal` викликає `loadConfigFromFile`, який толерує відсутній конфіг пакета (повертає `null`, `viteFinal` мерджить порожній список плагінів). Емпірично перевірено на `tauri-components/npm`: `storybook build` + повний прогін тестів у реальному chromium — без жодного `vite.config.*` у пакеті. `hasStandardBuild` прибрано з `scope/main.mjs` разом з усіма згадками; для пакета, де канонічний скафолд дійсно не підходить (справжня екзотика білду — не сам факт відсутності Vite-конфіга), власник додає його у `storybook.optOut` вручну — окрема автоматична детекція "нестандартного build" дублювала б наявний opt-out і давала б хибні спрацювання.
19
21
 
20
22
  Опційно (вимкнено за замовчуванням) — app-проєкти (`vue` у `dependencies`, не бібліотека, + `src/pages/`). Детекція реалізована в `scope/main.mjs`, але не викликається доки в `.n-rules.json` не задано прапорець хвилі 2 — рішення розширювати скоуп на app-проєкти залишається відкритим питанням ADR.
21
23
 
22
24
  **Opt-out:** окремий пакет можна виключити зі скоупу через `.n-rules.json` → `storybook.optOut: string[]` (root dir пакета, той самий формат що й у виводі workspace-роутингу — `.` для кореня, `packages/ui` тощо). Виняток — намір, не помилка: якщо в `optOut` вказано неіснуючий workspace-пакет, `scope`-концерн репортує це як застаріле налаштування.
23
25
 
24
- Логіка детекції (поріг, opt-out, нестандартний build, app-проєкти) — у `scope/main.mjs`, не дублюється тут.
26
+ Логіка детекції (поріг, opt-out, app-проєкти) — у `scope/main.mjs`, не дублюється тут.
27
+
28
+ `vitest-config`-концерн (Кластер 5, нижче) генерує baseline `vitest.config.mjs`/`vitest.stryker.config.mjs` з `import viteConfig from './<vite.config.*>'` — для source-only пакета без `vite.config.*` це зламало б import неіснуючого файлу; `vitest-config/fix-vitest-config.mjs#applyViteConfigImport` підставляє замість import-у порожній локальний `const viteConfig = {}` (еквівалент для `mergeConfig`), решта baseline-структури не змінюється.
25
29
 
26
30
  ## Канонічний скафолд
27
31
 
@@ -52,6 +56,8 @@ alwaysApply: false
52
56
 
53
57
  (домердж у наявний масив `ignore`, не заміна). Звірено з реальним `knip.json` пілотного консюмера.
54
58
 
59
+ Симетрична ручна правка для `oxfmt`: `.storybook/public/mockServiceWorker.js` (згенерований пакетом `msw` файл, `npx msw init` — той самий service worker, на який вказує `staticDirs` у `main.js`) слід додати в `.oxfmtrc.json` консюмера в `ignorePatterns: ["**/mockServiceWorker.js"]` — реформатування псує pristine-стан цього файлу (той самий принцип, що й `**/adr/**`-виняток `oxfmt`/`cspell`, `docs/adr/adr-виключення-oxfmt-cspell.md`).
60
+
55
61
  ## Vitest-конфіг і Stryker-ізоляція (Кластер 5)
56
62
 
57
63
  Канонічний `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`, не дублюється тут.
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: plugins/lang-js/rules/storybook/scope/main.mjs
5
5
  docgen:
6
- crc: 04c71162
6
+ crc: e6dab761
7
7
  model: openai-codex/gpt-5.5
8
- tier: cloud-avg
9
8
  score: 90
10
9
  issues: internal-name:isVueComponentLibraryPkg,judge:inaccurate:0.98
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -11,7 +11,6 @@ import { isVueComponentLibraryPkg } from '../../vue/packages/main.mjs'
11
11
 
12
12
  const CONFIG_FILE = '.n-rules.json'
13
13
  const LEGACY_CONFIG_FILE = '.n-cursor.json'
14
- const VITE_CONFIG_FILES = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
15
14
 
16
15
  /** Поріг кількості `.vue`-файлів для скоупу канону Storybook (ADR Кластер 1). */
17
16
  export const VUE_FILE_THRESHOLD = 3
@@ -86,18 +85,6 @@ export async function countVueFiles(absPkgDir, ignorePaths) {
86
85
  return count
87
86
  }
88
87
 
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
88
  /**
102
89
  * Чи є пакет app-проєктом (не бібліотекою) для хвилі 2: `vue` у `dependencies` (не лише
103
90
  * `peerDependencies`) і не бібліотека компонентів. Реалізовано зараз (щоб не переписувати
@@ -129,7 +116,6 @@ async function evaluateCandidate(rootDir, cwd, matches, ignorePaths) {
129
116
  return null
130
117
  }
131
118
  if (!matches(pkg)) return null
132
- if (!hasStandardBuild(absDir)) return null
133
119
  const vueFileCount = await countVueFiles(absDir, ignorePaths)
134
120
  if (vueFileCount < VUE_FILE_THRESHOLD) return null
135
121
  return { rootDir, absDir, pkg, vueFileCount }
@@ -138,8 +124,11 @@ async function evaluateCandidate(rootDir, cwd, matches, ignorePaths) {
138
124
  /**
139
125
  * Збирає workspace-пакети у скоупі канону Storybook хвилі 1: Vue-компонентна бібліотека
140
126
  * (`vue` у `peerDependencies`, маркер `isVueComponentLibraryPkg` — той самий, що й `vue.mdc`)
141
- * з не менше {@link VUE_FILE_THRESHOLD} `.vue`-файлами, без `storybook.optOut`, зі
142
- * стандартним build (`vite.config.*`). Хвиля 2 (app-проєкти) додається лише за явного
127
+ * з не менше {@link VUE_FILE_THRESHOLD} `.vue`-файлами, без `storybook.optOut`. Наявність
128
+ * `vite.config.*` пакета НЕ умова скоупу (rollout tauri-components/npm, хвиля 1.4):
129
+ * канонічний скафолд (`viteConfigPath` на `empty-vite.config.js`, `loadConfigFromFile`
130
+ * толерує відсутній конфіг) працює й для source-only Vue-бібліотек без власного Vite-білду
131
+ * — див. секцію "Скоуп" у `main.mdc`. Хвиля 2 (app-проєкти) додається лише за явного
143
132
  * прапорця `storybook.detectApps` у `.n-rules.json`.
144
133
  * @param {string} cwd абсолютний корінь репозиторію
145
134
  * @returns {Promise<InScopePackage[]>} пакети у скоупі
@@ -173,7 +162,7 @@ export async function collectInScopeVuePackages(cwd) {
173
162
  /**
174
163
  * Self-check конфігурації: `.n-rules.json` → `storybook.optOut` не має посилатись на
175
164
  * неіснуючі workspace-пакети (застаріле налаштування — пакет перейменували/видалили, а
176
- * opt-out лишився). Сама детекція скоупу (поріг, build, app-проєкти) — pure-функції вище,
165
+ * opt-out лишився). Сама детекція скоупу (поріг, app-проєкти) — pure-функції вище,
177
166
  * покриті тестами напряму; тут лише конфіг-гігієна.
178
167
  * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
179
168
  * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат лінту
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: fix-vitest-config.mjs
4
4
  resource: plugins/lang-js/rules/storybook/vitest-config/fix-vitest-config.mjs
5
5
  docgen:
6
- crc: 35b10da8
6
+ crc: f00f073b
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: plugins/lang-js/rules/storybook/vitest-config/main.mjs
5
5
  docgen:
6
- crc: 543716c9
6
+ crc: ae5f59a0
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 85
9
9
  issues: internal-name:detectStoriesGlob,anchor-miss:(vitest-config.mdc),judge:inaccurate:0.99
@@ -38,6 +38,10 @@ const HERE = dirname(fileURLToPath(import.meta.url))
38
38
  const TEMPLATE_DIR = join(HERE, 'template')
39
39
  const STORIES_GLOB_TOKEN = '__STORYBOOK_STORIES_GLOB__'
40
40
  const VITE_CONFIG_IMPORT_TOKEN = '__VITE_CONFIG_IMPORT__'
41
+ // Рядок import-у як він буквально лежить у baseline-шаблонах (до token-підстановки) —
42
+ // для source-only пакета (без власного vite.config.*, хвиля 1.4) замінюється на порожній
43
+ // placeholder, а не на import неіснуючого файлу.
44
+ const VITE_CONFIG_IMPORT_LINE = `import viteConfig from './${VITE_CONFIG_IMPORT_TOKEN}'\n`
41
45
 
42
46
  const TRIGGER_REASONS = new Set([
43
47
  REASON_VITEST_CONFIG_MISSING,
@@ -254,6 +258,22 @@ async function computeVitestConfigAugment(absPkgDir, vitestConfigPath) {
254
258
  return next === src ? null : next
255
259
  }
256
260
 
261
+ /**
262
+ * Підставляє в baseline-шаблон або import наявного `vite.config.*` пакета (звичайний
263
+ * шлях), або — для source-only пакета без жодного `vite.config.*` (хвиля 1.4,
264
+ * `resolveViteConfigName` повертає `null`) — порожній локальний placeholder замість
265
+ * import-у неіснуючого файлу. `mergeConfig({}, defineConfig({...}))` еквівалентний
266
+ * самому `defineConfig({...})`, тож решта структури шаблону (сама `mergeConfig`-обгортка)
267
+ * лишається незмінною — не потрібен окремий "плоский" варіант шаблону.
268
+ * @param {string} template вихідний текст baseline-шаблону (з токеном `__VITE_CONFIG_IMPORT__`)
269
+ * @param {string | null} viteConfigName ім'я `vite.config.*` пакета або `null`
270
+ * @returns {string} текст з підставленим import-ом чи placeholder-ом
271
+ */
272
+ function applyViteConfigImport(template, viteConfigName) {
273
+ if (viteConfigName) return template.split(VITE_CONFIG_IMPORT_TOKEN).join(viteConfigName)
274
+ return template.replace(VITE_CONFIG_IMPORT_LINE, 'const viteConfig = {}\n')
275
+ }
276
+
257
277
  /**
258
278
  * Генерує повністю новий `vitest.config.mjs` (unit+storybook projects) для
259
279
  * пакета без жодного наявного vitest-конфіга. Експортовано — переюз у
@@ -265,11 +285,8 @@ export async function buildFreshVitestConfig(absPkgDir) {
265
285
  const template = await readFile(join(TEMPLATE_DIR, 'vitest.config.baseline.mjs'), 'utf8')
266
286
  const storiesGlob = storiesGlobForVitestConfig(absPkgDir)
267
287
  const viteConfigName = resolveViteConfigName(absPkgDir)
268
- const content = template
269
- .split(STORIES_GLOB_TOKEN)
270
- .join(storiesGlob)
271
- .split(VITE_CONFIG_IMPORT_TOKEN)
272
- .join(viteConfigName)
288
+ const withStories = template.split(STORIES_GLOB_TOKEN).join(storiesGlob)
289
+ const content = applyViteConfigImport(withStories, viteConfigName)
273
290
  return { path: join(absPkgDir, 'vitest.config.mjs'), content }
274
291
  }
275
292
 
@@ -283,7 +300,7 @@ export async function buildFreshVitestConfig(absPkgDir) {
283
300
  export async function buildStrykerConfig(absPkgDir) {
284
301
  const template = await readFile(join(TEMPLATE_DIR, 'vitest.stryker.config.baseline.mjs'), 'utf8')
285
302
  const viteConfigName = resolveViteConfigName(absPkgDir)
286
- return template.split(VITE_CONFIG_IMPORT_TOKEN).join(viteConfigName)
303
+ return applyViteConfigImport(template, viteConfigName)
287
304
  }
288
305
 
289
306
  /**
@@ -66,12 +66,16 @@ export function resolveVitestConfigPath(absPkgDir) {
66
66
  /**
67
67
  * Резолвить ім'я `vite.config.*` пакета для import-шляху в baseline-шаблонах
68
68
  * (той самий файл, що й `viteFinal` у `.storybook/main.js`). Пакет у скоупі
69
- * гарантовано має один із них (`scope/main.mjs#hasStandardBuild`).
69
+ * Storybook НЕ гарантовано має власний `vite.config.*` (хвиля 1.4 — вимогу
70
+ * `hasStandardBuild` прибрано зі скоуп-детекції, `scope/main.mjs`) — source-only
71
+ * Vue-бібліотека (напр. tauri-components/npm) законно потрапляє у скоуп без
72
+ * жодного `vite.config.*`. `null` — сигнал викликачу (`fix-vitest-config.mjs`)
73
+ * підставити порожній placeholder замість імпорту неіснуючого файлу.
70
74
  * @param {string} absPkgDir абсолютний шлях кореня пакета
71
- * @returns {string} ім'я файлу vite-конфіга (дефолт `vite.config.js`)
75
+ * @returns {string | null} ім'я файлу vite-конфіга або `null`, якщо жодного немає
72
76
  */
73
77
  export function resolveViteConfigName(absPkgDir) {
74
- return VITE_CONFIG_NAMES.find(n => existsSync(join(absPkgDir, n))) ?? 'vite.config.js'
78
+ return VITE_CONFIG_NAMES.find(n => existsSync(join(absPkgDir, n))) ?? null
75
79
  }
76
80
 
77
81
  /**