@7n/rules-lang-js 0.8.0 → 0.10.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/doc-files/docs/extractors.md +1 -1
- package/doc-files/docs/index.md +1 -0
- package/doc-files/docs/js-facts.md +47 -0
- package/doc-files/docs/units-js.md +1 -2
- package/doc-files/extractors.mjs +135 -12
- package/doc-files/js-facts.mjs +28 -0
- package/doc-files/units-js.mjs +26 -10
- package/package.json +1 -1
- package/rules/bun/package_json/package_json.mdc +3 -1
- package/rules/bun/package_json/package_json.rego +30 -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 +46 -0
- package/rules/storybook/adopt/main.mjs +379 -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 +60 -0
- package/rules/storybook/mocking/concern.json +3 -0
- package/rules/storybook/mocking/mocking.mdc +108 -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 +164 -0
- package/rules/storybook/scaffold/template/docs/index.md +10 -0
- package/rules/storybook/scaffold/template/docs/main.md +30 -0
- package/rules/storybook/scaffold/template/docs/preview.md +46 -0
- package/rules/storybook/scaffold/template/main.js +45 -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 +38 -0
- package/rules/storybook/vitest-config/docs/index.md +10 -0
- package/rules/storybook/vitest-config/docs/main.md +72 -0
- package/rules/storybook/vitest-config/fix-vitest-config.mjs +340 -0
- package/rules/storybook/vitest-config/main.mjs +358 -0
- package/rules/storybook/vitest-config/template/docs/index.md +12 -0
- package/rules/storybook/vitest-config/template/docs/storybook-project-entry.md +34 -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 +29 -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 +22 -0
- package/rules/storybook/vitest-config/template/unit-project-entry.js +5 -0
- package/rules/storybook/vitest-config/template/vitest.config.baseline.mjs +37 -0
- package/rules/storybook/vitest-config/template/vitest.stryker.config.baseline.mjs +19 -0
- package/rules/storybook/vitest-config/vitest-config.mdc +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.0] - 2026-07-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- storybook: канон Storybook хвилі 1 для Vue-компонентних бібліотек — детекція скоупу (isVueComponentLibraryPkg, поріг ≥3 .vue, opt-out), канонічний скафолд .storybook/main.js+preview.js+mocks/gql-sse.js, package.json#scripts.storybook (ADR канон-storybook-для-vue-компонентних-бібліотек)
|
|
8
|
+
- npm-module/bun: governance-виняток канону Storybook (кластер 7 ADR канон-storybook-для-vue-компонентних-бібліотек) — npm_package_json.rego дозволяє канонічні Storybook-devDeps (storybook, @storybook/vue3-vite, @storybook/vue3, msw, msw-storybook-addon) у npm/package.json із зафіксованою точною версією (deny на неканонічний пакет або неканонічну версію); bun/package_json.rego розширює root-only test peers на @vitest/browser + playwright (browser-mode provider для named vitest project "storybook", лише chromium) та @storybook/addon-vitest (storybookTest-плагін того самого vitest-конфіга) — Storybook-identity-пакети у корінь свідомо не додаються
|
|
9
|
+
- storybook: vitest-config-концерн хвилі 1 (ADR Кластер 5) — canonical test.projects unit+storybook (browser-mode, лише chromium, stories-glob) дописується поверх наявного vitest-конфіга, ізольований vitest.stryker.config генерується поруч (Stryker крашиться на browser-mode projects)
|
|
10
|
+
- storybook: концерни mocking (docs-only рецепти router/tfm/Apollo-MSW/Pinia/page-story) і hygiene (undeclared third-party imports у .vue, auto-detect sassVariables) — ADR Кластер 3/6
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- storybook: підключено concern-и scope/scaffold/vitest-config до unified lint-рушія (lint-блок у concern.json — check:true без lint мовчки ігнорувався run-detectors.mjs), додано --adopt-режим (adopt/main.mjs) і скіл n-storybook
|
|
15
|
+
|
|
16
|
+
## [0.9.0] - 2026-07-20
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- doc-files: Vue SFC-екстрактор (`.vue` через optional peer `vue/compiler-sfc`) — props/emits/exposed як псевдо-експорти, слоти з `@slot`-коментарів шаблону, юніти зі зміщеними у файл офсетами
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- doc-files: JSDoc-атрибуція експортів/юнітів через реальні AST-коментарі парсера (не regex по сирому тексту) — усуває false positive, коли '/**'-подібний текст трапляється всередині // -коментаря чи рядкового літералу
|
|
25
|
+
|
|
3
26
|
## [0.8.0] - 2026-07-20
|
|
4
27
|
|
|
5
28
|
### Added
|
package/doc-files/docs/index.md
CHANGED
|
@@ -7,5 +7,6 @@ resource: plugins/lang-js/doc-files/
|
|
|
7
7
|
| Файл | Тип |
|
|
8
8
|
| ------------------------------- | --------- |
|
|
9
9
|
| [extractors.mjs](extractors.md) | JS Module |
|
|
10
|
+
| [js-facts.mjs](js-facts.md) | JS Module |
|
|
10
11
|
| [units-js.mjs](units-js.md) | JS Module |
|
|
11
12
|
| [vue.mjs](vue.md) | JS Module |
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: js-facts.mjs
|
|
4
|
+
resource: plugins/lang-js/doc-files/js-facts.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: c0e84691
|
|
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
|
+
Модуль формує поведінковий профіль для `parseJsDoc`, `extractFileHeader`, `precedingJsDoc`, `extractExports`, `extractImports`, `extractInternalSymbols`, `extractLocalSymbols` і `extractMarkers`: зчитує JSDoc, заголовок файлу, імпорти, експорти, локальні та внутрішні символи, щоб описувати публічну поверхню й службові частини коду. Під час обходу свідомо пропускає `.github`, `.git`, `node_modules`, `base/`, `ua/` і `.firebase`. Модуль звертається до мережі, використовує кешування в межах одного прогону та за окремих помилок повертає порожнє значення, зокрема `null`, замість винятку.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
- parseJsDoc — розбирає JSDoc у читабельний опис, окремо виділяє параметри й текст повернення.
|
|
20
|
+
- extractFileHeader — бере провідний блок-коментар файлу як намір, якщо він стоїть на початку до будь-якого коду чи import.
|
|
21
|
+
- precedingJsDoc — знаходить найближчий JSDoc-блок, що стоїть впритул перед потрібною позицією.
|
|
22
|
+
- extractExports — збирає експортовані оголошення разом із пов’язаним JSDoc, щоб описати публічну поверхню модуля.
|
|
23
|
+
- extractImports — розкладає імпорти на stdlib, npm та internal; внутрішні шляхи не змішує з зовнішніми.
|
|
24
|
+
- extractInternalSymbols — витягує імена символів із внутрішніх імпортів, щоб їх не подавати як зовнішній API.
|
|
25
|
+
- extractLocalSymbols — знаходить неекспортовані top-level функції й класи як службові елементи модуля.
|
|
26
|
+
- extractMarkers — визначає поведінкові ознаки коду, зокрема мережеві звернення, кешування, обробку помилок, читання-only та свідомі пропуски шляхів `.github`, `.git`, `node_modules`, `base/`, `ua/`, `.firebase`.
|
|
27
|
+
|
|
28
|
+
## Публічний API
|
|
29
|
+
|
|
30
|
+
- parseJsDoc — Опис (без @-тегів) + параметри з `@param` як «name — опис».
|
|
31
|
+
- extractFileHeader — Провідний блок-коментар файлу (намір), якщо він перед першим import/кодом.
|
|
32
|
+
- precedingJsDoc — Блок-коментар, що стоїть ВПРИТУЛ перед позицією (лише пробіли між ними).
|
|
33
|
+
`(?:(?!\*/)[\s\S])*` гарантує, що тіло не містить `*/`, тож захоплюється рівно один
|
|
34
|
+
найближчий блок — без жадібного «перестрибування» через імпорти/код.
|
|
35
|
+
- extractExports — Експорти + JSDoc, що безпосередньо передує кожному.
|
|
36
|
+
- extractImports — Імпорти, класифіковані на stdlib / npm / internal.
|
|
37
|
+
- extractInternalSymbols — Імена символів, імпортованих із внутрішніх модулів — їх модель не має згадувати.
|
|
38
|
+
- extractLocalSymbols — Імена top-level функцій/класів, які НЕ експортуються (службові помічники).
|
|
39
|
+
Модель не має подавати їх як «публічні функції» у Поведінці/API (R6).
|
|
40
|
+
Const-стрілки свідомо не ловимо — менше false-positive на змістовних константах.
|
|
41
|
+
- extractMarkers — Поведінкові маркери — евристики регулярками.
|
|
42
|
+
|
|
43
|
+
## Гарантії поведінки
|
|
44
|
+
|
|
45
|
+
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
46
|
+
- Кешує результати в межах одного прогону.
|
|
47
|
+
- Свідомо пропускає шляхи: `.github`, `.git`, `node_modules`, `base/`, `ua/`, `.firebase`.
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: units-js.mjs
|
|
4
4
|
resource: plugins/lang-js/doc-files/units-js.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: d6ca02fb
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
|
-
tier: cloud-min
|
|
9
8
|
score: 100
|
|
10
9
|
issues: judge:inaccurate:0.94
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
package/doc-files/extractors.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** @see ./docs/extractors.md */
|
|
2
|
+
import { parseProgramAndCommentsOrNull } from '@7n/rules/scripts/utils/ast-scan-utils.mjs'
|
|
2
3
|
import { extractUnitsJs } from './units-js.mjs'
|
|
3
4
|
import { extractFactsVue, extractUnitsVue } from './vue.mjs'
|
|
5
|
+
import { jsDocCommentBefore } from './js-facts.mjs'
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Мовний doc-files-екстрактор JS-екосистеми для конвеєра `@7n/rules`
|
|
@@ -36,8 +38,14 @@ const JSDOC_CLOSE_RE = /\*\/\s*$/
|
|
|
36
38
|
const STAR_PREFIX_RE = /^\s*\*?\s?/
|
|
37
39
|
const PARAM_LINE_RE = /^@param[ \t]{1,8}(?:\{[^}]{0,200}\}[ \t]{1,8})?\[?([\w.]{1,80})\]?[ \t]{0,8}(.{0,400})$/
|
|
38
40
|
const RETURNS_LINE_RE = /^@returns?[ \t]{1,8}(?:\{[^}]{0,200}\}[ \t]{1,8})?(.{0,400})$/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
// `(?!\/)` одразу після відкриття — без нього glob-рядок на кшталт `'src/**/linux.rs'`
|
|
42
|
+
// (символи `/`,`*`,`*`,`/`) читається як порожній коментар-відкриття `/**/`, і жадібний
|
|
43
|
+
// пошук найближчого `*/` «протікає» аж до наступного РЕАЛЬНОГО закриття JSDoc, змішуючи
|
|
44
|
+
// код між ними у `desc`. Справжній JSDoc ніколи не має `/` одразу після `/**`. Regex-фолбек
|
|
45
|
+
// для випадків без `comments` від парсера (див. `jsDocCommentBefore` — надійніший шлях,
|
|
46
|
+
// коли парсинг вдався, бо AST уже коректно розрізняє справжні коментарі й `//`-текст).
|
|
47
|
+
const FILE_HEADER_RE = /^\s*\/\*\*(?!\/)([\s\S]*?)\*\//
|
|
48
|
+
const PRECEDING_JSDOC_RE = /\/\*\*(?!\/)(?:(?!\*\/)[\s\S])*\*\/\s*$/
|
|
41
49
|
const EXPORT_DECL_RE = /export\s+(?:async\s+)?(function|const|class)\s+(\w+)/g
|
|
42
50
|
// Top-level function/class декларації (колонка 0) — для R6: службові функції,
|
|
43
51
|
// які не експортуються, не мають протікати у Поведінку/API як «публічні».
|
|
@@ -102,6 +110,63 @@ function cleanJsDoc(raw) {
|
|
|
102
110
|
.trim()
|
|
103
111
|
}
|
|
104
112
|
|
|
113
|
+
// Заголовок `\@param`/`\@returns` із незакритим на тому ж рядку типом (`\@param {{`
|
|
114
|
+
// на початку багаторядкового object-type). `.*` без `s`-прапора — навмисно: `l`
|
|
115
|
+
// уже без `\n` (рядки з `text.split('\n')`), тож `.` природно зупиняється на межі рядка.
|
|
116
|
+
const TAG_HEAD_RE = /^@(param|returns?)\b[ \t]*(\{.*)?$/
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {string} s текст
|
|
120
|
+
* @param {string} ch односимвольний рядок для підрахунку
|
|
121
|
+
* @returns {number} кількість входжень `ch` у `s`
|
|
122
|
+
*/
|
|
123
|
+
function countOccurrences(s, ch) {
|
|
124
|
+
return s.split(ch).length - 1
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Просуває стан пропуску багаторядкового object-type (`\@param {{ ... }}`) на один
|
|
129
|
+
* рядок: рахує баланс дужок, і коли він сходиться в 0 — домальовує рядок як
|
|
130
|
+
* звичайний `\@param name опис`/`\@returns опис` (текст після останньої `}`).
|
|
131
|
+
* @param {{tag:'param'|'returns', depth:number}} braceSkip стан пропуску (мутується)
|
|
132
|
+
* @param {string} l поточний рядок
|
|
133
|
+
* @returns {{line:string|null}} `line:null` — рядок ще всередині типу (пропустити); інакше — реконструйований рядок
|
|
134
|
+
*/
|
|
135
|
+
function advanceBraceSkip(braceSkip, l) {
|
|
136
|
+
braceSkip.depth += countOccurrences(l, '{') - countOccurrences(l, '}')
|
|
137
|
+
if (braceSkip.depth > 0) return { line: null }
|
|
138
|
+
return { line: `@${braceSkip.tag} ${l.slice(l.lastIndexOf('}') + 1).trim()}` }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Виявляє старт багаторядкового `\@param {{`/`\@returns {{` (тип не закрився на
|
|
143
|
+
* цьому ж рядку — більше `{`, ніж `}`).
|
|
144
|
+
* @param {string} l поточний рядок
|
|
145
|
+
* @returns {{tag:'param'|'returns', depth:number}|null} стан пропуску або null, якщо не старт
|
|
146
|
+
*/
|
|
147
|
+
function detectMultilineTagStart(l) {
|
|
148
|
+
const tagHead = l.match(TAG_HEAD_RE)
|
|
149
|
+
if (!tagHead?.[2]) return null
|
|
150
|
+
const opens = countOccurrences(tagHead[2], '{')
|
|
151
|
+
const closes = countOccurrences(tagHead[2], '}')
|
|
152
|
+
if (opens <= closes) return null
|
|
153
|
+
return { tag: tagHead[1].startsWith('return') ? 'returns' : 'param', depth: opens - closes }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Дописує continuation-рядок (обгорнутий хвіст) до відповідного \@param/\@returns.
|
|
158
|
+
* @param {'returns'|{kind:'param', idx:number}} continuation активний тег
|
|
159
|
+
* @param {Array<{name:string, desc:string}>} params накопичені параметри (мутуються)
|
|
160
|
+
* @param {string} ret поточний текст `@returns`
|
|
161
|
+
* @param {string} tail новий текст для дописування
|
|
162
|
+
* @returns {string} оновлений `ret` (для `returns`; для `param` — вхідний `ret` без змін)
|
|
163
|
+
*/
|
|
164
|
+
function appendContinuation(continuation, params, ret, tail) {
|
|
165
|
+
if (continuation === 'returns') return `${ret} ${tail}`.trim()
|
|
166
|
+
params[continuation.idx].desc = `${params[continuation.idx].desc} ${tail}`.trim()
|
|
167
|
+
return ret
|
|
168
|
+
}
|
|
169
|
+
|
|
105
170
|
/**
|
|
106
171
|
* Опис (без @-тегів) + параметри з `@param` як «name — опис».
|
|
107
172
|
* @param {string} raw сирий JSDoc-блок
|
|
@@ -113,20 +178,52 @@ function parseJsDoc(raw) {
|
|
|
113
178
|
const descLines = []
|
|
114
179
|
const params = []
|
|
115
180
|
let ret = ''
|
|
116
|
-
|
|
181
|
+
// Рядок без `@` на початку — це або (до першого тегу) частина `desc`, або (після
|
|
182
|
+
// @param/@returns) обгорнутий на новий рядок «хвіст» ЦЬОГО тегу. Без відстеження
|
|
183
|
+
// continuation такий хвіст мовчки падав у `descLines`, змішуючи текст @returns/
|
|
184
|
+
// @param у загальний опис (напр. довге `@returns` на 2 рядки).
|
|
185
|
+
let continuation = null // null | 'desc' | 'returns' | { kind: 'param', idx: number }
|
|
186
|
+
// Багаторядковий `@param {{ ... }}`/`@returns {{ ... }}` (складний object-type,
|
|
187
|
+
// не закритий на тому ж рядку): тіло типу пропускаємо (не тягнемо в desc/params/ret),
|
|
188
|
+
// рахуючи баланс дужок по рядках через `advanceBraceSkip`.
|
|
189
|
+
let braceSkip = null // null | { tag: 'param'|'returns', depth: number }
|
|
190
|
+
for (const rawLine of lines) {
|
|
191
|
+
let l = rawLine
|
|
192
|
+
if (braceSkip) {
|
|
193
|
+
const advanced = advanceBraceSkip(braceSkip, l)
|
|
194
|
+
if (advanced.line === null) continue
|
|
195
|
+
l = advanced.line
|
|
196
|
+
braceSkip = null
|
|
197
|
+
}
|
|
117
198
|
const pm = l.match(PARAM_LINE_RE)
|
|
118
199
|
if (pm) {
|
|
119
200
|
const desc = pm[2].trim()
|
|
120
201
|
// «опис.» — JSDoc-заглушка без сенсу; не тягнемо її як факт
|
|
121
202
|
params.push({ name: pm[1], desc: desc === 'опис.' ? '' : desc })
|
|
203
|
+
continuation = { kind: 'param', idx: params.length - 1 }
|
|
122
204
|
continue
|
|
123
205
|
}
|
|
124
206
|
const rm = l.match(RETURNS_LINE_RE)
|
|
125
207
|
if (rm) {
|
|
126
208
|
ret = rm[1].trim()
|
|
209
|
+
continuation = 'returns'
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
const multilineStart = detectMultilineTagStart(l)
|
|
213
|
+
if (multilineStart) {
|
|
214
|
+
braceSkip = multilineStart
|
|
215
|
+
continuation = null
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
if (l.startsWith('@')) {
|
|
219
|
+
continuation = null // невідомий/непідтримуваний тег — не продовжуємо в нього
|
|
127
220
|
continue
|
|
128
221
|
}
|
|
129
|
-
if (l.
|
|
222
|
+
if (continuation && continuation !== 'desc' && l.trim()) {
|
|
223
|
+
ret = appendContinuation(continuation, params, ret, l.trim())
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
continuation = 'desc'
|
|
130
227
|
descLines.push(l)
|
|
131
228
|
}
|
|
132
229
|
return { desc: descLines.join('\n').trim(), params, ret }
|
|
@@ -134,10 +231,22 @@ function parseJsDoc(raw) {
|
|
|
134
231
|
|
|
135
232
|
/**
|
|
136
233
|
* Провідний блок-коментар файлу (намір), якщо він перед першим import/кодом.
|
|
234
|
+
* `comments` (з парсера, `parseProgramAndCommentsOrNull`) — точний шлях: перший
|
|
235
|
+
* коментар файлу має бути саме ним. Без `comments` (парсинг не вдався, або
|
|
236
|
+
* виклик над фрагментом без AST — напр. Vue script-блок через `VUE_HELPERS`)
|
|
237
|
+
* — regex-фолбек на сирому тексті.
|
|
137
238
|
* @param {string} src вміст файлу
|
|
239
|
+
* @param {Array<{type:string, value:string, start:number, end:number}>|null} [comments] список коментарів парсера або null
|
|
138
240
|
* @returns {string} текст header-коментаря або порожній рядок
|
|
139
241
|
*/
|
|
140
|
-
function extractFileHeader(src) {
|
|
242
|
+
function extractFileHeader(src, comments = null) {
|
|
243
|
+
if (comments) {
|
|
244
|
+
const first = comments[0]
|
|
245
|
+
const isLeadingJsDoc = first?.type === 'Block' && first.value.startsWith('*')
|
|
246
|
+
if (isLeadingJsDoc && src.slice(0, first.start).trim() === '')
|
|
247
|
+
return parseJsDoc(src.slice(first.start, first.end)).desc
|
|
248
|
+
return ''
|
|
249
|
+
}
|
|
141
250
|
const m = src.match(FILE_HEADER_RE)
|
|
142
251
|
if (!m) return ''
|
|
143
252
|
// має бути на самому початку (до import/код)
|
|
@@ -147,8 +256,11 @@ function extractFileHeader(src) {
|
|
|
147
256
|
|
|
148
257
|
/**
|
|
149
258
|
* Блок-коментар, що стоїть ВПРИТУЛ перед позицією (лише пробіли між ними).
|
|
150
|
-
*
|
|
151
|
-
*
|
|
259
|
+
* Regex-фолбек для випадків без `comments` від парсера (див. `jsDocCommentBefore`
|
|
260
|
+
* — надійніший AST-based шлях, коли парсинг вдався). `(?:(?!\*/)[\s\S])*` гарантує,
|
|
261
|
+
* що тіло не містить `*/`, тож захоплюється рівно один найближчий блок — без
|
|
262
|
+
* жадібного «перестрибування» через імпорти/код (окрім залишкового класу false
|
|
263
|
+
* positive усередині `//`-коментарів, який і закриває `jsDocCommentBefore`).
|
|
152
264
|
* @param {string} prefix вміст файлу до позиції експорту
|
|
153
265
|
* @returns {string|null} JSDoc-блок або null якщо немає
|
|
154
266
|
*/
|
|
@@ -158,15 +270,18 @@ function precedingJsDoc(prefix) {
|
|
|
158
270
|
}
|
|
159
271
|
|
|
160
272
|
/**
|
|
161
|
-
* Експорти + JSDoc, що безпосередньо передує кожному.
|
|
273
|
+
* Експорти + JSDoc, що безпосередньо передує кожному. З `comments` (парсер) —
|
|
274
|
+
* точна AST-based атрибуція (`jsDocCommentBefore`); без них (парсинг не вдався,
|
|
275
|
+
* або виклик над фрагментом без AST) — regex-фолбек (`precedingJsDoc`).
|
|
162
276
|
* @param {string} src вміст файлу
|
|
277
|
+
* @param {Array<{type:string, value:string, start:number, end:number}>|null} [comments] список коментарів парсера або null
|
|
163
278
|
* @returns {Array<object>} список експортів із метаданими
|
|
164
279
|
*/
|
|
165
|
-
function extractExports(src) {
|
|
280
|
+
function extractExports(src, comments = null) {
|
|
166
281
|
const out = []
|
|
167
282
|
for (const m of src.matchAll(EXPORT_DECL_RE)) {
|
|
168
283
|
const [, kind, name] = m
|
|
169
|
-
const jsdocRaw = precedingJsDoc(src.slice(0, m.index))
|
|
284
|
+
const jsdocRaw = comments ? jsDocCommentBefore(comments, src, m.index) : precedingJsDoc(src.slice(0, m.index))
|
|
170
285
|
out.push({ name, kind, ...(jsdocRaw ? parseJsDoc(jsdocRaw) : { desc: '', params: [], ret: '' }) })
|
|
171
286
|
}
|
|
172
287
|
return out
|
|
@@ -263,6 +378,12 @@ const VUE_HELPERS = {
|
|
|
263
378
|
|
|
264
379
|
/**
|
|
265
380
|
* Головний екстрактор: код файлу → факт-лист.
|
|
381
|
+
* Коментарі беруться з реального AST-парсера (`parseProgramAndCommentsOrNull`),
|
|
382
|
+
* не regex по сирому тексту — усуває клас false positive, де "/**"-подібний
|
|
383
|
+
* текст усередині `//`-коментаря чи рядкового літералу (напр. glob-патерн)
|
|
384
|
+
* помилково читається як відкриття JSDoc. Парсинг не вдався (синтаксична
|
|
385
|
+
* помилка) → `comments: null`, `extractFileHeader`/`extractExports` падають
|
|
386
|
+
* назад на свій regex-шлях (той самий, що й до цієї зміни).
|
|
266
387
|
* @param {string} src вміст файлу
|
|
267
388
|
* @param {string} relPath шлях (для контексту/мови екстрактора)
|
|
268
389
|
* @returns {{relPath:string, lang:string, header:string, exports:Array, imports:object, markers:object}} структура фактів про файл
|
|
@@ -273,11 +394,13 @@ export function extractFacts(src, relPath) {
|
|
|
273
394
|
if (!['js', 'mjs', 'ts'].includes(lang)) {
|
|
274
395
|
return { relPath, lang, unsupported: true, header: '', exports: [], imports: {}, markers: {} }
|
|
275
396
|
}
|
|
397
|
+
const parsed = parseProgramAndCommentsOrNull(src, relPath)
|
|
398
|
+
const comments = parsed?.comments ?? null
|
|
276
399
|
return {
|
|
277
400
|
relPath,
|
|
278
401
|
lang,
|
|
279
|
-
header: extractFileHeader(src),
|
|
280
|
-
exports: extractExports(src),
|
|
402
|
+
header: extractFileHeader(src, comments),
|
|
403
|
+
exports: extractExports(src, comments),
|
|
281
404
|
imports: extractImports(src),
|
|
282
405
|
internalSymbols: extractInternalSymbols(src),
|
|
283
406
|
localSymbols: extractLocalSymbols(src),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** @see ./docs/js-facts.md */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JSDoc-коментар (Block, `/** ... */`), що стоїть ВПРИТУЛ перед позицією (лише
|
|
5
|
+
* пробіли між ними) — з реального списку коментарів парсера (`comments` від
|
|
6
|
+
* `parseProgramAndCommentsOrNull`), не regex по сирому тексту. Спільна для
|
|
7
|
+
* `extractors.mjs` (експорти js/mjs/ts) і `units-js.mjs` (юніти) — усуває клас
|
|
8
|
+
* false positive, де "/**"-подібний текст трапляється всередині `//`-коментаря
|
|
9
|
+
* чи рядкового літералу (напр. glob `'src/**/x.rs'` чи `// приклад: /** ... */`)
|
|
10
|
+
* — токенізатор там уже коректно визначив межі справжніх коментарів, а
|
|
11
|
+
* regex-сканер такого тексту не бачить окремо і жадібно «протікає» до
|
|
12
|
+
* наступного реального `*/`, змішуючи проміжний код в опис. Винесена в окремий
|
|
13
|
+
* модуль (без залежностей на `extractors.mjs`/`units-js.mjs`), щоб обидва могли
|
|
14
|
+
* її імпортувати без циклічного імпорту між собою.
|
|
15
|
+
* @param {Array<{type:string, value:string, start:number, end:number}>} comments список коментарів парсера (у порядку файлу)
|
|
16
|
+
* @param {string} src вміст файлу (для перевірки, що проміжок — лише пробіли)
|
|
17
|
+
* @param {number} pos позиція, перед якою шукаємо коментар
|
|
18
|
+
* @returns {string|null} дослівний `/** ... */`-текст або null, якщо немає
|
|
19
|
+
*/
|
|
20
|
+
export function jsDocCommentBefore(comments, src, pos) {
|
|
21
|
+
let best = null
|
|
22
|
+
for (const c of comments) {
|
|
23
|
+
if (c.type !== 'Block' || !c.value.startsWith('*') || c.end > pos) continue
|
|
24
|
+
if (!best || c.end > best.end) best = c
|
|
25
|
+
}
|
|
26
|
+
if (!best || src.slice(best.end, pos).trim() !== '') return null
|
|
27
|
+
return src.slice(best.start, best.end)
|
|
28
|
+
}
|
package/doc-files/units-js.mjs
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/** @see ./docs/units-js.md */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { parseProgramAndCommentsOrNull, walkAstWithAncestors } from '@7n/rules/scripts/utils/ast-scan-utils.mjs'
|
|
4
|
+
import { jsDocCommentBefore } from './js-facts.mjs'
|
|
4
5
|
|
|
5
|
-
//
|
|
6
|
-
|
|
6
|
+
// Regex-фолбек для випадків без `comments` від парсера (парсинг не вдався —
|
|
7
|
+
// не мало б статися тут, бо `extractUnitsJs` і так вимагає успішний `program`,
|
|
8
|
+
// але `parsed.comments` теоретично може бути порожнім масивом на дивному вході).
|
|
9
|
+
// `(?!\/)` одразу після відкриття — без нього glob-рядок `'src/**/linux.rs'`
|
|
10
|
+
// читається як порожній `/**/`, і жадібний пошук найближчого `*/` протікає до
|
|
11
|
+
// наступного реального закриття JSDoc, змішуючи проміжний код у витягнутий опис.
|
|
12
|
+
const JSDOC_BEFORE_RE = /\/\*\*(?!\/)(?:(?!\*\/)[\s\S])*\*\/\s*$/
|
|
7
13
|
const JSDOC_OPEN_RE = /^\s*\/\*\*?/
|
|
8
14
|
const JSDOC_CLOSE_RE = /\*\/\s*$/
|
|
9
15
|
const STAR_PREFIX_RE = /^\s*\*?\s?/
|
|
@@ -25,12 +31,19 @@ function cleanDoc(raw) {
|
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
|
-
* JSDoc, що передує позиції `start` у джерелі (або порожній рядок).
|
|
34
|
+
* JSDoc, що передує позиції `start` у джерелі (або порожній рядок). З `comments`
|
|
35
|
+
* (реальний список від парсера) — точна AST-based атрибуція через
|
|
36
|
+
* `jsDocCommentBefore` (js-facts.mjs): усуває клас false positive, де
|
|
37
|
+
* "/**"-подібний текст усередині `//`-коментаря чи рядкового літералу (напр.
|
|
38
|
+
* glob-патерн) помилково читається regex-ом як відкриття JSDoc. Порожній
|
|
39
|
+
* `comments` (немає жодного коментаря в файлі) — фолбек на `JSDOC_BEFORE_RE`.
|
|
29
40
|
* @param {string} src вміст файлу
|
|
30
41
|
* @param {number} start зміщення початку декларації
|
|
42
|
+
* @param {Array<{type:string, value:string, start:number, end:number}>} comments список коментарів парсера
|
|
31
43
|
* @returns {string} очищений опис
|
|
32
44
|
*/
|
|
33
|
-
function precedingDoc(src, start) {
|
|
45
|
+
function precedingDoc(src, start, comments) {
|
|
46
|
+
if (comments.length) return cleanDoc(jsDocCommentBefore(comments, src, start))
|
|
34
47
|
const m = src.slice(0, start).match(JSDOC_BEFORE_RE)
|
|
35
48
|
return cleanDoc(m ? m[0] : '')
|
|
36
49
|
}
|
|
@@ -75,11 +88,12 @@ function collectCalls(node) {
|
|
|
75
88
|
* @param {number} docStart зміщення для пошуку JSDoc (зовнішній export-вузол)
|
|
76
89
|
* @param {string} src вміст файлу
|
|
77
90
|
* @param {Array<object>} units акумулятор
|
|
91
|
+
* @param {Array<{type:string, value:string, start:number, end:number}>} comments список коментарів парсера
|
|
78
92
|
* @returns {void}
|
|
79
93
|
*/
|
|
80
|
-
function pushUnits(decl, exported, docStart, src, units) {
|
|
94
|
+
function pushUnits(decl, exported, docStart, src, units, comments) {
|
|
81
95
|
if (!decl || typeof decl !== 'object') return
|
|
82
|
-
const doc = precedingDoc(src, docStart)
|
|
96
|
+
const doc = precedingDoc(src, docStart, comments)
|
|
83
97
|
if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
|
|
84
98
|
const name = decl.id?.name
|
|
85
99
|
if (!name) return
|
|
@@ -120,17 +134,19 @@ function pushUnits(decl, exported, docStart, src, units) {
|
|
|
120
134
|
* @returns {Array<{name:string, kind:string, exported:boolean, span:{start:number,end:number}, body:string, calls:string[], doc:string}>|null} юніти або null, якщо файл не парситься
|
|
121
135
|
*/
|
|
122
136
|
export function extractUnitsJs(src, relPath = 'scan.ts') {
|
|
123
|
-
const
|
|
137
|
+
const parsed = parseProgramAndCommentsOrNull(src, relPath)
|
|
138
|
+
const program = parsed?.program
|
|
124
139
|
if (!program || !Array.isArray(program.body)) return null
|
|
140
|
+
const comments = parsed.comments
|
|
125
141
|
|
|
126
142
|
const units = []
|
|
127
143
|
for (const node of program.body) {
|
|
128
144
|
const isExport =
|
|
129
145
|
(node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') && node.declaration
|
|
130
146
|
if (isExport) {
|
|
131
|
-
pushUnits(node.declaration, true, node.start, src, units)
|
|
147
|
+
pushUnits(node.declaration, true, node.start, src, units, comments)
|
|
132
148
|
} else {
|
|
133
|
-
pushUnits(node, false, node.start, src, units)
|
|
149
|
+
pushUnits(node, false, node.start, src, units, comments)
|
|
134
150
|
}
|
|
135
151
|
}
|
|
136
152
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/rules-lang-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -9,6 +9,8 @@ Rego-пакет: `bun.package_json`
|
|
|
9
9
|
Gate виносить два класи deny:
|
|
10
10
|
|
|
11
11
|
1. **Заборонені top-level поля** — будь-яке поле з `package.json.deny.json` присутнє у файлі (навіть із порожнім значенням `{}`).
|
|
12
|
-
2. **devDependencies не з білого списку** — дозволені лише `@nitra/*`/`@7n/*` та root-only тестові peer/tools (`vitest`, `@vitest/coverage-v8`, `@stryker-mutator/vitest-runner`, `@playwright/test`). Будь-який інший пакет → deny. CLI-тули, які `n-rules lint` спавнить через `bunx` (oxlint, jscpd, v8r, github-actionlint тощо), у root не пінуються — вони приїжджають як `dependencies` пакета `@7n/rules` (npm-module.mdc).
|
|
12
|
+
2. **devDependencies не з білого списку** — дозволені лише `@nitra/*`/`@7n/*` та root-only тестові peer/tools (`vitest`, `@vitest/coverage-v8`, `@vitest/browser`, `@stryker-mutator/vitest-runner`, `@stryker-mutator/core`, `@playwright/test`, `playwright`, `@storybook/addon-vitest`, `@7n/test`). Будь-який інший пакет → deny. CLI-тули, які `n-rules lint` спавнить через `bunx` (oxlint, jscpd, v8r, github-actionlint тощо), у root не пінуються — вони приїжджають як `dependencies` пакета `@7n/rules` (npm-module.mdc).
|
|
13
|
+
|
|
14
|
+
`@vitest/browser`/`playwright`/`@storybook/addon-vitest` — додані для named vitest project "storybook" (канон Storybook, кластер 5: browser-mode, лише chromium) — той самий root vitest.config, що й `unit`-проект; `@storybook/addon-vitest` постачає `storybookTest`-плагін для цього vitest-конфіга (канонічний template правила `storybook`). **Межа з `npm-module.mdc`**: Storybook-специфічні identity-пакети (`storybook`, `@storybook/vue3-vite`, `@storybook/vue3`, `msw`, `msw-storybook-addon`) у цей allowlist **не** додаються — вони живуть у `npm/package.json` консюмер-пакета (канон Storybook, кластер 7 Governance), бо `isStorybookRoot()` у `@7n/test` детектує Storybook-скоуп саме за тим файлом, не кореневим. `@storybook/addon-vitest` — виняток із цієї межі: він test-tooling (vitest-плагін), а не identity-маркер, тож root, як і решта vitest-peer'ів.
|
|
13
15
|
|
|
14
16
|
Перевірки, що потребують FS або cross-file контексту (наприклад наявність `yarn.lock`), лишаються у JS-шарі.
|
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
# - `devDependencies` лише `@nitra/*` + root-only тестові peer/tools для `@7n/test coverage`
|
|
9
9
|
# (правило `test` enabled завжди — див. `test/auto.md`; published workspace-и не мають
|
|
10
10
|
# `devDependencies` за `npm-module.mdc`)
|
|
11
|
+
# - `@vitest/browser`/`playwright`/`@storybook/addon-vitest` (browser-mode provider +
|
|
12
|
+
# `storybookTest`-плагін для named vitest project "storybook", лише chromium — канон
|
|
13
|
+
# Storybook кластер 5) теж root-only test peers: той самий vitest.config, що й
|
|
14
|
+
# `unit`-проект, живе в корені монорепо-споживача. Storybook-специфічні
|
|
15
|
+
# identity-пакети (`storybook`, `@storybook/vue3*`, `msw*`) НЕ сюди — вони живуть у
|
|
16
|
+
# `npm/package.json` (канон Storybook кластер 7, `npm-module.mdc`), бо
|
|
17
|
+
# `isStorybookRoot()` @7n/test читає саме той файл, не кореневий package.json.
|
|
18
|
+
# `@storybook/addon-vitest` — виняток із цього правила: це test-tooling (плагін
|
|
19
|
+
# vitest-конфіга), а не Storybook-identity-маркер, тож root, а не npm/package.json.
|
|
11
20
|
#
|
|
12
21
|
# Перевірки, які потребують FS / cross-file контексту, лишаються у JS.
|
|
13
22
|
package bun.package_json
|
|
@@ -50,7 +59,27 @@ deny contains msg if {
|
|
|
50
59
|
# @stryker-mutator/core — обов'язковий exact-pin peer vitest-runner@9+ (раніше тягнувся транзитивно)
|
|
51
60
|
# @7n/test — оркестратор `coverage` (npx @7n/test coverage); devDependency, щоб npx резолвив
|
|
52
61
|
# локально без мережевого fetch щоразу.
|
|
53
|
-
|
|
62
|
+
# @vitest/browser + playwright — провайдер browser-mode для named vitest project
|
|
63
|
+
# "storybook" (канон Storybook кластер 5: лише chromium, PR — швидкий
|
|
64
|
+
# --project=storybook). `playwright` (не `@playwright/test`) — сирий driver, який
|
|
65
|
+
# @vitest/browser використовує як provider; `@playwright/test` лишається окремо для
|
|
66
|
+
# змістовних E2E-сценаріїв (n-vue.mdc).
|
|
67
|
+
# @storybook/addon-vitest — постачає `storybookTest` для vitest-плагіна в канонічному
|
|
68
|
+
# vitest.config named-проекту "storybook" (той самий канон Storybook кластер 5); версія
|
|
69
|
+
# з лінійки Storybook 9.x (узгоджена з `storybook`@9.1.10, запіненим у
|
|
70
|
+
# npm_package_json.rego) — allowlist тут за іменем, точний пінінг версії root-tooling
|
|
71
|
+
# не робимо (на відміну від Storybook-identity-пакетів у npm/package.json).
|
|
72
|
+
allowed_root_test_deps := {
|
|
73
|
+
"vitest",
|
|
74
|
+
"@vitest/coverage-v8",
|
|
75
|
+
"@vitest/browser",
|
|
76
|
+
"@stryker-mutator/vitest-runner",
|
|
77
|
+
"@stryker-mutator/core",
|
|
78
|
+
"@playwright/test",
|
|
79
|
+
"playwright",
|
|
80
|
+
"@storybook/addon-vitest",
|
|
81
|
+
"@7n/test",
|
|
82
|
+
}
|
|
54
83
|
|
|
55
84
|
allowed_root_dev_dependency(name) if {
|
|
56
85
|
startswith(name, "@nitra/")
|
|
@@ -16,10 +16,13 @@ Rego-пакет: `npm-module.npm_package_json`
|
|
|
16
16
|
- Обовʼязкове, має бути непорожнім масивом.
|
|
17
17
|
- Subset-of перевірка: кожне значення з канонічного сніпету має бути присутнє у `files`. За замовчуванням — `"types"` обовʼязковий.
|
|
18
18
|
|
|
19
|
-
**Поле `devDependencies`** (inverse-pattern
|
|
19
|
+
**Поле `devDependencies`** (inverse-pattern + Storybook-виняток, логіка в rego):
|
|
20
20
|
|
|
21
21
|
- Не публікуються користувачам пакета — має бути відсутнє або порожнє `{}`.
|
|
22
|
-
-
|
|
22
|
+
- **Виняток — канонічні Storybook-пакети** (канон Storybook, кластер 7 Governance: `docs/adr/канон-storybook-для-vue-компонентних-бібліотек.md`): `storybook`, `@storybook/vue3-vite`, `@storybook/vue3`, `msw`, `msw-storybook-addon` дозволені як devDeps саме тут, у `npm/package.json` консюмер-пакета — **не** в кореневому `package.json`. Обґрунтування: майбутній `isStorybookRoot()` у `@7n/test` читає саме цей файл, щоб визначити Storybook-скоуп workspace-пакета, тож маркер-пакети мають бути видимі тут, а не в кореневих tooling-deps. Версія кожного канонічного пакета зафіксована точно (map `storybook_canon_dev_deps` у rego) — присутність пакета з іншою версією теж deny (окреме повідомлення, не плутати з allowlist-забороною).
|
|
23
|
+
- Будь-який інший devDep (не з канонічного Storybook-списку) → deny з переліком імен. Dev-інструментарій переноситься у кореневий `package.json`; CLI-тули, які пакет спавнить через `bunx` у репозиторіях-споживачах (пінінг версій), — у `dependencies` (кореневе bun-правило `package_json` такі пакети в root devDeps не пускає).
|
|
24
|
+
|
|
25
|
+
Канон Storybook-devDeps та їхні версії — static map у `npm_package_json.rego` (не template-driven: це опційний allowlist, а не mandatory-presence дані, тож генеричний T0-fix-writer цього concern-а їх у кожен `package.json` не мерджить — див. коментар на початку rego-файлу).
|
|
23
26
|
|
|
24
27
|
Канонічний сніпет `files`: [package.json.snippet.json](./template/package.json.snippet.json)
|
|
25
28
|
|
|
@@ -50,8 +53,20 @@ FS-перевірки (наявність файлу зі шляху `types`, с
|
|
|
50
53
|
{ "files": ["bin", "mdc"] }
|
|
51
54
|
```
|
|
52
55
|
|
|
53
|
-
✗ Неправильно — наявні `devDependencies
|
|
56
|
+
✗ Неправильно — наявні `devDependencies`, не з канонічного Storybook-списку:
|
|
54
57
|
|
|
55
58
|
```json
|
|
56
59
|
{ "devDependencies": { "@7n/rules": "^1.0.0" } }
|
|
57
60
|
```
|
|
61
|
+
|
|
62
|
+
✓ Правильно — канонічний Storybook-devDep із зафіксованою версією (канон Storybook):
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{ "devDependencies": { "storybook": "9.1.10" } }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
✗ Неправильно — Storybook-devDep присутній, але версія не збігається з каноном:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{ "devDependencies": { "storybook": "8.0.0" } }
|
|
72
|
+
```
|