@7n/rules-lang-js 0.7.0 → 0.8.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 +12 -0
- package/doc-files/docs/extractors.md +1 -2
- package/doc-files/docs/index.md +1 -0
- package/doc-files/docs/vue.md +34 -0
- package/doc-files/extractors.mjs +29 -4
- package/doc-files/vue.mjs +215 -0
- package/package.json +10 -3
- package/rules/js-bun-redis/lib/docs/redis-imports.md +2 -2
- package/rules/style/admin_table/docs/main.md +1 -1
- package/rules/style/admin_table/main.mjs +2 -2
- package/rules/style/gap/docs/main.md +1 -1
- package/rules/style/gap/main.mjs +2 -2
- package/rules/style/lint/docs/fix-lint.md +1 -1
- package/rules/style/lint/docs/main.md +1 -1
- package/rules/style/lint/fix-lint.mjs +1 -1
- package/rules/style/lint/main.mjs +18 -6
- package/rules/style/quasar_fixes/docs/main.md +1 -1
- package/rules/style/quasar_fixes/main.mjs +2 -2
- package/rules/style/tooling/docs/fix-tooling.md +1 -1
- package/rules/style/tooling/docs/main.md +1 -1
- package/rules/style/tooling/fix-tooling.mjs +1 -1
- package/rules/style/tooling/main.mjs +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-07-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- doc-files: Vue SFC-екстрактор (`<script setup>`) — extractFactsVue/extractUnitsVue через optional peer vue/compiler-sfc; props/emits/expose/слоти як публічний контракт, юніти зі span-корекцією (ADR 260719-2155)
|
|
8
|
+
|
|
9
|
+
## [0.7.1] - 2026-07-20
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- style/lint: stylelint — задекларована залежність плагіна (раніше резолвилась лише транзитивно у цьому монорепо через @nitra/stylelint-config); відсутність тула тепер дає видимий warn-diagnostic замість мовчазного no-op (незалежний консюмер бачив би 'зелений' style-лінт, який насправді нічого не перевіряв)
|
|
14
|
+
|
|
3
15
|
## [0.7.0] - 2026-07-19
|
|
4
16
|
|
|
5
17
|
### Added
|
|
@@ -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: 0519b5be
|
|
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
|
@@ -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,6 @@
|
|
|
1
1
|
/** @see ./docs/extractors.md */
|
|
2
2
|
import { extractUnitsJs } from './units-js.mjs'
|
|
3
|
+
import { extractFactsVue, extractUnitsVue } from './vue.mjs'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Мовний doc-files-екстрактор JS-екосистеми для конвеєра `@7n/rules`
|
|
@@ -249,6 +250,17 @@ function extractMarkers(src) {
|
|
|
249
250
|
skips: [...skips]
|
|
250
251
|
}
|
|
251
252
|
}
|
|
253
|
+
/** JS-хелпери, які Vue-екстрактор переюзає над текстом script-блоку SFC. */
|
|
254
|
+
const VUE_HELPERS = {
|
|
255
|
+
extractFileHeader,
|
|
256
|
+
extractExports,
|
|
257
|
+
extractImports,
|
|
258
|
+
extractInternalSymbols,
|
|
259
|
+
extractLocalSymbols,
|
|
260
|
+
extractMarkers,
|
|
261
|
+
parseJsDoc
|
|
262
|
+
}
|
|
263
|
+
|
|
252
264
|
/**
|
|
253
265
|
* Головний екстрактор: код файлу → факт-лист.
|
|
254
266
|
* @param {string} src вміст файлу
|
|
@@ -257,6 +269,7 @@ function extractMarkers(src) {
|
|
|
257
269
|
*/
|
|
258
270
|
export function extractFacts(src, relPath) {
|
|
259
271
|
const lang = relPath.split('.').pop()
|
|
272
|
+
if (lang === 'vue') return extractFactsVue(src, relPath, VUE_HELPERS)
|
|
260
273
|
if (!['js', 'mjs', 'ts'].includes(lang)) {
|
|
261
274
|
return { relPath, lang, unsupported: true, header: '', exports: [], imports: {}, markers: {} }
|
|
262
275
|
}
|
|
@@ -272,17 +285,29 @@ export function extractFacts(src, relPath) {
|
|
|
272
285
|
}
|
|
273
286
|
}
|
|
274
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Юніти: `.vue` — зі script-блоку SFC (span-и в координатах файлу), решта — oxc AST.
|
|
290
|
+
* @param {string} src вміст файлу
|
|
291
|
+
* @param {string} relPath шлях (вибір мови)
|
|
292
|
+
* @returns {Array<object>|null} юніти або null
|
|
293
|
+
*/
|
|
294
|
+
function extractUnits(src, relPath) {
|
|
295
|
+
if (relPath.toLowerCase().endsWith('.vue')) return extractUnitsVue(src, relPath, extractUnitsJs)
|
|
296
|
+
return extractUnitsJs(src, relPath)
|
|
297
|
+
}
|
|
298
|
+
|
|
275
299
|
/**
|
|
276
300
|
* Default-експорт для handler-модуля extension-point `doc-files`.
|
|
277
|
-
* `.vue`
|
|
278
|
-
*
|
|
279
|
-
*
|
|
301
|
+
* `.vue` (SFC зі `<script setup>`) парситься через optional peer `vue/compiler-sfc`
|
|
302
|
+
* (ADR 260719-2155): props/emits/expose/слоти → факт-лист; без peer чи script-блоку —
|
|
303
|
+
* fallback `unsupported` (whole-file шлях, як до впровадження).
|
|
304
|
+
* @type {{ id: string, extensions: string[], extractFacts: typeof extractFacts, extractUnits: typeof extractUnits }}
|
|
280
305
|
*/
|
|
281
306
|
const jsDocFilesExtractor = {
|
|
282
307
|
id: 'js',
|
|
283
308
|
extensions: ['.js', '.mjs', '.ts', '.vue'],
|
|
284
309
|
extractFacts,
|
|
285
|
-
extractUnits
|
|
310
|
+
extractUnits
|
|
286
311
|
}
|
|
287
312
|
|
|
288
313
|
export default jsDocFilesExtractor
|
|
@@ -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.8.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",
|
|
@@ -58,10 +58,12 @@
|
|
|
58
58
|
"jscpd": "^5.0.11",
|
|
59
59
|
"knip": "^6.22.0",
|
|
60
60
|
"oxc-parser": "^0.137.0",
|
|
61
|
-
"oxlint": "^1.68.0"
|
|
61
|
+
"oxlint": "^1.68.0",
|
|
62
|
+
"stylelint": "^17.6.0"
|
|
62
63
|
},
|
|
63
64
|
"peerDependencies": {
|
|
64
|
-
"@7n/rules": ">=1.27.0"
|
|
65
|
+
"@7n/rules": ">=1.27.0",
|
|
66
|
+
"vue": "^3.0.0"
|
|
65
67
|
},
|
|
66
68
|
"publishConfig": {
|
|
67
69
|
"access": "public"
|
|
@@ -69,5 +71,10 @@
|
|
|
69
71
|
"engines": {
|
|
70
72
|
"bun": ">=1.3",
|
|
71
73
|
"node": ">=24"
|
|
74
|
+
},
|
|
75
|
+
"peerDependenciesMeta": {
|
|
76
|
+
"vue": {
|
|
77
|
+
"optional": true
|
|
78
|
+
}
|
|
72
79
|
}
|
|
73
80
|
}
|
|
@@ -122,7 +122,7 @@ export function findRedisImportsInText(
|
|
|
122
122
|
|
|
123
123
|
### Внутрішні (репозиторій)
|
|
124
124
|
|
|
125
|
-
Імпорт із `'
|
|
125
|
+
Імпорт із `'@7n/rules/scripts/utils/ast-scan-utils.mjs'` (ядро) — спільні утиліти AST-сканера, які використовують правила з декількох плагінів:
|
|
126
126
|
|
|
127
127
|
- `dynamicImportModule(node)` — якщо вузол AST є динамічним `import('mod')`, повертає рядок `'mod'`, інакше `null`/`undefined`.
|
|
128
128
|
- `langFromPath(path)` — обчислює `lang` для `oxc-parser` за розширенням шляху.
|
|
@@ -198,7 +198,7 @@ import x from 'my-redis-helpers' // довільний сторонній пак
|
|
|
198
198
|
Контрольний перелік, за яким можна **відтворити** функціонал модуля «з нуля»:
|
|
199
199
|
|
|
200
200
|
1. Створити файл `redis-imports.mjs`, що імпортує `parseSync` з `oxc-parser`.
|
|
201
|
-
2. Імпортувати з `'
|
|
201
|
+
2. Імпортувати з `'@7n/rules/scripts/utils/ast-scan-utils.mjs'` шість утиліт: `dynamicImportModule`, `langFromPath`, `normalizeSnippet`, `offsetToLine`, `requireCallModule`, `walkAstWithAncestors`.
|
|
202
202
|
3. Оголосити константу `SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u`.
|
|
203
203
|
4. Оголосити константу `FORBIDDEN_MODULE_NAMES` як `Set` із елементами: `'ioredis'`, `'node-redis'`, `'redis'`, `'@redis/client'`, `'@redis/json'`, `'@redis/search'`, `'@redis/time-series'`, `'@redis/bloom'`.
|
|
204
204
|
5. Реалізувати приватну функцію `isForbiddenRedisModule(mod)`: повертає `true`, якщо `FORBIDDEN_MODULE_NAMES.has(mod)` **або** `mod.startsWith('ioredis/')` **або** `mod.startsWith('redis/')` **або** `mod.startsWith('@redis/')`.
|
|
@@ -13,8 +13,8 @@ const DEFINITION_RE = /\.n-admin-table\b/u
|
|
|
13
13
|
* клас `n-admin-table`, він має бути визначений хоч в одному `.scss`/`.css`/`.vue`
|
|
14
14
|
* (guide: `admin_table.mdc`). Для усунення false positives — крос-файлова перевірка,
|
|
15
15
|
* тож потребує whole-repo сканування незалежно від `ctx.files`.
|
|
16
|
-
* @param {import('
|
|
17
|
-
* @returns {Promise<import('
|
|
16
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону (cwd)
|
|
17
|
+
* @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат зі зібраними violations
|
|
18
18
|
*/
|
|
19
19
|
export async function lint(ctx) {
|
|
20
20
|
const reporter = createViolationReporter(ctx)
|
package/rules/style/gap/main.mjs
CHANGED
|
@@ -13,8 +13,8 @@ const DEFINITION_RE = /\.n-gap-(xs|sm|md|lg)\b/gu
|
|
|
13
13
|
* використаний у `.vue`, має бути визначений хоч в одному `.scss`/`.css`/`.vue`
|
|
14
14
|
* (guide: `gap.mdc`). Крос-файлова перевірка usage↔definition — потребує whole-repo
|
|
15
15
|
* сканування незалежно від `ctx.files`.
|
|
16
|
-
* @param {import('
|
|
17
|
-
* @returns {Promise<import('
|
|
16
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону (cwd)
|
|
17
|
+
* @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат зі зібраними violations
|
|
18
18
|
*/
|
|
19
19
|
export async function lint(ctx) {
|
|
20
20
|
const reporter = createViolationReporter(ctx)
|
|
@@ -40,7 +40,7 @@ async function listStyleFiles(cwd, ctxFiles) {
|
|
|
40
40
|
return (r.stdout ?? '').split('\0').filter(Boolean)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/** @type {import('
|
|
43
|
+
/** @type {import('@7n/rules/scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
|
|
44
44
|
export const patterns = [
|
|
45
45
|
{
|
|
46
46
|
id: 'style-stylelint-fix',
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lint-поверхня style: read-only detector (stylelint без --fix) для css/scss/vue.
|
|
3
|
-
* Per-file (ctx.files) або весь проєкт (ctx.files === undefined).
|
|
4
|
-
*
|
|
3
|
+
* Per-file (ctx.files) або весь проєкт (ctx.files === undefined). `stylelint` —
|
|
4
|
+
* задекларована залежність плагіна (справжній `npm install` завжди його ставить);
|
|
5
|
+
* якщо резолв все ж не вдався (побитий install, hoisting-аномалія) — крок
|
|
6
|
+
* пропускається з видимим warn-diagnostic, а не мовчки (без npx-автовстановлення).
|
|
5
7
|
*/
|
|
6
8
|
import { existsSync } from 'node:fs'
|
|
7
9
|
import { join } from 'node:path'
|
|
@@ -33,8 +35,8 @@ export function resolveStylelint(cwd) {
|
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Detector style/lint (read-only).
|
|
36
|
-
* @param {import('
|
|
37
|
-
* @returns {Promise<import('
|
|
38
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону (cwd, files)
|
|
39
|
+
* @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат зі зібраними violations
|
|
38
40
|
*/
|
|
39
41
|
export async function lint(ctx) {
|
|
40
42
|
const reporter = createViolationReporter(ctx)
|
|
@@ -54,8 +56,18 @@ export async function lint(ctx) {
|
|
|
54
56
|
|
|
55
57
|
const stylelint = resolveStylelint(cwd)
|
|
56
58
|
if (!stylelint) {
|
|
57
|
-
// stylelint
|
|
58
|
-
|
|
59
|
+
// stylelint — задекларована залежність плагіна, тож відсутність тут аномальна
|
|
60
|
+
// (побитий install, hoisting) — крок пропущено з видимим warn, не мовчки.
|
|
61
|
+
const result = reporter.result()
|
|
62
|
+
result.diagnostics = [
|
|
63
|
+
{
|
|
64
|
+
level: 'warn',
|
|
65
|
+
message:
|
|
66
|
+
'lint-style: `stylelint` не резолвиться (ні node_modules/.bin, ні PATH) — CSS/SCSS/Vue-стилі НЕ перевірені ' +
|
|
67
|
+
'цим прогоном (style.mdc). `stylelint` — залежність @7n/rules-lang-js; переустанови плагін, якщо бачиш це.'
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
return result
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
const r = await spawnAsync(stylelint, targets, { cwd, shell: false })
|
|
@@ -21,8 +21,8 @@ const FIXES = [
|
|
|
21
21
|
* `<q-scroll-area>` або `<q-tooltip>`, у `.scss`/`.css`/`.vue` має бути відповідний
|
|
22
22
|
* CSS-фікс (guide: `quasar_fixes.mdc`). Крос-файлова перевірка usage↔definition —
|
|
23
23
|
* потребує whole-repo сканування незалежно від `ctx.files`.
|
|
24
|
-
* @param {import('
|
|
25
|
-
* @returns {Promise<import('
|
|
24
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону (cwd)
|
|
25
|
+
* @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат зі зібраними violations
|
|
26
26
|
*/
|
|
27
27
|
export async function lint(ctx) {
|
|
28
28
|
const reporter = createViolationReporter(ctx)
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: plugins/lang-js/rules/style/tooling/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: aeb36134
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
8
|
score: 90
|
|
9
9
|
issues: anchor-miss:extensions.json,anchor-miss:settings.json,judge:inaccurate:0.99
|
|
@@ -12,7 +12,7 @@ const STYLELINTIGNORE_MISSING_RE = /\.stylelintignore не існує/u
|
|
|
12
12
|
const STYLELINTIGNORE_NO_DIST_RE = /\.stylelintignore не містить рядка dist\//u
|
|
13
13
|
const NO_STYLELINT_CONFIG_RE = /Немає конфігу stylelint/u
|
|
14
14
|
|
|
15
|
-
/** @type {import('
|
|
15
|
+
/** @type {import('@7n/rules/scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
|
|
16
16
|
export const patterns = [
|
|
17
17
|
{
|
|
18
18
|
id: 'style-stylelintignore-create',
|
|
@@ -45,8 +45,8 @@ async function checkStylelintConfigPresence(reporter, cwd) {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Перевіряє відповідність проєкту правилам style.mdc
|
|
48
|
-
* @param {import('
|
|
49
|
-
* @returns {Promise<import('
|
|
48
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону (cwd тощо)
|
|
49
|
+
* @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат зі зібраними violations
|
|
50
50
|
*/
|
|
51
51
|
export async function lint(ctx) {
|
|
52
52
|
const cwd = ctx.cwd
|