@7n/rules-lang-js 0.7.1 → 0.9.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 +16 -0
- package/doc-files/docs/extractors.md +1 -2
- package/doc-files/docs/index.md +2 -0
- package/doc-files/docs/js-facts.md +47 -0
- package/doc-files/docs/units-js.md +1 -2
- package/doc-files/docs/vue.md +34 -0
- package/doc-files/extractors.mjs +164 -16
- package/doc-files/js-facts.mjs +28 -0
- package/doc-files/units-js.mjs +26 -10
- package/doc-files/vue.mjs +215 -0
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.0] - 2026-07-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- doc-files: Vue SFC-екстрактор (`.vue` через optional peer `vue/compiler-sfc`) — props/emits/exposed як псевдо-експорти, слоти з `@slot`-коментарів шаблону, юніти зі зміщеними у файл офсетами
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- doc-files: JSDoc-атрибуція експортів/юнітів через реальні AST-коментарі парсера (не regex по сирому тексту) — усуває false positive, коли '/**'-подібний текст трапляється всередині // -коментаря чи рядкового літералу
|
|
12
|
+
|
|
13
|
+
## [0.8.0] - 2026-07-20
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- doc-files: Vue SFC-екстрактор (`<script setup>`) — extractFactsVue/extractUnitsVue через optional peer vue/compiler-sfc; props/emits/expose/слоти як публічний контракт, юніти зі span-корекцією (ADR 260719-2155)
|
|
18
|
+
|
|
3
19
|
## [0.7.1] - 2026-07-20
|
|
4
20
|
|
|
5
21
|
### Fixed
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: extractors.mjs
|
|
4
4
|
resource: plugins/lang-js/doc-files/extractors.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: ec2c4b59
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
|
-
tier: cloud-min
|
|
9
8
|
score: 100
|
|
10
9
|
issues: judge:inaccurate:0.98
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
package/doc-files/docs/index.md
CHANGED
|
@@ -7,4 +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 |
|
|
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
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: vue.mjs
|
|
4
|
+
resource: plugins/lang-js/doc-files/vue.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 8651a381
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
`vueScriptBlock`, `extractFactsVue` і `extractUnitsVue` працюють із Vue SFC як з джерелом для подальшого витягування фактів і юнітів у координатах оригінального `.vue`. Вони підтримують fail-safe поведінку: перехоплюють помилки, не кидають винятків назовні та за окремих збоїв повертають порожнє значення, зокрема `null`, замість помилки.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
- `vueScriptBlock` — розбирає `.vue` як SFC і повертає `script setup` або `script` блок із дескриптором; якщо peer `vue` відсутній, SFC битий або script-блоку немає, повертає порожній результат замість помилки.
|
|
21
|
+
- `extractFactsVue` — формує факт-лист для Vue SFC на основі `script`-блоку: виділяє публічний контракт компонента через props, emits, expose і slots, а також додає JS-факти з `script`; якщо `vue` недоступний, SFC битий або script-блоку немає, повертає `unsupported`.
|
|
22
|
+
- `extractUnitsVue` — витягує JS/TS-юніти зі `script`-блоку Vue-файла і переносить їхні span-позиції в координати оригінального `.vue`; якщо `vue` недоступний, SFC битий або script-блоку немає, повертає порожній результат.
|
|
23
|
+
|
|
24
|
+
## Публічний API
|
|
25
|
+
|
|
26
|
+
- vueScriptBlock — Дістає script-блок із SFC, віддаючи пріоритет `<script setup>`, разом із дескриптором.
|
|
27
|
+
- extractFactsVue — Збирає публічний контракт Vue SFC: props, emits, expose і slots, а також повторно використовує JS-хелпери для header, imports і markers із тексту script-блоку; `<template>` і `<style>` у факти не потрапляють. Без peer `vue` або на битому SFC чи без script-блоку повертає `unsupported` у whole-file режимі, як до впровадження.
|
|
28
|
+
- extractUnitsVue — Будує JS-юніти з script-блоку `.vue` і зсуває span-и на позиції в оригінальному `.vue` файлі, щоб anchors і CRC вказували саме на вихідний SFC, а не на вирізаний фрагмент.
|
|
29
|
+
|
|
30
|
+
## Гарантії поведінки
|
|
31
|
+
|
|
32
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
33
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
34
|
+
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
package/doc-files/extractors.mjs
CHANGED
|
@@ -1,5 +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'
|
|
4
|
+
import { extractFactsVue, extractUnitsVue } from './vue.mjs'
|
|
5
|
+
import { jsDocCommentBefore } from './js-facts.mjs'
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Мовний doc-files-екстрактор JS-екосистеми для конвеєра `@7n/rules`
|
|
@@ -35,8 +38,14 @@ const JSDOC_CLOSE_RE = /\*\/\s*$/
|
|
|
35
38
|
const STAR_PREFIX_RE = /^\s*\*?\s?/
|
|
36
39
|
const PARAM_LINE_RE = /^@param[ \t]{1,8}(?:\{[^}]{0,200}\}[ \t]{1,8})?\[?([\w.]{1,80})\]?[ \t]{0,8}(.{0,400})$/
|
|
37
40
|
const RETURNS_LINE_RE = /^@returns?[ \t]{1,8}(?:\{[^}]{0,200}\}[ \t]{1,8})?(.{0,400})$/
|
|
38
|
-
|
|
39
|
-
|
|
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*$/
|
|
40
49
|
const EXPORT_DECL_RE = /export\s+(?:async\s+)?(function|const|class)\s+(\w+)/g
|
|
41
50
|
// Top-level function/class декларації (колонка 0) — для R6: службові функції,
|
|
42
51
|
// які не експортуються, не мають протікати у Поведінку/API як «публічні».
|
|
@@ -101,6 +110,63 @@ function cleanJsDoc(raw) {
|
|
|
101
110
|
.trim()
|
|
102
111
|
}
|
|
103
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
|
+
|
|
104
170
|
/**
|
|
105
171
|
* Опис (без @-тегів) + параметри з `@param` як «name — опис».
|
|
106
172
|
* @param {string} raw сирий JSDoc-блок
|
|
@@ -112,20 +178,52 @@ function parseJsDoc(raw) {
|
|
|
112
178
|
const descLines = []
|
|
113
179
|
const params = []
|
|
114
180
|
let ret = ''
|
|
115
|
-
|
|
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
|
+
}
|
|
116
198
|
const pm = l.match(PARAM_LINE_RE)
|
|
117
199
|
if (pm) {
|
|
118
200
|
const desc = pm[2].trim()
|
|
119
201
|
// «опис.» — JSDoc-заглушка без сенсу; не тягнемо її як факт
|
|
120
202
|
params.push({ name: pm[1], desc: desc === 'опис.' ? '' : desc })
|
|
203
|
+
continuation = { kind: 'param', idx: params.length - 1 }
|
|
121
204
|
continue
|
|
122
205
|
}
|
|
123
206
|
const rm = l.match(RETURNS_LINE_RE)
|
|
124
207
|
if (rm) {
|
|
125
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 // невідомий/непідтримуваний тег — не продовжуємо в нього
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
if (continuation && continuation !== 'desc' && l.trim()) {
|
|
223
|
+
ret = appendContinuation(continuation, params, ret, l.trim())
|
|
126
224
|
continue
|
|
127
225
|
}
|
|
128
|
-
|
|
226
|
+
continuation = 'desc'
|
|
129
227
|
descLines.push(l)
|
|
130
228
|
}
|
|
131
229
|
return { desc: descLines.join('\n').trim(), params, ret }
|
|
@@ -133,10 +231,22 @@ function parseJsDoc(raw) {
|
|
|
133
231
|
|
|
134
232
|
/**
|
|
135
233
|
* Провідний блок-коментар файлу (намір), якщо він перед першим import/кодом.
|
|
234
|
+
* `comments` (з парсера, `parseProgramAndCommentsOrNull`) — точний шлях: перший
|
|
235
|
+
* коментар файлу має бути саме ним. Без `comments` (парсинг не вдався, або
|
|
236
|
+
* виклик над фрагментом без AST — напр. Vue script-блок через `VUE_HELPERS`)
|
|
237
|
+
* — regex-фолбек на сирому тексті.
|
|
136
238
|
* @param {string} src вміст файлу
|
|
239
|
+
* @param {Array<{type:string, value:string, start:number, end:number}>|null} [comments] список коментарів парсера або null
|
|
137
240
|
* @returns {string} текст header-коментаря або порожній рядок
|
|
138
241
|
*/
|
|
139
|
-
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
|
+
}
|
|
140
250
|
const m = src.match(FILE_HEADER_RE)
|
|
141
251
|
if (!m) return ''
|
|
142
252
|
// має бути на самому початку (до import/код)
|
|
@@ -146,8 +256,11 @@ function extractFileHeader(src) {
|
|
|
146
256
|
|
|
147
257
|
/**
|
|
148
258
|
* Блок-коментар, що стоїть ВПРИТУЛ перед позицією (лише пробіли між ними).
|
|
149
|
-
*
|
|
150
|
-
*
|
|
259
|
+
* Regex-фолбек для випадків без `comments` від парсера (див. `jsDocCommentBefore`
|
|
260
|
+
* — надійніший AST-based шлях, коли парсинг вдався). `(?:(?!\*/)[\s\S])*` гарантує,
|
|
261
|
+
* що тіло не містить `*/`, тож захоплюється рівно один найближчий блок — без
|
|
262
|
+
* жадібного «перестрибування» через імпорти/код (окрім залишкового класу false
|
|
263
|
+
* positive усередині `//`-коментарів, який і закриває `jsDocCommentBefore`).
|
|
151
264
|
* @param {string} prefix вміст файлу до позиції експорту
|
|
152
265
|
* @returns {string|null} JSDoc-блок або null якщо немає
|
|
153
266
|
*/
|
|
@@ -157,15 +270,18 @@ function precedingJsDoc(prefix) {
|
|
|
157
270
|
}
|
|
158
271
|
|
|
159
272
|
/**
|
|
160
|
-
* Експорти + JSDoc, що безпосередньо передує кожному.
|
|
273
|
+
* Експорти + JSDoc, що безпосередньо передує кожному. З `comments` (парсер) —
|
|
274
|
+
* точна AST-based атрибуція (`jsDocCommentBefore`); без них (парсинг не вдався,
|
|
275
|
+
* або виклик над фрагментом без AST) — regex-фолбек (`precedingJsDoc`).
|
|
161
276
|
* @param {string} src вміст файлу
|
|
277
|
+
* @param {Array<{type:string, value:string, start:number, end:number}>|null} [comments] список коментарів парсера або null
|
|
162
278
|
* @returns {Array<object>} список експортів із метаданими
|
|
163
279
|
*/
|
|
164
|
-
function extractExports(src) {
|
|
280
|
+
function extractExports(src, comments = null) {
|
|
165
281
|
const out = []
|
|
166
282
|
for (const m of src.matchAll(EXPORT_DECL_RE)) {
|
|
167
283
|
const [, kind, name] = m
|
|
168
|
-
const jsdocRaw = precedingJsDoc(src.slice(0, m.index))
|
|
284
|
+
const jsdocRaw = comments ? jsDocCommentBefore(comments, src, m.index) : precedingJsDoc(src.slice(0, m.index))
|
|
169
285
|
out.push({ name, kind, ...(jsdocRaw ? parseJsDoc(jsdocRaw) : { desc: '', params: [], ret: '' }) })
|
|
170
286
|
}
|
|
171
287
|
return out
|
|
@@ -249,22 +365,42 @@ function extractMarkers(src) {
|
|
|
249
365
|
skips: [...skips]
|
|
250
366
|
}
|
|
251
367
|
}
|
|
368
|
+
/** JS-хелпери, які Vue-екстрактор переюзає над текстом script-блоку SFC. */
|
|
369
|
+
const VUE_HELPERS = {
|
|
370
|
+
extractFileHeader,
|
|
371
|
+
extractExports,
|
|
372
|
+
extractImports,
|
|
373
|
+
extractInternalSymbols,
|
|
374
|
+
extractLocalSymbols,
|
|
375
|
+
extractMarkers,
|
|
376
|
+
parseJsDoc
|
|
377
|
+
}
|
|
378
|
+
|
|
252
379
|
/**
|
|
253
380
|
* Головний екстрактор: код файлу → факт-лист.
|
|
381
|
+
* Коментарі беруться з реального AST-парсера (`parseProgramAndCommentsOrNull`),
|
|
382
|
+
* не regex по сирому тексту — усуває клас false positive, де "/**"-подібний
|
|
383
|
+
* текст усередині `//`-коментаря чи рядкового літералу (напр. glob-патерн)
|
|
384
|
+
* помилково читається як відкриття JSDoc. Парсинг не вдався (синтаксична
|
|
385
|
+
* помилка) → `comments: null`, `extractFileHeader`/`extractExports` падають
|
|
386
|
+
* назад на свій regex-шлях (той самий, що й до цієї зміни).
|
|
254
387
|
* @param {string} src вміст файлу
|
|
255
388
|
* @param {string} relPath шлях (для контексту/мови екстрактора)
|
|
256
389
|
* @returns {{relPath:string, lang:string, header:string, exports:Array, imports:object, markers:object}} структура фактів про файл
|
|
257
390
|
*/
|
|
258
391
|
export function extractFacts(src, relPath) {
|
|
259
392
|
const lang = relPath.split('.').pop()
|
|
393
|
+
if (lang === 'vue') return extractFactsVue(src, relPath, VUE_HELPERS)
|
|
260
394
|
if (!['js', 'mjs', 'ts'].includes(lang)) {
|
|
261
395
|
return { relPath, lang, unsupported: true, header: '', exports: [], imports: {}, markers: {} }
|
|
262
396
|
}
|
|
397
|
+
const parsed = parseProgramAndCommentsOrNull(src, relPath)
|
|
398
|
+
const comments = parsed?.comments ?? null
|
|
263
399
|
return {
|
|
264
400
|
relPath,
|
|
265
401
|
lang,
|
|
266
|
-
header: extractFileHeader(src),
|
|
267
|
-
exports: extractExports(src),
|
|
402
|
+
header: extractFileHeader(src, comments),
|
|
403
|
+
exports: extractExports(src, comments),
|
|
268
404
|
imports: extractImports(src),
|
|
269
405
|
internalSymbols: extractInternalSymbols(src),
|
|
270
406
|
localSymbols: extractLocalSymbols(src),
|
|
@@ -272,17 +408,29 @@ export function extractFacts(src, relPath) {
|
|
|
272
408
|
}
|
|
273
409
|
}
|
|
274
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Юніти: `.vue` — зі script-блоку SFC (span-и в координатах файлу), решта — oxc AST.
|
|
413
|
+
* @param {string} src вміст файлу
|
|
414
|
+
* @param {string} relPath шлях (вибір мови)
|
|
415
|
+
* @returns {Array<object>|null} юніти або null
|
|
416
|
+
*/
|
|
417
|
+
function extractUnits(src, relPath) {
|
|
418
|
+
if (relPath.toLowerCase().endsWith('.vue')) return extractUnitsVue(src, relPath, extractUnitsJs)
|
|
419
|
+
return extractUnitsJs(src, relPath)
|
|
420
|
+
}
|
|
421
|
+
|
|
275
422
|
/**
|
|
276
423
|
* Default-експорт для handler-модуля extension-point `doc-files`.
|
|
277
|
-
* `.vue`
|
|
278
|
-
*
|
|
279
|
-
*
|
|
424
|
+
* `.vue` (SFC зі `<script setup>`) парситься через optional peer `vue/compiler-sfc`
|
|
425
|
+
* (ADR 260719-2155): props/emits/expose/слоти → факт-лист; без peer чи script-блоку —
|
|
426
|
+
* fallback `unsupported` (whole-file шлях, як до впровадження).
|
|
427
|
+
* @type {{ id: string, extensions: string[], extractFacts: typeof extractFacts, extractUnits: typeof extractUnits }}
|
|
280
428
|
*/
|
|
281
429
|
const jsDocFilesExtractor = {
|
|
282
430
|
id: 'js',
|
|
283
431
|
extensions: ['.js', '.mjs', '.ts', '.vue'],
|
|
284
432
|
extractFacts,
|
|
285
|
-
extractUnits
|
|
433
|
+
extractUnits
|
|
286
434
|
}
|
|
287
435
|
|
|
288
436
|
export default jsDocFilesExtractor
|
|
@@ -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
|
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/** @see ./docs/vue.md */
|
|
2
|
+
|
|
3
|
+
// Optional peer `vue`: компілятор резолвиться один раз при завантаженні модуля.
|
|
4
|
+
// Свідомо НЕ статичний import — без установленого peer модуль має лишитися
|
|
5
|
+
// робочим (extractFactsVue → unsupported), а не завалити весь handler lang-js
|
|
6
|
+
// (catch у loadDocFilesExtractors мовчки прибрав би і JS/TS-екстрактор).
|
|
7
|
+
let compilerSfc = null
|
|
8
|
+
try {
|
|
9
|
+
compilerSfc = await import('vue/compiler-sfc')
|
|
10
|
+
} catch {
|
|
11
|
+
/* peer `vue` не встановлено — .vue лишається unsupported (whole-file шлях) */
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const QUOTED_NAME_RE = /'([^']{1,80})'|"([^"]{1,80})"/g
|
|
15
|
+
// Object-style defineEmits<{ save: [id: number] }> — ключ вимагає tuple-значення
|
|
16
|
+
// (`:\s*[`), щоб не ловити label-и всередині tuple (`[id: number]`) як імена подій.
|
|
17
|
+
const EMIT_OBJECT_KEY_RE = /([\w$-]{1,80})\s*:\s*\[/g
|
|
18
|
+
const EMITS_GENERIC_RE = /defineEmits<([^>]{1,2000})>/
|
|
19
|
+
const EMITS_ARRAY_RE = /defineEmits\(\s*\[([^\]]{0,2000})\]/
|
|
20
|
+
const EXPOSE_CALL_RE = /defineExpose\s*\(\s*\{([^()]{0,2000})\}\s*\)/
|
|
21
|
+
// Провідний ідентифікатор одного елемента defineExpose (`focus`, `...rest`, `b: c`).
|
|
22
|
+
const EXPOSE_ITEM_RE = /^(?:\.{3})?([\w$]{1,80})/
|
|
23
|
+
// HTML-коментар template: [^>] гарантує зупинку на першому `>` (кінець `-->`).
|
|
24
|
+
const HTML_COMMENT_RE = /<!--([^>]{0,300})>/g
|
|
25
|
+
const SLOT_NAME_RE = /^[\w-]{1,80}/
|
|
26
|
+
const SLOT_LEAD_MARK_RE = /^[:—-]{1,3}/
|
|
27
|
+
// Канонічний патерн JSDoc-блоку без зворотного перебору.
|
|
28
|
+
const JSDOC_BLOCK_RE = /\/\*\*[^*]*(?:\*(?!\/)[^*]*)*\*\//g
|
|
29
|
+
const WORD_RE = /[\w$-]{1,80}/g
|
|
30
|
+
// Модифікатори/ключові слова, які стоять між JSDoc-блоком і власне іменем
|
|
31
|
+
// (декларації, `(e: '…'`-префікс сигнатури emit-події).
|
|
32
|
+
const SKIP_WORDS = new Set(['readonly', 'export', 'async', 'function', 'const', 'class', 'e'])
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Порожній факт-лист unsupported-фолбеку (peer відсутній / битий SFC / без script).
|
|
36
|
+
* @param {string} relPath шлях файлу
|
|
37
|
+
* @returns {object} факт-лист із `unsupported: true`
|
|
38
|
+
*/
|
|
39
|
+
function unsupportedFacts(relPath) {
|
|
40
|
+
return { relPath, lang: 'vue', unsupported: true, header: '', exports: [], imports: {}, markers: {} }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Розбирає SFC і повертає script-блок (`<script setup>` пріоритетно) з дескриптором.
|
|
45
|
+
* @param {string} src вміст `.vue` файлу
|
|
46
|
+
* @param {string} relPath шлях (filename для компілятора)
|
|
47
|
+
* @returns {{ block: object, descriptor: object }|null} блок+дескриптор або null (нема компілятора / битий SFC / нема script)
|
|
48
|
+
*/
|
|
49
|
+
export function vueScriptBlock(src, relPath) {
|
|
50
|
+
if (!compilerSfc) return null
|
|
51
|
+
let descriptor
|
|
52
|
+
try {
|
|
53
|
+
;({ descriptor } = compilerSfc.parse(src, { filename: relPath }))
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
const block = descriptor.scriptSetup ?? descriptor.script
|
|
58
|
+
if (!block?.content?.trim()) return null
|
|
59
|
+
return { block, descriptor }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Мапа «імʼя → JSDoc-опис» для всього script-блоку: кожен JSDoc-блок
|
|
64
|
+
* привʼязується до першого ідентифікатора одразу після нього (interface-member,
|
|
65
|
+
* ключ обʼєкта, декларація function/const для defineExpose-shorthand, сигнатура
|
|
66
|
+
* emit-події). Одна статична регулярка замість динамічних per-name.
|
|
67
|
+
* @param {string} content текст script-блоку
|
|
68
|
+
* @param {(raw: string) => {desc: string}} parseJsDoc парсер JSDoc з extractors
|
|
69
|
+
* @returns {Map<string, string>} імʼя → опис
|
|
70
|
+
*/
|
|
71
|
+
function jsDocMap(content, parseJsDoc) {
|
|
72
|
+
const map = new Map()
|
|
73
|
+
for (const m of content.matchAll(JSDOC_BLOCK_RE)) {
|
|
74
|
+
const after = content.slice(m.index + m[0].length, m.index + m[0].length + 120)
|
|
75
|
+
for (const w of after.matchAll(WORD_RE)) {
|
|
76
|
+
if (SKIP_WORDS.has(w[0])) continue
|
|
77
|
+
if (!map.has(w[0])) map.set(w[0], parseJsDoc(m[0]).desc)
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return map
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Імена props через `compileScript().bindings` — канонічний резолв і обʼєктної
|
|
86
|
+
* форми, і generic `defineProps<Props>()` з interface у тому ж блоці. Якщо
|
|
87
|
+
* компіляція впала (битий TS/макрос) — props не витягуються (порожній список).
|
|
88
|
+
* @param {object} descriptor SFC-дескриптор
|
|
89
|
+
* @param {string} relPath шлях (id для compileScript)
|
|
90
|
+
* @returns {string[]} імена props
|
|
91
|
+
*/
|
|
92
|
+
function vuePropNames(descriptor, relPath) {
|
|
93
|
+
try {
|
|
94
|
+
const compiled = compilerSfc.compileScript(descriptor, { id: relPath })
|
|
95
|
+
return Object.entries(compiled.bindings ?? {})
|
|
96
|
+
.filter(([, type]) => type === 'props')
|
|
97
|
+
.map(([name]) => name)
|
|
98
|
+
} catch {
|
|
99
|
+
return []
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Імена подій із defineEmits: масив-форма (`['save']`), function-signature
|
|
105
|
+
* generic (`(e: 'save'): void`) — лапковані літерали; object-style generic
|
|
106
|
+
* (`{ save: [id: number] }`) — ключі з tuple-значенням.
|
|
107
|
+
* @param {string} content текст script-блоку
|
|
108
|
+
* @returns {string[]} імена подій
|
|
109
|
+
*/
|
|
110
|
+
function vueEmitNames(content) {
|
|
111
|
+
const m = content.match(EMITS_GENERIC_RE) ?? content.match(EMITS_ARRAY_RE)
|
|
112
|
+
if (!m) return []
|
|
113
|
+
const body = m[1]
|
|
114
|
+
const quoted = Array.from(body.matchAll(QUOTED_NAME_RE), q => q[1] ?? q[2])
|
|
115
|
+
if (quoted.length) return [...new Set(quoted)]
|
|
116
|
+
return [...new Set(Array.from(body.matchAll(EMIT_OBJECT_KEY_RE), k => k[1]))]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Імена публічно виставлених через defineExpose полів (обʼєктна форма).
|
|
121
|
+
* @param {string} content текст script-блоку
|
|
122
|
+
* @returns {string[]} імена exposed-полів
|
|
123
|
+
*/
|
|
124
|
+
function vueExposeNames(content) {
|
|
125
|
+
const m = content.match(EXPOSE_CALL_RE)
|
|
126
|
+
if (!m) return []
|
|
127
|
+
const names = m[1]
|
|
128
|
+
.split(',')
|
|
129
|
+
.map(item => item.trim().match(EXPOSE_ITEM_RE)?.[1])
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
return [...new Set(names)]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Слоти з маркер-коментарів slot у template (імʼя + опційний опис).
|
|
136
|
+
* Розбір двокроковий: спершу HTML-коментарі однією простою регуляркою,
|
|
137
|
+
* далі — плоский JS-парсинг тексту (без складних патернів).
|
|
138
|
+
* @param {string} template текст template-блоку
|
|
139
|
+
* @returns {Array<{name: string, desc: string}>} слоти
|
|
140
|
+
*/
|
|
141
|
+
function vueSlots(template) {
|
|
142
|
+
const slots = []
|
|
143
|
+
for (const m of template.matchAll(HTML_COMMENT_RE)) {
|
|
144
|
+
// m[1] — вміст до першого `>`, тобто разом із хвостовим `--` від `-->`
|
|
145
|
+
const text = m[1].endsWith('--') ? m[1].slice(0, -2).trim() : m[1].trim()
|
|
146
|
+
if (!text.startsWith('@slot')) continue
|
|
147
|
+
const rest = text.slice('@slot'.length).trim()
|
|
148
|
+
const name = rest.match(SLOT_NAME_RE)?.[0]
|
|
149
|
+
if (!name) continue
|
|
150
|
+
const desc = rest.slice(name.length).trim().replace(SLOT_LEAD_MARK_RE, '').trim()
|
|
151
|
+
slots.push({ name, desc })
|
|
152
|
+
}
|
|
153
|
+
return slots
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Факт-лист для Vue SFC (`<script setup>` пріоритетно): props/emits/expose/слоти
|
|
158
|
+
* як публічний контракт компонента + переюз JS-хелперів (header, imports,
|
|
159
|
+
* markers) над текстом script-блоку — `<template>`/`<style>` у факти не течуть.
|
|
160
|
+
* Без peer `vue`, на битому SFC чи без script-блоку — `unsupported` (whole-file
|
|
161
|
+
* шлях, як до впровадження).
|
|
162
|
+
* @param {string} src вміст `.vue` файлу
|
|
163
|
+
* @param {string} relPath шлях файлу
|
|
164
|
+
* @param {object} h JS-хелпери з extractors (extractFileHeader, extractExports, extractImports, extractInternalSymbols, extractLocalSymbols, extractMarkers, parseJsDoc)
|
|
165
|
+
* @returns {object} факт-лист (`lang: 'vue'`)
|
|
166
|
+
*/
|
|
167
|
+
export function extractFactsVue(src, relPath, h) {
|
|
168
|
+
const sb = vueScriptBlock(src, relPath)
|
|
169
|
+
if (!sb) return unsupportedFacts(relPath)
|
|
170
|
+
const { block, descriptor } = sb
|
|
171
|
+
const content = block.content
|
|
172
|
+
const docs = jsDocMap(content, h.parseJsDoc)
|
|
173
|
+
const entry = kind => name => ({ name, kind, desc: docs.get(name) ?? '', params: [], ret: '' })
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
relPath,
|
|
177
|
+
lang: 'vue',
|
|
178
|
+
header: h.extractFileHeader(content),
|
|
179
|
+
exports: [
|
|
180
|
+
...vuePropNames(descriptor, relPath).map(entry('prop')),
|
|
181
|
+
...vueEmitNames(content).map(entry('emit')),
|
|
182
|
+
...vueExposeNames(content).map(entry('expose')),
|
|
183
|
+
...h.extractExports(content)
|
|
184
|
+
],
|
|
185
|
+
slots: vueSlots(descriptor.template?.content ?? ''),
|
|
186
|
+
imports: h.extractImports(content),
|
|
187
|
+
internalSymbols: h.extractInternalSymbols(content),
|
|
188
|
+
localSymbols: h.extractLocalSymbols(content),
|
|
189
|
+
markers: h.extractMarkers(content)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Юніт-шар для `.vue`: JS-юніти зі script-блоку з корекцією span-ів на offset
|
|
195
|
+
* блоку в файлі — anchors/CRC мають вказувати на позиції ОРИГІНАЛЬНОГО `.vue`,
|
|
196
|
+
* а не script-фрагмента.
|
|
197
|
+
* @param {string} src вміст `.vue` файлу
|
|
198
|
+
* @param {string} relPath шлях файлу
|
|
199
|
+
* @param {(src: string, relPath: string) => Array<object>|null} unitsJs екстрактор юнітів js/ts
|
|
200
|
+
* @returns {Array<object>|null} юніти або null (нема компілятора / script / не парситься)
|
|
201
|
+
*/
|
|
202
|
+
export function extractUnitsVue(src, relPath, unitsJs) {
|
|
203
|
+
const sb = vueScriptBlock(src, relPath)
|
|
204
|
+
if (!sb) return null
|
|
205
|
+
const { block } = sb
|
|
206
|
+
// relPath завжди закінчується на .vue (диспетчер викликає лише для нього)
|
|
207
|
+
const pseudoPath = relPath.slice(0, -'.vue'.length) + (block.lang === 'ts' ? '.ts' : '.js')
|
|
208
|
+
const units = unitsJs(block.content, pseudoPath)
|
|
209
|
+
if (!units) return null
|
|
210
|
+
const offset = block.loc.start.offset
|
|
211
|
+
for (const u of units) {
|
|
212
|
+
u.span = { start: u.span.start + offset, end: u.span.end + offset }
|
|
213
|
+
}
|
|
214
|
+
return units
|
|
215
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/rules-lang-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"stylelint": "^17.6.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@7n/rules": ">=1.27.0"
|
|
65
|
+
"@7n/rules": ">=1.27.0",
|
|
66
|
+
"vue": "^3.0.0"
|
|
66
67
|
},
|
|
67
68
|
"publishConfig": {
|
|
68
69
|
"access": "public"
|
|
@@ -70,5 +71,10 @@
|
|
|
70
71
|
"engines": {
|
|
71
72
|
"bun": ">=1.3",
|
|
72
73
|
"node": ">=24"
|
|
74
|
+
},
|
|
75
|
+
"peerDependenciesMeta": {
|
|
76
|
+
"vue": {
|
|
77
|
+
"optional": true
|
|
78
|
+
}
|
|
73
79
|
}
|
|
74
80
|
}
|