@rt-tools/agent-kit 0.8.3 → 0.9.1
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/README.md +21 -0
- package/assets/checks/board.github.mjs +48 -1
- package/assets/checks/check-board.github.mjs +84 -1
- package/assets/checks/check-lib-layers.mjs +13 -524
- package/assets/checks/check-specs.mjs +11 -782
- package/assets/checks/check-styles.mjs +185 -15
- package/assets/checks/lib-boundaries.mjs +143 -0
- package/assets/checks/lib-common.mjs +149 -0
- package/assets/checks/lib-domains.mjs +205 -0
- package/assets/checks/lib-manifests.mjs +60 -0
- package/assets/checks/lib-reexports.mjs +101 -0
- package/assets/checks/rt-kit-checks.config.mjs +17 -0
- package/assets/checks/spec-anchors.mjs +297 -0
- package/assets/checks/spec-common.mjs +222 -0
- package/assets/checks/spec-contract.mjs +152 -0
- package/assets/checks/spec-scenarios.mjs +201 -0
- package/assets/defaults/project.sh +8 -0
- package/assets/hooks/git-guard-push-tests.sh +8 -4
- package/assets/hooks/skill-gate.sh +1 -1
- package/assets/hooks/sql-guard-parse.sh +187 -0
- package/assets/hooks/sql-guard-request.sh +117 -0
- package/assets/hooks/sql-guard-target.sh +134 -0
- package/assets/hooks/sql-guard-write.sh +212 -0
- package/assets/hooks/sql-guard.sh +26 -596
- package/assets/hooks/waiting-turn-guard.sh +42 -13
- package/assets/laws/delivery.md +7 -0
- package/assets/laws/work-conduct.md +9 -0
- package/assets/patterns/admin-lists-screen.md +25 -14
- package/assets/patterns/admin-nav-item.md +1 -1
- package/assets/patterns/component-structure-new.md +1 -1
- package/assets/patterns/entity-aside.md +4 -2
- package/assets/patterns/observability-record.md +9 -0
- package/assets/patterns/shared-code-new.md +2 -2
- package/assets/patterns/task-flow-close.md +7 -1
- package/assets/rules/git-workflow.azure.md +7 -0
- package/assets/rules/git-workflow.github.md +41 -0
- package/assets/rules/git-workflow.gitlab.md +7 -0
- package/assets/rules/lib-layers.md +4 -0
- package/assets/rules/lists.md +10 -10
- package/assets/rules/shared-code.md +1 -1
- package/assets/rules/task-flow.md +47 -7
- package/assets/rules/testing.md +31 -0
- package/assets/rules/typescript-conventions.md +7 -0
- package/assets/skills/agent-kit.md +36 -0
- package/assets/templates/proposal.md +21 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +117 -87
- package/bin/agent-kit.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/lib/argv.d.ts.map +1 -1
- package/lib/argv.js +6 -4
- package/lib/argv.js.map +1 -1
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +2 -1
- package/lib/assets.js.map +1 -1
- package/lib/cargo.d.ts +20 -0
- package/lib/cargo.d.ts.map +1 -1
- package/lib/cargo.js.map +1 -1
- package/lib/cascade.d.ts +55 -0
- package/lib/cascade.d.ts.map +1 -0
- package/lib/cascade.js +131 -0
- package/lib/cascade.js.map +1 -0
- package/lib/catalog.d.ts +0 -75
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +44 -127
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +152 -85
- package/lib/commands.js.map +1 -1
- package/lib/companion.d.ts.map +1 -1
- package/lib/companion.js +5 -5
- package/lib/companion.js.map +1 -1
- package/lib/config.d.ts +2 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +7 -5
- package/lib/config.js.map +1 -1
- package/lib/enroll.d.ts +64 -0
- package/lib/enroll.d.ts.map +1 -0
- package/lib/enroll.js +170 -0
- package/lib/enroll.js.map +1 -0
- package/lib/freshness.d.ts.map +1 -1
- package/lib/freshness.js +31 -17
- package/lib/freshness.js.map +1 -1
- package/lib/hooks-map.d.ts +30 -0
- package/lib/hooks-map.d.ts.map +1 -1
- package/lib/hooks-map.js +80 -18
- package/lib/hooks-map.js.map +1 -1
- package/lib/integrity.d.ts +1 -2
- package/lib/integrity.d.ts.map +1 -1
- package/lib/integrity.js +0 -1
- package/lib/integrity.js.map +1 -1
- package/lib/observations.d.ts.map +1 -1
- package/lib/observations.js +25 -12
- package/lib/observations.js.map +1 -1
- package/lib/order.d.ts +10 -0
- package/lib/order.d.ts.map +1 -0
- package/lib/order.js +14 -0
- package/lib/order.js.map +1 -0
- package/lib/picker.d.ts.map +1 -1
- package/lib/picker.js +8 -2
- package/lib/picker.js.map +1 -1
- package/lib/plan.js +1 -1
- package/lib/plan.js.map +1 -1
- package/lib/proposals.d.ts.map +1 -1
- package/lib/proposals.js +25 -8
- package/lib/proposals.js.map +1 -1
- package/lib/sections.js +1 -1
- package/lib/sections.js.map +1 -1
- package/lib/ship.d.ts.map +1 -1
- package/lib/ship.js +9 -1
- package/lib/ship.js.map +1 -1
- package/lib/shipment.d.ts.map +1 -1
- package/lib/shipment.js +14 -10
- package/lib/shipment.js.map +1 -1
- package/lib/snapshot.d.ts.map +1 -1
- package/lib/snapshot.js +2 -1
- package/lib/snapshot.js.map +1 -1
- package/lib/stamp.js +1 -1
- package/lib/stamp.js.map +1 -1
- package/lib/sync.d.ts +12 -2
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +11 -10
- package/lib/sync.js.map +1 -1
- package/lib/vars.d.ts.map +1 -1
- package/lib/vars.js +2 -3
- package/lib/vars.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.9.1.tgz +0 -0
- package/rt-tools-agent-kit-0.8.3.tgz +0 -0
|
@@ -9,46 +9,24 @@
|
|
|
9
9
|
* контейнер та же пятёрка, что у общего
|
|
10
10
|
* бэкенд libs/api/<домен>/ api, data-access, feature, util
|
|
11
11
|
*
|
|
12
|
+
* Каждый предмет сверки живёт своим модулем рядом: доменная сетка и состав слоёв —
|
|
13
|
+
* `lib-domains.mjs`, файлы либы вместе с её именем, тегом и алиасом — `lib-manifests.mjs`,
|
|
14
|
+
* границы и списки зависимостей — `lib-boundaries.mjs`, барели и реэкспорты —
|
|
15
|
+
* `lib-reexports.mjs`, общее чтение дерева — `lib-common.mjs`. Здесь остаётся прогон: он
|
|
16
|
+
* собирает либы, зовёт по ним каждый предмет и складывает найденное в один перечень.
|
|
17
|
+
*
|
|
12
18
|
* Исключения перечислены в tools/lib-layers-allowlist.json.
|
|
13
19
|
* Ненулевой код возврата и перечень расхождений.
|
|
14
20
|
*/
|
|
15
|
-
import { existsSync
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
16
22
|
import { join } from 'node:path';
|
|
17
23
|
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* то есть отвечала «нарушений нет» там, где она не смотрела вовсе.
|
|
25
|
-
*/
|
|
26
|
-
const LIBS_ROOT = CONFIG.libsRoot;
|
|
27
|
-
const API_FAMILY = CONFIG.apiFamily;
|
|
28
|
-
const FLAT_LAYERS = ['api', 'data-access', 'ui', 'util'];
|
|
29
|
-
const FEATURE_DOMAIN_LAYERS = [...FLAT_LAYERS, 'feature', 'shell'].sort();
|
|
30
|
-
const COMMON_DOMAIN_LAYERS = [...FLAT_LAYERS, 'feature'].sort();
|
|
31
|
-
/** У бэкенда нет `ui` и `shell`: отдавать разметку и роутиться ему нечем */
|
|
32
|
-
const API_DOMAIN_LAYERS = ['api', 'data-access', 'feature', 'util'];
|
|
33
|
-
/** Где ищутся либы, оказавшиеся вне доменной сетки */
|
|
34
|
-
const LIB_ROOTS = [...FAMILIES, API_FAMILY].map((family) => `${LIBS_ROOT}/${family}`);
|
|
35
|
-
const REQUIRED_FILES = ['project.json', 'tsconfig.json', 'vitest.config.mts', 'src/index.ts'];
|
|
36
|
-
const BOUNDARIES_DIR = 'eslint/boundaries/domains';
|
|
37
|
-
|
|
38
|
-
const problems = [];
|
|
39
|
-
const report = (path, message) => problems.push(`${path}: ${message}`);
|
|
40
|
-
|
|
41
|
-
const readJson = (path) => JSON.parse(readFileSync(join(ROOT, path), 'utf8'));
|
|
42
|
-
const isDir = (path) => existsSync(join(ROOT, path)) && statSync(join(ROOT, path)).isDirectory();
|
|
43
|
-
const IGNORED_DIRS = ['src', 'node_modules'];
|
|
44
|
-
const dirsIn = (path) =>
|
|
45
|
-
isDir(path)
|
|
46
|
-
? readdirSync(join(ROOT, path), { withFileTypes: true })
|
|
47
|
-
.filter((entry) => entry.isDirectory() && !IGNORED_DIRS.includes(entry.name))
|
|
48
|
-
.map((entry) => entry.name)
|
|
49
|
-
.sort()
|
|
50
|
-
: [];
|
|
51
|
-
const isLib = (path) => existsSync(join(ROOT, path, 'project.json'));
|
|
24
|
+
import { checkBoundaries, checkCoreLibs, checkNoDependencyLibs } from './lib-boundaries.mjs';
|
|
25
|
+
import { LIB_ROOTS, isDir, problems, report } from './lib-common.mjs';
|
|
26
|
+
import { collectAllDomainLibs, collectApiDomainLibs, collectFlatLibs, collectStrayLibs } from './lib-domains.mjs';
|
|
27
|
+
import { checkAliases, checkLib } from './lib-manifests.mjs';
|
|
28
|
+
import { checkReexports } from './lib-reexports.mjs';
|
|
29
|
+
import { ROOT, allowlistOf, skipUnless } from './rt-kit-checks.config.mjs';
|
|
52
30
|
|
|
53
31
|
// Дерево без доменной сетки проверять нечем: раскладка либ есть не у всякого, кто берёт пакет.
|
|
54
32
|
// Пока пропуска не было, первая же установка получала отказ «нет tsconfig.base.json» — то есть
|
|
@@ -58,495 +36,6 @@ skipUnless(
|
|
|
58
36
|
`раскладки либ ${LIB_ROOTS.join(', ')} или файла tsconfig.base.json`
|
|
59
37
|
);
|
|
60
38
|
|
|
61
|
-
/**
|
|
62
|
-
* Список принятых долгов. Читается помощником настроек, а не напрямую: файла в свежем дереве
|
|
63
|
-
* нет вовсе, и прямое чтение роняло проверку отказом «нет такого файла» — то есть первая же
|
|
64
|
-
* установка получала поломку вместо отчёта о том, что долгов нет.
|
|
65
|
-
*/
|
|
66
|
-
const allowlist = readAllowlist('lib-layers');
|
|
67
|
-
const pathsOf = (key) => (allowlist[key] ?? []).map((exception) => exception.path);
|
|
68
|
-
|
|
69
|
-
/** Паттерн `<корень>/x/*` покрывает и сам каталог `<корень>/x`: исключение снимается целиком */
|
|
70
|
-
const matches = (patterns, path) =>
|
|
71
|
-
patterns.some((pattern) => {
|
|
72
|
-
if (!pattern.endsWith('/*')) {
|
|
73
|
-
return path === pattern;
|
|
74
|
-
}
|
|
75
|
-
const prefix = pattern.slice(0, -2);
|
|
76
|
-
return path === prefix || path.startsWith(`${prefix}/`);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
/** Каталог, который доменом не является вовсе: основание семейства, общие либы, бэкенд */
|
|
80
|
-
const isNotDomain = (path) => matches(pathsOf('notDomains'), path);
|
|
81
|
-
|
|
82
|
-
/** Домен старой раскладки: не проверяется ни как домен, ни как набор либ — уезжает целиком */
|
|
83
|
-
const isLegacyDomain = (path) => matches(pathsOf('legacyDomains'), path);
|
|
84
|
-
|
|
85
|
-
/** Либа старой раскладки внутри живого домена: домен проверяется, сама либа — нет */
|
|
86
|
-
const isLegacyLib = (path) => matches(pathsOf('legacyLibs'), path);
|
|
87
|
-
|
|
88
|
-
/** Домен, которому один заполненный слой разрешён: причина названа в списке исключений */
|
|
89
|
-
const isSingleLayerDomain = (path) => matches(pathsOf('singleLayerDomains'), path);
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Файлы либы, кроме бареля: барель есть у пустого слоя так же, как у заполненного, и
|
|
93
|
-
* пустой слой от заполненного по нему не отличить.
|
|
94
|
-
*/
|
|
95
|
-
function sourceCount(libPath) {
|
|
96
|
-
const inside = (dir) => {
|
|
97
|
-
let total = 0;
|
|
98
|
-
for (const entry of readdirSync(join(ROOT, dir), { withFileTypes: true })) {
|
|
99
|
-
if (entry.isDirectory()) {
|
|
100
|
-
if (!IGNORED_DIRS.includes(entry.name)) {
|
|
101
|
-
total += inside(`${dir}/${entry.name}`);
|
|
102
|
-
}
|
|
103
|
-
} else if (entry.name.endsWith('.ts')) {
|
|
104
|
-
total += 1;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return total;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
return isDir(`${libPath}/src/lib`) ? inside(`${libPath}/src/lib`) : 0;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Домен ли это. Домен заводится под предмет, и предмет виден по тому, что заполнен у
|
|
116
|
-
* домена не один слой: экраны и состояние, запросы и механика, выход к чужому и то,
|
|
117
|
-
* что мы отдаём. Заполнен ровно один — это слот, который заполнит следующая задача, и
|
|
118
|
-
* от домена он не отличим ничем, кроме намерения.
|
|
119
|
-
*
|
|
120
|
-
* Домен вносится строкой в список исключений вместе с причиной, и причина бывает
|
|
121
|
-
* настоящей: сборка PDF со своими шрифтами не едет ни в чей чужой граф зависимостей.
|
|
122
|
-
*/
|
|
123
|
-
function checkDomainIsFilled(domainPath, libs) {
|
|
124
|
-
if (isSingleLayerDomain(domainPath)) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const filled = libs.filter((libPath) => sourceCount(libPath) > 0);
|
|
128
|
-
if (libs.length > 1 && filled.length === 1) {
|
|
129
|
-
report(
|
|
130
|
-
domainPath,
|
|
131
|
-
`заполнен один слой — \`${filled[0].slice(domainPath.length + 1)}\`: домен от слота этим не отличить. ` +
|
|
132
|
-
'Либо содержимое переезжает в существующую либу, либо домен вносится строкой с причиной в `singleLayerDomains`'
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Пути, которые обход либ обязан пропустить */
|
|
138
|
-
const isIgnoredLib = (path) => isNotDomain(path) || isLegacyDomain(path) || isLegacyLib(path);
|
|
139
|
-
|
|
140
|
-
const projectName = (libPath) => libPath.replace(/^libs\//, '').replaceAll('/', '-');
|
|
141
|
-
const projectTag = (libPath) => `scope:${projectName(libPath)}`;
|
|
142
|
-
const importAlias = (libPath) => `${CONFIG.importScope}/${libPath.slice(`${LIBS_ROOT}/`.length)}`;
|
|
143
|
-
|
|
144
|
-
/** Собирает пути всех либ домена и попутно проверяет состав его слоёв */
|
|
145
|
-
function collectDomainLibs(domainPath, isCommon) {
|
|
146
|
-
const expected = isCommon ? COMMON_DOMAIN_LAYERS : FEATURE_DOMAIN_LAYERS;
|
|
147
|
-
const actual = dirsIn(domainPath);
|
|
148
|
-
|
|
149
|
-
const missing = expected.filter((layer) => !actual.includes(layer));
|
|
150
|
-
const extra = actual.filter((layer) => !expected.includes(layer));
|
|
151
|
-
if (missing.length > 0) {
|
|
152
|
-
report(domainPath, `нет слоёв: ${missing.join(', ')}`);
|
|
153
|
-
}
|
|
154
|
-
if (extra.length > 0) {
|
|
155
|
-
report(
|
|
156
|
-
domainPath,
|
|
157
|
-
`лишние каталоги: ${extra.join(', ')}${extra.includes('shell') && isCommon ? ' (у общего домена shell не бывает: он не роутится)' : ''}`
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const libs = actual.filter((layer) => FLAT_LAYERS.includes(layer) || layer === 'shell').map((layer) => `${domainPath}/${layer}`);
|
|
162
|
-
|
|
163
|
-
if (actual.includes('feature')) {
|
|
164
|
-
const featurePath = `${domainPath}/feature`;
|
|
165
|
-
if (isCommon) {
|
|
166
|
-
libs.push(featurePath);
|
|
167
|
-
} else {
|
|
168
|
-
const screens = dirsIn(featurePath).filter((screen) => isLib(`${featurePath}/${screen}`));
|
|
169
|
-
if (screens.length === 0) {
|
|
170
|
-
report(featurePath, 'у фичевого домена `feature` — каталог с либой на экран, здесь ни одной');
|
|
171
|
-
}
|
|
172
|
-
libs.push(...screens.map((screen) => `${featurePath}/${screen}`));
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
checkDomainIsFilled(domainPath, libs);
|
|
177
|
-
|
|
178
|
-
return libs;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Домены бэкенда: `<корень либ>/<семья бэкенда>/<домен>` с четырьмя слоями. `feature` бывает и либой,
|
|
183
|
-
* и каталогом с либой на proto-сервис — по тому же правилу, по которому у
|
|
184
|
-
* фичевого домена фронта `feature/<экран>`.
|
|
185
|
-
*
|
|
186
|
-
* Пустой слой — не дефект: `api` пуст у домена, который ни с кем чужим не
|
|
187
|
-
* говорит, `data-access` — у того, что не касается базы. Слой всё равно заводится
|
|
188
|
-
* либой, иначе граница домена объявлена не полностью.
|
|
189
|
-
*/
|
|
190
|
-
function collectApiDomainLibs() {
|
|
191
|
-
const libs = [];
|
|
192
|
-
|
|
193
|
-
for (const entry of dirsIn(`${LIBS_ROOT}/${API_FAMILY}`)) {
|
|
194
|
-
const domainPath = `${LIBS_ROOT}/${API_FAMILY}/${entry}`;
|
|
195
|
-
if (isNotDomain(domainPath) || isLegacyDomain(domainPath)) {
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const actual = dirsIn(domainPath);
|
|
200
|
-
const missing = API_DOMAIN_LAYERS.filter((layer) => !actual.includes(layer));
|
|
201
|
-
const extra = actual.filter((layer) => !API_DOMAIN_LAYERS.includes(layer));
|
|
202
|
-
if (missing.length > 0) {
|
|
203
|
-
report(domainPath, `нет слоёв: ${missing.join(', ')}`);
|
|
204
|
-
}
|
|
205
|
-
if (extra.length > 0) {
|
|
206
|
-
report(
|
|
207
|
-
domainPath,
|
|
208
|
-
`лишние каталоги: ${extra.join(', ')}${extra.includes('shell') || extra.includes('ui') ? ' (у бэкенда ui и shell не бывает)' : ''}`
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const domainLibs = actual
|
|
213
|
-
.filter((layer) => layer !== 'feature' && API_DOMAIN_LAYERS.includes(layer))
|
|
214
|
-
.map((layer) => `${domainPath}/${layer}`);
|
|
215
|
-
|
|
216
|
-
if (actual.includes('feature')) {
|
|
217
|
-
const featurePath = `${domainPath}/feature`;
|
|
218
|
-
if (isLib(featurePath)) {
|
|
219
|
-
domainLibs.push(featurePath);
|
|
220
|
-
} else {
|
|
221
|
-
const services = dirsIn(featurePath).filter((service) => isLib(`${featurePath}/${service}`));
|
|
222
|
-
if (services.length === 0) {
|
|
223
|
-
report(featurePath, '`feature` — либа или каталог с либой на proto-сервис, здесь ни того ни другого');
|
|
224
|
-
}
|
|
225
|
-
domainLibs.push(...services.map((service) => `${featurePath}/${service}`));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
checkDomainIsFilled(domainPath, domainLibs);
|
|
230
|
-
libs.push(...domainLibs);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return libs.sort();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** Все либы доменной сетки обоих семейств */
|
|
237
|
-
function collectAllDomainLibs() {
|
|
238
|
-
const libs = [];
|
|
239
|
-
|
|
240
|
-
for (const family of FAMILIES) {
|
|
241
|
-
for (const entry of dirsIn(`${LIBS_ROOT}/${family}`)) {
|
|
242
|
-
if (entry === 'common') {
|
|
243
|
-
for (const commonDomain of dirsIn(`${LIBS_ROOT}/${family}/common`)) {
|
|
244
|
-
libs.push(...collectDomainLibs(`${LIBS_ROOT}/${family}/common/${commonDomain}`, true));
|
|
245
|
-
}
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
const domainPath = `${LIBS_ROOT}/${family}/${entry}`;
|
|
249
|
-
if (isNotDomain(domainPath) || isLegacyDomain(domainPath)) {
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
libs.push(...collectDomainLibs(domainPath, false));
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return libs.sort();
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** Каждая либа на диске должна быть либой доменной сетки или значиться в allowlist */
|
|
260
|
-
function collectStrayLibs(knownLibs) {
|
|
261
|
-
const strays = [];
|
|
262
|
-
const walk = (path) => {
|
|
263
|
-
if (isLegacyDomain(path) || isNotDomain(path)) {
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
if (isLib(path)) {
|
|
267
|
-
if (!knownLibs.includes(path) && !isIgnoredLib(path)) {
|
|
268
|
-
strays.push(path);
|
|
269
|
-
}
|
|
270
|
-
// либа старой раскладки может держать внутри себя новые слои — обход продолжается
|
|
271
|
-
if (!isLegacyLib(path)) {
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
dirsIn(path).forEach((entry) => walk(`${path}/${entry}`));
|
|
276
|
-
};
|
|
277
|
-
LIB_ROOTS.forEach((root) => walk(root));
|
|
278
|
-
return strays;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Плоские либы бэкенда: `<корень либ>/<семья бэкенда>/<домен>`. Лесенки слоёв у них нет по
|
|
283
|
-
* устройству — домен это одна либа, — но всё остальное проверяется наравне с
|
|
284
|
-
* доменной сеткой. До сужения исключение по этому корню снимало с них и состав
|
|
285
|
-
* файлов, и тег, и алиас: новая либа заводилась без `vitest.config.mts`, и
|
|
286
|
-
* `nx test` по ней молча не гонял ни одной спеки.
|
|
287
|
-
*/
|
|
288
|
-
function collectFlatLibs() {
|
|
289
|
-
return (allowlist.flatLibRoots ?? [])
|
|
290
|
-
.flatMap((root) => dirsIn(root).map((entry) => `${root}/${entry}`))
|
|
291
|
-
.filter((path) => isLib(path))
|
|
292
|
-
.sort();
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function checkLib(libPath, { requirePrefix = true } = {}) {
|
|
296
|
-
for (const file of REQUIRED_FILES) {
|
|
297
|
-
if (!existsSync(join(ROOT, libPath, file))) {
|
|
298
|
-
report(libPath, `нет файла ${file}`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (!existsSync(join(ROOT, libPath, 'project.json'))) {
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const project = readJson(`${libPath}/project.json`);
|
|
307
|
-
if (project.name !== projectName(libPath)) {
|
|
308
|
-
report(libPath, `имя проекта «${project.name}» не совпадает с путём, ожидается «${projectName(libPath)}»`);
|
|
309
|
-
}
|
|
310
|
-
if (project.sourceRoot !== `${libPath}/src`) {
|
|
311
|
-
report(libPath, `sourceRoot «${project.sourceRoot}» не совпадает с путём`);
|
|
312
|
-
}
|
|
313
|
-
// Префикс — про селекторы компонентов, а у бэкенда компонентов нет
|
|
314
|
-
if (requirePrefix && project.prefix !== 'vm') {
|
|
315
|
-
report(libPath, `prefix «${project.prefix}» вместо обязательного «vm»`);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const tags = project.tags ?? [];
|
|
319
|
-
if (tags.length !== 1) {
|
|
320
|
-
report(libPath, `тегов ${tags.length}, а должен быть ровно один: ${projectTag(libPath)}`);
|
|
321
|
-
} else if (tags[0] !== projectTag(libPath)) {
|
|
322
|
-
report(libPath, `тег «${tags[0]}» не совпадает с путём, ожидается «${projectTag(libPath)}»`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function checkAliases(libs) {
|
|
327
|
-
const paths = readJson('tsconfig.base.json').compilerOptions.paths;
|
|
328
|
-
for (const libPath of libs) {
|
|
329
|
-
const alias = importAlias(libPath);
|
|
330
|
-
const target = `./${libPath}/src/index.ts`;
|
|
331
|
-
if (!paths[alias]) {
|
|
332
|
-
report(libPath, `в tsconfig.base.json нет алиаса ${alias}`);
|
|
333
|
-
} else if (paths[alias][0] !== target) {
|
|
334
|
-
report(libPath, `алиас ${alias} указывает на ${paths[alias][0]}, а не на ${target}`);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Либа должна быть описана ровно одним `sourceTag` и ровно в одном файле границ:
|
|
341
|
-
* описанная дважды либа означает два разных allowlist'а на неё, и какой победит —
|
|
342
|
-
* вопрос порядка сборки. В чужих allowlist'ах тег, наоборот, встречается сколько
|
|
343
|
-
* угодно раз: это перечисление тех, кому либа видна.
|
|
344
|
-
*/
|
|
345
|
-
function checkBoundaries(libs) {
|
|
346
|
-
if (!isDir(BOUNDARIES_DIR)) {
|
|
347
|
-
report(BOUNDARIES_DIR, 'каталога с файлами границ нет');
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const files = readdirSync(join(ROOT, BOUNDARIES_DIR)).filter((name) => name.endsWith('.config.mjs'));
|
|
352
|
-
const sources = files.map((name) => ({ name, text: readFileSync(join(ROOT, BOUNDARIES_DIR, name), 'utf8') }));
|
|
353
|
-
|
|
354
|
-
for (const libPath of libs) {
|
|
355
|
-
const tag = projectTag(libPath);
|
|
356
|
-
const declaration = `sourceTag: '${tag}'`;
|
|
357
|
-
const owners = sources.flatMap(({ name, text }) => {
|
|
358
|
-
const count = text.split(declaration).length - 1;
|
|
359
|
-
return count > 0 ? Array.from({ length: count }, () => name) : [];
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
if (owners.length === 0) {
|
|
363
|
-
report(libPath, `тег ${tag} не описан ни в одном файле ${BOUNDARIES_DIR}/`);
|
|
364
|
-
} else if (owners.length > 1) {
|
|
365
|
-
report(libPath, `тег ${tag} описан ${owners.length} раза: ${owners.join(', ')}`);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Либы, у которых список зависимостей обязан оставаться пустым. Барель такой либы
|
|
372
|
-
* уезжает в бандл бэкенда, а сборка API идёт без tree-shaking: любая зависимость
|
|
373
|
-
* расходится оттуда по всему графу.
|
|
374
|
-
*/
|
|
375
|
-
const NO_DEPENDENCY_TAGS = ['scope:common-util'];
|
|
376
|
-
|
|
377
|
-
function checkNoDependencyLibs() {
|
|
378
|
-
if (!isDir(BOUNDARIES_DIR)) {
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const files = readdirSync(join(ROOT, BOUNDARIES_DIR)).filter((name) => name.endsWith('.config.mjs'));
|
|
383
|
-
|
|
384
|
-
for (const tag of NO_DEPENDENCY_TAGS) {
|
|
385
|
-
const declaration = `sourceTag: '${tag}'`;
|
|
386
|
-
let described = false;
|
|
387
|
-
|
|
388
|
-
for (const name of files) {
|
|
389
|
-
const text = readFileSync(join(ROOT, BOUNDARIES_DIR, name), 'utf8');
|
|
390
|
-
const declaredAt = text.indexOf(declaration);
|
|
391
|
-
if (declaredAt === -1) {
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
described = true;
|
|
395
|
-
|
|
396
|
-
const rest = text.slice(declaredAt);
|
|
397
|
-
const listAt = rest.indexOf('onlyDependOnLibsWithTags:');
|
|
398
|
-
const opensAt = rest.indexOf('[', listAt);
|
|
399
|
-
const closesAt = rest.indexOf(']', opensAt);
|
|
400
|
-
if (listAt === -1 || opensAt === -1 || closesAt === -1) {
|
|
401
|
-
report(`${BOUNDARIES_DIR}/${name}`, `у ${tag} не найден список зависимостей`);
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const dependencies = rest.slice(opensAt + 1, closesAt).trim();
|
|
406
|
-
if (dependencies.length > 0) {
|
|
407
|
-
report(`${BOUNDARIES_DIR}/${name}`, `${tag} ни от кого не зависит, а его список непуст: ${dependencies}`);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (!described) {
|
|
412
|
-
report(BOUNDARIES_DIR, `тег ${tag} не описан ни в одном файле границ`);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/** Барель собирает наружу собственные файлы либы, и только они в нём законны */
|
|
418
|
-
const isBarrel = (path) => path.endsWith('/index.ts');
|
|
419
|
-
|
|
420
|
-
/** Файл той же либы адресуется относительным путём, чужая либа — алиасом */
|
|
421
|
-
const isOwnFile = (module) => module.startsWith('.');
|
|
422
|
-
|
|
423
|
-
/** `export { X } from '…'` и `import { X } …` вместе с `export { X };` */
|
|
424
|
-
const REEXPORT_FROM = /^export\s+(?:type\s+)?\{([^}]*)\}\s*from\s*'([^']+)';/gm;
|
|
425
|
-
const BARE_EXPORT = /^export\s+(?:type\s+)?\{([^}]*)\};/gm;
|
|
426
|
-
const IMPORTED_NAMES = /^import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*'([^']+)';/gm;
|
|
427
|
-
|
|
428
|
-
const namesIn = (list) =>
|
|
429
|
-
list
|
|
430
|
-
.split(',')
|
|
431
|
-
.map(
|
|
432
|
-
(part) =>
|
|
433
|
-
part
|
|
434
|
-
.trim()
|
|
435
|
-
.replace(/^type\s+/, '')
|
|
436
|
-
.split(/\s+as\s+/)[0]
|
|
437
|
-
)
|
|
438
|
-
.filter(Boolean);
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Символ, объявленный в другой либе, наружу не проходит.
|
|
442
|
-
*
|
|
443
|
-
* Реэкспорт заводится тогда, когда потребителю не хватает прав на либу-источник,
|
|
444
|
-
* и протаскивает её через соседа мимо границы тегов: по графу зависимостей
|
|
445
|
-
* выходит, что слой её не видит. Ни линт, ни сборка реэкспорт от собственного
|
|
446
|
-
* объявления не отличают.
|
|
447
|
-
*
|
|
448
|
-
* В бареле проверяются только чужие либы: собственные файлы он и собирает.
|
|
449
|
-
* Пока барель пропускался целиком, чужой символ проходил через него молча —
|
|
450
|
-
* так `common/util` отдавала работу с датами всему бэкенду через слой `util`
|
|
451
|
-
* домена Postgres.
|
|
452
|
-
*/
|
|
453
|
-
function checkReexports() {
|
|
454
|
-
for (const file of collectSourceFiles()) {
|
|
455
|
-
if (file.includes('/gen/') || file.includes('/generated/')) {
|
|
456
|
-
continue;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const ownFilesAllowed = isBarrel(file);
|
|
460
|
-
const text = readFileSync(join(ROOT, file), 'utf8');
|
|
461
|
-
for (const [, list, module] of text.matchAll(REEXPORT_FROM)) {
|
|
462
|
-
if (ownFilesAllowed && isOwnFile(module)) {
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
report(file, `реэкспорт из \`${module}\`: ${namesIn(list).join(', ')} — потребитель импортирует их у источника`);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const imported = new Map();
|
|
469
|
-
for (const [, list, module] of text.matchAll(IMPORTED_NAMES)) {
|
|
470
|
-
namesIn(list).forEach((name) => imported.set(name, module));
|
|
471
|
-
}
|
|
472
|
-
for (const [, list] of text.matchAll(BARE_EXPORT)) {
|
|
473
|
-
for (const name of namesIn(list).filter((name) => imported.has(name))) {
|
|
474
|
-
if (ownFilesAllowed && isOwnFile(imported.get(name))) {
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
477
|
-
report(file, `реэкспорт \`${name}\` из \`${imported.get(name)}\` — потребитель импортирует его у источника`);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/** Файлы либ, кроме спек: реэкспорт в спеке бессмыслен, а обходить их дешевле */
|
|
484
|
-
function collectSourceFiles() {
|
|
485
|
-
const files = [];
|
|
486
|
-
const walk = (dir) => {
|
|
487
|
-
for (const entry of readdirSync(join(ROOT, dir), { withFileTypes: true })) {
|
|
488
|
-
if (IGNORED_DIRS.includes(entry.name) && entry.name !== 'src') {
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
const path = `${dir}/${entry.name}`;
|
|
492
|
-
if (entry.isDirectory()) {
|
|
493
|
-
walk(path);
|
|
494
|
-
} else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.spec.ts')) {
|
|
495
|
-
files.push(path);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
};
|
|
499
|
-
|
|
500
|
-
CONFIG.sourceRoots.filter(isDir).forEach(walk);
|
|
501
|
-
|
|
502
|
-
return files;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* Основание семейства видит `common/util` и слои `util` общих доменов своей
|
|
507
|
-
* семьи. Всё сверх этого названо строкой здесь: основание зовут все домены, и
|
|
508
|
-
* любая его зависимость становится общей для всей семьи.
|
|
509
|
-
*/
|
|
510
|
-
const CORE_EXCEPTIONS = {
|
|
511
|
-
// Транспорт Connect в admin-transport.provider.ts: дескриптор логина и токен транспорта
|
|
512
|
-
'scope:admin-core': ['scope:common-proto', 'scope:common-connect'],
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
/** `scope:<семья>-common-<домен>-util` — слой `util` общего домена той же семьи */
|
|
516
|
-
const isFamilyCommonUtil = (tag, family) => new RegExp(`^scope:${family}-common-[a-z0-9-]+-util$`).test(tag);
|
|
517
|
-
|
|
518
|
-
async function checkCoreLibs() {
|
|
519
|
-
// Свод границ есть не у всякого дерева с доменной сеткой: он собирается отдельным файлом, и
|
|
520
|
-
// пока его отсутствие не обрабатывалось, проверка падала на импорте — то есть отвечала
|
|
521
|
-
// поломкой на дерево, где границы объявлены иначе.
|
|
522
|
-
const boundaries = join(ROOT, 'eslint/boundaries/index.mjs');
|
|
523
|
-
if (!existsSync(boundaries)) {
|
|
524
|
-
console.log('пропущено: свода границ eslint/boundaries/index.mjs в дереве нет');
|
|
525
|
-
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
const { allBoundaries } = await import(boundaries);
|
|
529
|
-
|
|
530
|
-
for (const family of FAMILIES) {
|
|
531
|
-
const tag = `scope:${family}-core`;
|
|
532
|
-
const rule = allBoundaries.find((entry) => entry.sourceTag === tag);
|
|
533
|
-
if (!rule) {
|
|
534
|
-
continue;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const allowed = CORE_EXCEPTIONS[tag] ?? [];
|
|
538
|
-
for (const dependency of rule.onlyDependOnLibsWithTags ?? []) {
|
|
539
|
-
if (dependency === 'scope:common-util' || isFamilyCommonUtil(dependency, family) || allowed.includes(dependency)) {
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
542
|
-
report(
|
|
543
|
-
`${BOUNDARIES_DIR}/${family}-core.config.mjs`,
|
|
544
|
-
`${tag} видит ${dependency}: основанию семейства доступны только scope:common-util и scope:${family}-common-*-util`
|
|
545
|
-
);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
39
|
const domainLibs = collectAllDomainLibs();
|
|
551
40
|
const apiLibs = collectApiDomainLibs();
|
|
552
41
|
const flatLibs = collectFlatLibs();
|