@7n/rules 1.32.2 → 1.33.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 +6 -0
- package/bin/n-rules.js +17 -7
- package/package.json +1 -1
- package/scripts/lib/docs/resolve-plugins.md +1 -1
- package/scripts/lib/resolve-plugins.mjs +90 -10
package/CHANGELOG.md
CHANGED
package/bin/n-rules.js
CHANGED
|
@@ -365,9 +365,11 @@ async function readConfig(paths = {}) {
|
|
|
365
365
|
}
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
// Плагіни: явне поле plugins конфігу
|
|
369
|
-
//
|
|
370
|
-
|
|
368
|
+
// Плагіни: явне поле plugins конфігу (з per-категорійним backfill автодетекту — ADR
|
|
369
|
+
// 260719-2154) або повний автодетект, коли поля нема; sync-контекст — з установкою devDep.
|
|
370
|
+
// Результат записуємо у конфіг (нижче), щоб hook/lint не залежали від детекту й мережі.
|
|
371
|
+
const declaredPlugins = Array.isArray(rawConfig?.plugins) ? rawConfig.plugins : null
|
|
372
|
+
const resolvedPlugins = resolvePluginList(cwd(), rawConfig)
|
|
371
373
|
const rulesDirs = resolveRulesDirs(cwd(), rawConfig, bundledRulesDir).map(d => d.rulesDir)
|
|
372
374
|
const { names: availableRules } = await aggregateRuleSources(rulesDirs)
|
|
373
375
|
const availableSkills = await discoverBundledSkillNames(bundledSkillsDir)
|
|
@@ -442,9 +444,17 @@ async function readConfig(paths = {}) {
|
|
|
442
444
|
if (merged['disable-skills']?.length) {
|
|
443
445
|
normalized['disable-skills'] = merged['disable-skills']
|
|
444
446
|
}
|
|
445
|
-
if (!('plugins' in parsedConfig)
|
|
446
|
-
|
|
447
|
-
|
|
447
|
+
if (!('plugins' in parsedConfig)) {
|
|
448
|
+
if (resolvedPlugins.length > 0) {
|
|
449
|
+
normalized.plugins = resolvedPlugins
|
|
450
|
+
console.log(`🔌 Автодетект плагінів: ${resolvedPlugins.join(', ')} — записано у ${CONFIG_FILE}\n`)
|
|
451
|
+
}
|
|
452
|
+
} else if (declaredPlugins && JSON.stringify(resolvedPlugins) !== JSON.stringify(declaredPlugins)) {
|
|
453
|
+
// per-категорійний backfill (resolvePluginList, ADR 260719-2154) додав плагін
|
|
454
|
+
// категорії, відсутньої в явному plugins — фіксуємо результат у конфізі.
|
|
455
|
+
normalized.plugins = resolvedPlugins
|
|
456
|
+
const added = resolvedPlugins.filter(p => !declaredPlugins.includes(p))
|
|
457
|
+
console.log(`🔌 Доповнено plugins у ${CONFIG_FILE} (${added.join(', ')}) — категорія не була задекларована\n`)
|
|
448
458
|
}
|
|
449
459
|
return sortConfigIdArrays(normalized)
|
|
450
460
|
}
|
|
@@ -465,7 +475,7 @@ async function readConfig(paths = {}) {
|
|
|
465
475
|
$schema: CONFIG_SCHEMA_URL,
|
|
466
476
|
rules: autoDetectedRules.rules,
|
|
467
477
|
skills: autoDetectedSkills.skills,
|
|
468
|
-
...(
|
|
478
|
+
...(resolvedPlugins.length > 0 && { plugins: resolvedPlugins })
|
|
469
479
|
})
|
|
470
480
|
await writeFile(configPath, `${JSON.stringify(defaultConfig, null, 2)}\n`, 'utf8')
|
|
471
481
|
console.log(
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: resolve-plugins.mjs
|
|
4
4
|
resource: npm/scripts/lib/resolve-plugins.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 2bac9b49
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
Резолв плагінів @7n/rules: визначає, які пакети-плагіни активні у проєкті, де їхні `rules/`-каталоги, які capabilities вони надають і які handlers надають.
|
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
* Резолв плагінів `@7n/rules`: які пакети-плагіни активні у проєкті, де їхні `rules/`,
|
|
3
3
|
* які capabilities вони дають і які handlers надають.
|
|
4
4
|
*
|
|
5
|
-
* Джерело правди — поле `plugins: string[]` у `.n-rules.json`
|
|
6
|
-
*
|
|
7
|
-
* (
|
|
8
|
-
*
|
|
5
|
+
* Джерело правди — поле `plugins: string[]` у `.n-rules.json`. Явний `[]` = «плагіни
|
|
6
|
+
* вимкнено» (автодетект не застосовується). Якщо поля немає взагалі — повний автодетект
|
|
7
|
+
* (`detectPluginsFromRepo`). Якщо `plugins` непорожній і складається **виключно** з пакетів
|
|
8
|
+
* за конвенцією `@7n/rules-<category>-<name>` (напр. `ci`, `lang`) — автодетект домішує
|
|
9
|
+
* лише ті категорії, яких у списку немає (ADR `260719-2154-per-category-автодетект-плагінів`);
|
|
10
|
+
* будь-який сторонній (не `@7n/rules-*`) пакет у списку вимикає backfill повністю — змішаний
|
|
11
|
+
* чи повністю кастомний список означає ручне керування, без сюрпризів. Файлові сигнали
|
|
12
|
+
* автодетекту: `.github/workflows/*.yml` → `@7n/rules-ci-github`; `azure-pipelines.yml` →
|
|
13
|
+
* `@7n/rules-ci-azure`, а без них — fallback за `repository.url` кореневого package.json
|
|
14
|
+
* (`github.com` / `dev.azure.com`).
|
|
9
15
|
*
|
|
10
16
|
* Установка: `ensurePluginInstalled` — плагін стає devDependency через `bun add -d` (bun сам
|
|
11
17
|
* резолвить актуальну версію; зміна видима у diff package.json). Фейл установки (offline,
|
|
@@ -153,18 +159,91 @@ export function detectPluginsFromRepo(projectRoot) {
|
|
|
153
159
|
return out
|
|
154
160
|
}
|
|
155
161
|
|
|
162
|
+
/** Naming convention плагінів, які розпізнає автодетект: `@7n/rules-<category>-<name>`. */
|
|
163
|
+
const PLUGIN_CATEGORY_RE = /^@7n\/rules-([a-z0-9]+)-/u
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Категорія плагіна за naming convention `@7n/rules-<category>-<name>` (напр. `ci`, `lang`).
|
|
167
|
+
* `null` — пакет поза цією конвенцією (сторонній/кастомний плагін); такий пакет ніколи не
|
|
168
|
+
* зʼявляється сам через автодетект і, якщо присутній у явному `config.plugins`, вимикає
|
|
169
|
+
* per-категорійний backfill для всього списку (див. `resolvePluginList`).
|
|
170
|
+
* @param {string} pkg npm-ім'я плагіна
|
|
171
|
+
* @returns {string | null} категорія або null
|
|
172
|
+
*/
|
|
173
|
+
export function pluginCategory(pkg) {
|
|
174
|
+
const m = PLUGIN_CATEGORY_RE.exec(pkg)
|
|
175
|
+
return m ? m[1] : null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Усі категорії, які реально може повернути `detectPluginsFromRepo` — для short-circuit коли declared вже покриває все. */
|
|
179
|
+
const ALL_KNOWN_CATEGORIES = new Set(
|
|
180
|
+
[...Object.values(KNOWN_CI_PLUGINS), ...Object.values(KNOWN_LANG_PLUGINS).map(l => l.pkg)]
|
|
181
|
+
.map(pkg => pluginCategory(pkg))
|
|
182
|
+
.filter(c => c !== null)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
/** Кеш `resolvePluginList` на процес: `root::declared-json` → результат (щоб не дублювати warning). */
|
|
186
|
+
const PLUGIN_LIST_CACHE = new Map()
|
|
187
|
+
|
|
156
188
|
/**
|
|
157
|
-
* Список плагінів проєкту: явний `config.plugins`
|
|
189
|
+
* Список плагінів проєкту: явний `config.plugins` або автодетект.
|
|
190
|
+
*
|
|
191
|
+
* Явний `plugins` непорожній і складається **виключно** з пакетів `@7n/rules-<category>-*` —
|
|
192
|
+
* автодетект домішує лише категорії, відсутні в списку (ADR
|
|
193
|
+
* `260719-2154-per-category-автодетект-плагінів`); категорія, присутня хоч одним пакетом,
|
|
194
|
+
* лишається зафіксованою користувачем. Якщо `plugins` містить хоча б один сторонній
|
|
195
|
+
* (не `@7n/rules-*`) пакет — це сигнал ручного керування, backfill вимикається повністю
|
|
196
|
+
* (як і раніше: список повертається як є). Явний `[]` — «плагіни вимкнено», без backfill.
|
|
197
|
+
* Поле відсутнє взагалі — повний автодетект.
|
|
198
|
+
*
|
|
199
|
+
* Результат кешується на процес за `(projectRoot, declared)` — виклик з `resolvePlugins`
|
|
200
|
+
* (через `resolveRulesDirs` тощо) і прямий виклик у sync-CLI інакше дублювали б і файловий
|
|
201
|
+
* скан, і warning про backfill.
|
|
158
202
|
* @param {string} projectRoot корінь репозиторію
|
|
159
203
|
* @param {{ plugins?: unknown } | null | undefined} config розпарсений `.n-rules.json` (може бути відсутній)
|
|
204
|
+
* @param {{ quiet?: boolean }} [options] `quiet:true` — без warning-у про плагін, доданий автодетектом (hot-path);
|
|
205
|
+
* ігнорується при cache hit — warning друкується щонайбільше раз на `(root, declared)` за процес
|
|
160
206
|
* @returns {string[]} npm-імена плагінів
|
|
161
207
|
*/
|
|
162
|
-
export function resolvePluginList(projectRoot, config) {
|
|
208
|
+
export function resolvePluginList(projectRoot, config, options = {}) {
|
|
209
|
+
const root = resolve(projectRoot)
|
|
163
210
|
const declared = config?.plugins
|
|
164
|
-
|
|
165
|
-
|
|
211
|
+
const cacheKey = `${root}::${Array.isArray(declared) ? JSON.stringify(declared) : '∅'}`
|
|
212
|
+
const cached = PLUGIN_LIST_CACHE.get(cacheKey)
|
|
213
|
+
if (cached) return cached
|
|
214
|
+
|
|
215
|
+
const result = computePluginList(root, declared, options)
|
|
216
|
+
PLUGIN_LIST_CACHE.set(cacheKey, result)
|
|
217
|
+
return result
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Обчислення `resolvePluginList` без кешу (винесено окремо, щоб кеш-обгортка лишалась тонкою).
|
|
222
|
+
* @param {string} root абсолютний корінь репозиторію (вже пройшов через `resolve()`)
|
|
223
|
+
* @param {unknown} declared сире значення `config.plugins`
|
|
224
|
+
* @param {{ quiet?: boolean }} options прокинуті опції виклику
|
|
225
|
+
* @returns {string[]} npm-імена плагінів
|
|
226
|
+
*/
|
|
227
|
+
function computePluginList(root, declared, options) {
|
|
228
|
+
if (!Array.isArray(declared)) return detectPluginsFromRepo(root)
|
|
229
|
+
|
|
230
|
+
const names = declared.filter(p => typeof p === 'string' && p.trim() !== '')
|
|
231
|
+
if (names.length === 0) return names
|
|
232
|
+
|
|
233
|
+
const declaredCategories = new Set(names.map(pkg => pluginCategory(pkg)))
|
|
234
|
+
// Хоч один сторонній пакет у списку — не вгадуємо намір, повертаємо як є.
|
|
235
|
+
if (declaredCategories.has(null)) return names
|
|
236
|
+
// Усі відомі категорії вже покриті явним списком — не марнуємо файлові сигнали.
|
|
237
|
+
if ([...ALL_KNOWN_CATEGORIES].every(c => declaredCategories.has(c))) return names
|
|
238
|
+
|
|
239
|
+
const missing = detectPluginsFromRepo(root).filter(pkg => !declaredCategories.has(pluginCategory(pkg)))
|
|
240
|
+
if (missing.length > 0 && options.quiet !== true) {
|
|
241
|
+
const word = missing.length > 1 ? 'плагіни' : 'плагін'
|
|
242
|
+
console.warn(
|
|
243
|
+
`⚠️ Додано автодетектом ${word} ${missing.join(', ')} — категорія не задекларована явно в .n-rules.json\n`
|
|
244
|
+
)
|
|
166
245
|
}
|
|
167
|
-
return
|
|
246
|
+
return [...names, ...missing]
|
|
168
247
|
}
|
|
169
248
|
|
|
170
249
|
/**
|
|
@@ -240,7 +319,7 @@ function readPluginManifest(packageRoot) {
|
|
|
240
319
|
*/
|
|
241
320
|
export function resolvePlugins(projectRoot, config, options = {}) {
|
|
242
321
|
const root = resolve(projectRoot)
|
|
243
|
-
const names = resolvePluginList(root, config)
|
|
322
|
+
const names = resolvePluginList(root, config, { quiet: options.quiet })
|
|
244
323
|
const cacheKey = `${root}${names.join(',')}${options.allowInstall !== false}`
|
|
245
324
|
const cached = RESOLVE_CACHE.get(cacheKey)
|
|
246
325
|
if (cached) return cached
|
|
@@ -341,4 +420,5 @@ export function getHandlers(projectRoot, config, point) {
|
|
|
341
420
|
/** Скидає кеш резолву (для тестів). */
|
|
342
421
|
export function clearPluginResolveCache() {
|
|
343
422
|
RESOLVE_CACHE.clear()
|
|
423
|
+
PLUGIN_LIST_CACHE.clear()
|
|
344
424
|
}
|