aact 2.1.5 → 3.0.0-beta.3
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 +50 -18
- package/dist/chunks/index.mjs +422 -0
- package/dist/chunks/index2.mjs +248 -0
- package/dist/chunks/index3.mjs +72 -0
- package/dist/cli/index.mjs +338 -542
- package/dist/index.d.mts +520 -229
- package/dist/index.d.ts +520 -229
- package/dist/index.mjs +5 -89
- package/dist/shared/aact.BpV1UCZJ.mjs +3 -0
- package/dist/shared/aact.CfImn7en.mjs +138 -0
- package/dist/shared/aact.CxegP3pU.mjs +900 -0
- package/package.json +44 -14
- package/dist/shared/aact.CJGFUdeF.mjs +0 -886
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ CLI и библиотека для валидации, анализа и ген
|
|
|
19
19
|
|
|
20
20
|
<img src="https://github.com/Byndyusoft/aact/assets/1096954/a3c3b3b0-a09b-4da7-aca4-5538159b371c" width="15"/> Телеграм-канал: [Архитектура распределённых систем](https://t.me/rsa_enc)
|
|
21
21
|
|
|
22
|
-
aact можно использовать двумя способами: как **CLI** (`npx aact check`, авто-фикс, генерация артефактов) или как **библиотеку** (импортировать `
|
|
22
|
+
aact можно использовать двумя способами: как **CLI** (`npx aact check`, авто-фикс, генерация артефактов) или как **библиотеку** (импортировать `aclRule`, `analyzeArchitecture` и пр. в свои тесты на vitest/jest). CLI — ниже, library-режим — в [соответствующем разделе](#использование-как-библиотеки).
|
|
23
23
|
|
|
24
24
|
## Quick Start (CLI)
|
|
25
25
|
|
|
@@ -93,26 +93,37 @@ export default config;
|
|
|
93
93
|
|
|
94
94
|
```ts
|
|
95
95
|
import {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
checkCrud,
|
|
96
|
+
plantumlFormat,
|
|
97
|
+
aclRule,
|
|
98
|
+
acyclicRule,
|
|
99
|
+
crudRule,
|
|
101
100
|
analyzeArchitecture,
|
|
101
|
+
validateModel,
|
|
102
102
|
} from "aact";
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
const model =
|
|
104
|
+
// Загрузка через format — возвращает Model + diagnostic issues
|
|
105
|
+
const { model, issues } = await plantumlFormat.load("architecture.puml");
|
|
106
|
+
for (const issue of issues) console.warn(`model:`, issue);
|
|
106
107
|
|
|
107
|
-
// Проверка правил
|
|
108
|
-
const aclViolations =
|
|
109
|
-
const cyclicViolations =
|
|
108
|
+
// Проверка правил — uniform signature (model, options?) => Violation[]
|
|
109
|
+
const aclViolations = aclRule.check(model);
|
|
110
|
+
const cyclicViolations = acyclicRule.check(model);
|
|
111
|
+
const crudViolations = crudRule.check(model, { repoTags: ["repo", "dao"] });
|
|
110
112
|
|
|
111
113
|
// Анализ метрик
|
|
112
114
|
const { report } = analyzeArchitecture(model);
|
|
113
115
|
console.log(`Elements: ${report.elementsCount}`);
|
|
116
|
+
|
|
117
|
+
// Прямой доступ к containers / boundaries — Record<name, ...>
|
|
118
|
+
for (const container of Object.values(model.containers)) {
|
|
119
|
+
console.log(`${container.kind} ${container.name}`);
|
|
120
|
+
}
|
|
121
|
+
const ordersService = model.containers["orders"];
|
|
114
122
|
```
|
|
115
123
|
|
|
124
|
+
Полный API: [`Model`](./src/model/types.ts), [`Format`](./src/formats/types.ts),
|
|
125
|
+
[`RuleDefinition`](./src/rules/types.ts). См. `CHANGELOG.md` для v2 → v3 migration.
|
|
126
|
+
|
|
116
127
|
## Примеры
|
|
117
128
|
|
|
118
129
|
Запускаемые из коробки (склонируй репо, `cd examples/<name>`, `npx aact check`):
|
|
@@ -122,7 +133,7 @@ console.log(`Elements: ${report.elementsCount}`);
|
|
|
122
133
|
|
|
123
134
|
Тестовые сценарии (для разработчиков пакета, запускаются через `vitest`):
|
|
124
135
|
|
|
125
|
-
- [`examples/banking-plantuml/`](examples/banking-plantuml/) и [`examples/microservices-structurizr/`](examples/microservices-structurizr/) — интеграционные тесты архитектуры из `
|
|
136
|
+
- [`examples/banking-plantuml/`](examples/banking-plantuml/) и [`examples/microservices-structurizr/`](examples/microservices-structurizr/) — интеграционные тесты архитектуры из `fixtures/`.
|
|
126
137
|
|
|
127
138
|
## Документация
|
|
128
139
|
|
|
@@ -130,6 +141,27 @@ console.log(`Elements: ${report.elementsCount}`);
|
|
|
130
141
|
- [ADR](ADRs/) — Architecture Decision Records
|
|
131
142
|
- [Roadmap](roadmap.md) — планы развития
|
|
132
143
|
|
|
144
|
+
## Testing
|
|
145
|
+
|
|
146
|
+
Тестовый стек разделён на четыре уровня:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pnpm test # все unit + integration + e2e
|
|
150
|
+
pnpm test:unit # только unit
|
|
151
|
+
pnpm test:integration # интеграционные на реальных фикстурах
|
|
152
|
+
pnpm test:e2e # subprocess-тесты CLI через execa
|
|
153
|
+
pnpm test:coverage # с v8 coverage + порогами
|
|
154
|
+
pnpm test:mutation # Stryker mutation testing
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Метрики качества тестов:**
|
|
158
|
+
|
|
159
|
+
- **Coverage** (v8): порог в CI — statements ≥95%, branches ≥85%, functions ≥95%, lines ≥95%
|
|
160
|
+
- **Mutation score** (Stryker) ≥95% — каждое смысловое изменение в исходнике должно ломать хотя бы один тест
|
|
161
|
+
- **Property-based** (`@fast-check/vitest`) на option-bearing правилах — защита от «hardcoded literal where option should be read» бага
|
|
162
|
+
- **Inline snapshots** на генераторах для regression-pin'а формата вывода
|
|
163
|
+
- **E2E** на цепочке `init → check → fix → recheck` через `npx aact` в subprocess
|
|
164
|
+
|
|
133
165
|
## Публичные материалы
|
|
134
166
|
|
|
135
167
|
### Раз архитектура — «as Code», почему бы её не покрыть тестами?!
|
|
@@ -172,13 +204,13 @@ https://www.youtube.com/watch?v=fb2UjqjHGUE
|
|
|
172
204
|
|
|
173
205
|
## Пример архитектуры, которую покроем тестами
|
|
174
206
|
|
|
175
|
-
[](fixtures/architecture/Demo%20Tests.svg)
|
|
176
208
|
|
|
177
209
|
## Пример тестов
|
|
178
210
|
|
|
179
|
-
1. [find diff in configs and uml containers](examples/banking-plantuml/architecture.test.ts) — проверяет актуальность списка микросервисов на архитектуре и в [конфигурации инфраструктуры](
|
|
180
|
-
2. [find diff in configs and uml dependencies](examples/banking-plantuml/architecture.test.ts) — проверяет актуальность зависимостей (связей) микросервисов на архитектуре и в [конфигурации инфраструктуры](
|
|
181
|
-
3. [check that urls and topics from relations exist in config](examples/banking-plantuml/architecture.test.ts) — проверяет соответствие между параметрами связей микросервисов (REST-урлы, топики kafka) на архитектуре и в [конфигурации инфраструктуры](
|
|
211
|
+
1. [find diff in configs and uml containers](examples/banking-plantuml/architecture.test.ts) — проверяет актуальность списка микросервисов на архитектуре и в [конфигурации инфраструктуры](fixtures/kubernetes/microservices)
|
|
212
|
+
2. [find diff in configs and uml dependencies](examples/banking-plantuml/architecture.test.ts) — проверяет актуальность зависимостей (связей) микросервисов на архитектуре и в [конфигурации инфраструктуры](fixtures/kubernetes/microservices)
|
|
213
|
+
3. [check that urls and topics from relations exist in config](examples/banking-plantuml/architecture.test.ts) — проверяет соответствие между параметрами связей микросервисов (REST-урлы, топики kafka) на архитектуре и в [конфигурации инфраструктуры](fixtures/kubernetes/microservices)
|
|
182
214
|
4. [only acl can depend on external systems](test/rules/acl.test.ts) — проверяет, что не нарушен выбранный принцип построения интеграций с внешними системами только через ACL (Anti Corruption Layer). Проверяет, что только acl-микросервисы имеют зависимости от внешних систем.
|
|
183
215
|
5. [connect to external systems only by API Gateway or kafka](examples/banking-plantuml/architecture.test.ts) — проверяет, что все внешние интеграции идут через API Gateway или через kafka
|
|
184
216
|
|
|
@@ -190,11 +222,11 @@ https://www.youtube.com/watch?v=fb2UjqjHGUE
|
|
|
190
222
|
|
|
191
223
|
### Ручная:
|
|
192
224
|
|
|
193
|
-
[](fixtures/architecture/Demo%20Tests.svg)
|
|
194
226
|
|
|
195
227
|
### Сгенерированная:
|
|
196
228
|
|
|
197
|
-
[](fixtures/architecture/Demo%20Generated.svg)
|
|
198
230
|
|
|
199
231
|
# Тестирование модульного монолита
|
|
200
232
|
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { p as getBoundary, q as getContainer } from '../shared/aact.CxegP3pU.mjs';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'pathe';
|
|
4
|
+
import { Comment, Stdlib_C4_Container_Component, Stdlib_C4_Context, Stdlib_C4_Dynamic_Rel, Relationship, Stdlib_C4_Boundary, parse } from 'plantuml-parser';
|
|
5
|
+
import { p as parseCsvTags } from '../shared/aact.BpV1UCZJ.mjs';
|
|
6
|
+
import { b as buildModel } from '../shared/aact.CfImn7en.mjs';
|
|
7
|
+
import 'valibot';
|
|
8
|
+
import 'consola';
|
|
9
|
+
|
|
10
|
+
const C4_KIND_MAP = Object.freeze({
|
|
11
|
+
// Person
|
|
12
|
+
Person: { kind: "Person", external: false },
|
|
13
|
+
Person_Ext: { kind: "Person", external: true },
|
|
14
|
+
// System / SystemDb / SystemQueue + _Ext variants
|
|
15
|
+
System: { kind: "System", external: false },
|
|
16
|
+
SystemDb: { kind: "System", external: false },
|
|
17
|
+
SystemQueue: { kind: "System", external: false },
|
|
18
|
+
System_Ext: { kind: "System", external: true },
|
|
19
|
+
SystemDb_Ext: { kind: "System", external: true },
|
|
20
|
+
SystemQueue_Ext: { kind: "System", external: true },
|
|
21
|
+
// Container variants
|
|
22
|
+
Container: { kind: "Container", external: false },
|
|
23
|
+
ContainerDb: { kind: "ContainerDb", external: false },
|
|
24
|
+
ContainerQueue: { kind: "ContainerQueue", external: false },
|
|
25
|
+
Container_Ext: { kind: "Container", external: true },
|
|
26
|
+
ContainerDb_Ext: { kind: "ContainerDb", external: true },
|
|
27
|
+
ContainerQueue_Ext: { kind: "ContainerQueue", external: true },
|
|
28
|
+
// Component variants
|
|
29
|
+
Component: { kind: "Component", external: false },
|
|
30
|
+
ComponentDb: { kind: "ComponentDb", external: false },
|
|
31
|
+
ComponentQueue: { kind: "ComponentQueue", external: false },
|
|
32
|
+
Component_Ext: { kind: "Component", external: true },
|
|
33
|
+
ComponentDb_Ext: { kind: "ComponentDb", external: true },
|
|
34
|
+
ComponentQueue_Ext: { kind: "ComponentQueue", external: true }
|
|
35
|
+
});
|
|
36
|
+
const parseC4MacroKind = (macroName) => C4_KIND_MAP[macroName];
|
|
37
|
+
const BOUNDARY_KIND_MAP = Object.freeze(
|
|
38
|
+
{
|
|
39
|
+
Boundary: "System",
|
|
40
|
+
// generic — sensible default
|
|
41
|
+
System_Boundary: "System",
|
|
42
|
+
Container_Boundary: "Container",
|
|
43
|
+
Component_Boundary: "Component",
|
|
44
|
+
Enterprise_Boundary: "Enterprise"
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
const parseBoundaryMacro = (macroName) => BOUNDARY_KIND_MAP[macroName] ?? "System";
|
|
48
|
+
const c4MacroName = (kind, external) => {
|
|
49
|
+
if (kind === "Person") return external ? "Person_Ext" : "Person";
|
|
50
|
+
if (kind === "System") return external ? "System_Ext" : "System";
|
|
51
|
+
return external ? `${kind}_Ext` : kind;
|
|
52
|
+
};
|
|
53
|
+
const boundaryMacroName = (kind) => {
|
|
54
|
+
if (kind === "System") return "System_Boundary";
|
|
55
|
+
if (kind === "Container") return "Container_Boundary";
|
|
56
|
+
if (kind === "Component") return "Component_Boundary";
|
|
57
|
+
return "Enterprise_Boundary";
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const isContextKind = (kind) => kind === "Person" || kind === "System";
|
|
61
|
+
const renderContainer = (container) => {
|
|
62
|
+
const macro = c4MacroName(container.kind, container.external);
|
|
63
|
+
const parts = [container.name, `"${container.label}"`];
|
|
64
|
+
if (isContextKind(container.kind)) {
|
|
65
|
+
if (container.description) parts.push(`"${container.description}"`);
|
|
66
|
+
} else {
|
|
67
|
+
if (container.technology) parts.push(`"${container.technology}"`);
|
|
68
|
+
else if (container.description) parts.push('""');
|
|
69
|
+
if (container.description) parts.push(`"${container.description}"`);
|
|
70
|
+
}
|
|
71
|
+
const named = [];
|
|
72
|
+
if (container.sprite) named.push(`$sprite="${container.sprite}"`);
|
|
73
|
+
if (container.tags.length > 0)
|
|
74
|
+
named.push(`$tags="${container.tags.join("+")}"`);
|
|
75
|
+
if (container.link) named.push(`$link="${container.link}"`);
|
|
76
|
+
return `${macro}(${[...parts, ...named].join(", ")})`;
|
|
77
|
+
};
|
|
78
|
+
const renderBoundary = (model, boundary, indent) => {
|
|
79
|
+
const inner = indent + " ";
|
|
80
|
+
const macro = boundaryMacroName(boundary.kind);
|
|
81
|
+
const childBoundaries = boundary.boundaryNames.map((name) => getBoundary(model, name)).filter((b) => b !== void 0).map((b) => renderBoundary(model, b, inner));
|
|
82
|
+
const childContainers = boundary.containerNames.map((name) => getContainer(model, name)).filter((c) => c !== void 0).map((c) => `${inner}${renderContainer(c)}`);
|
|
83
|
+
const parts = [boundary.name, `"${boundary.label}"`];
|
|
84
|
+
const named = [];
|
|
85
|
+
if (boundary.tags.length > 0)
|
|
86
|
+
named.push(`$tags="${boundary.tags.join("+")}"`);
|
|
87
|
+
if (boundary.link) named.push(`$link="${boundary.link}"`);
|
|
88
|
+
return [
|
|
89
|
+
`${indent}${macro}(${[...parts, ...named].join(", ")}) {`,
|
|
90
|
+
...childBoundaries,
|
|
91
|
+
...childContainers,
|
|
92
|
+
`${indent}}`
|
|
93
|
+
].join("\n");
|
|
94
|
+
};
|
|
95
|
+
const renderRelation = (from, relation) => {
|
|
96
|
+
const label = relation.description ?? "";
|
|
97
|
+
const parts = [from, relation.to, `"${label}"`];
|
|
98
|
+
if (relation.technology) parts.push(`"${relation.technology}"`);
|
|
99
|
+
const named = [];
|
|
100
|
+
if (relation.sprite) named.push(`$sprite="${relation.sprite}"`);
|
|
101
|
+
if (relation.tags.length > 0)
|
|
102
|
+
named.push(`$tags="${relation.tags.join("+")}"`);
|
|
103
|
+
if (relation.link) named.push(`$link="${relation.link}"`);
|
|
104
|
+
return `Rel(${[...parts, ...named].join(", ")})`;
|
|
105
|
+
};
|
|
106
|
+
const collectBoundedContainerNames = (model) => {
|
|
107
|
+
const names = /* @__PURE__ */ new Set();
|
|
108
|
+
const visit = (boundary) => {
|
|
109
|
+
for (const n of boundary.containerNames) names.add(n);
|
|
110
|
+
for (const child of boundary.boundaryNames) {
|
|
111
|
+
const b = getBoundary(model, child);
|
|
112
|
+
if (b) visit(b);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
for (const root of model.rootBoundaryNames) {
|
|
116
|
+
const b = getBoundary(model, root);
|
|
117
|
+
if (b) visit(b);
|
|
118
|
+
}
|
|
119
|
+
return names;
|
|
120
|
+
};
|
|
121
|
+
const renderBody = (model, standaloneContainers, boundaryLabel) => {
|
|
122
|
+
const rootBoundaries = model.rootBoundaryNames.map((n) => getBoundary(model, n)).filter((b) => b !== void 0);
|
|
123
|
+
if (boundaryLabel) {
|
|
124
|
+
return [
|
|
125
|
+
`Boundary(project, "${boundaryLabel}") {`,
|
|
126
|
+
...rootBoundaries.map((b) => renderBoundary(model, b, " ")),
|
|
127
|
+
...standaloneContainers.map((c) => ` ${renderContainer(c)}`),
|
|
128
|
+
`}`
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
return [
|
|
132
|
+
...rootBoundaries.map((b) => renderBoundary(model, b, "")),
|
|
133
|
+
...standaloneContainers.map((c) => renderContainer(c))
|
|
134
|
+
];
|
|
135
|
+
};
|
|
136
|
+
const generate = (model, options) => {
|
|
137
|
+
const boundedNames = collectBoundedContainerNames(model);
|
|
138
|
+
const standalone = Object.values(model.containers).filter(
|
|
139
|
+
(c) => !boundedNames.has(c.name)
|
|
140
|
+
);
|
|
141
|
+
const relations = Object.values(model.containers).flatMap(
|
|
142
|
+
(container) => container.relations.map((rel) => renderRelation(container.name, rel))
|
|
143
|
+
);
|
|
144
|
+
const content = [
|
|
145
|
+
`@startuml`,
|
|
146
|
+
`!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml`,
|
|
147
|
+
`LAYOUT_WITH_LEGEND()`,
|
|
148
|
+
`AddRelTag("async", $lineStyle = DottedLine())`,
|
|
149
|
+
"",
|
|
150
|
+
...renderBody(model, standalone, options?.boundaryLabel),
|
|
151
|
+
"",
|
|
152
|
+
...relations,
|
|
153
|
+
"@enduml"
|
|
154
|
+
].join("\n");
|
|
155
|
+
return {
|
|
156
|
+
files: [{ path: "architecture.puml", content }]
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const CONTAINER_LIKE_NAMES = /* @__PURE__ */ new Set([
|
|
161
|
+
"Container",
|
|
162
|
+
"ContainerDb",
|
|
163
|
+
"ContainerQueue",
|
|
164
|
+
"Container_Ext",
|
|
165
|
+
"ContainerDb_Ext",
|
|
166
|
+
"ContainerQueue_Ext",
|
|
167
|
+
"Component",
|
|
168
|
+
"ComponentDb",
|
|
169
|
+
"ComponentQueue",
|
|
170
|
+
"Component_Ext",
|
|
171
|
+
"ComponentDb_Ext",
|
|
172
|
+
"ComponentQueue_Ext"
|
|
173
|
+
]);
|
|
174
|
+
const CONTEXT_NAMES = /* @__PURE__ */ new Set([
|
|
175
|
+
"Person",
|
|
176
|
+
"Person_Ext",
|
|
177
|
+
"System",
|
|
178
|
+
"SystemDb",
|
|
179
|
+
"SystemQueue",
|
|
180
|
+
"System_Ext",
|
|
181
|
+
"SystemDb_Ext",
|
|
182
|
+
"SystemQueue_Ext"
|
|
183
|
+
]);
|
|
184
|
+
const BOUNDARY_NAMES = /* @__PURE__ */ new Set([
|
|
185
|
+
"Boundary",
|
|
186
|
+
"System_Boundary",
|
|
187
|
+
"Container_Boundary",
|
|
188
|
+
"Component_Boundary",
|
|
189
|
+
"Enterprise_Boundary"
|
|
190
|
+
]);
|
|
191
|
+
const filterElements = (elements) => {
|
|
192
|
+
const result = [];
|
|
193
|
+
for (const element of elements) {
|
|
194
|
+
if (element instanceof Comment) continue;
|
|
195
|
+
const typeName = element.type_?.name;
|
|
196
|
+
if (element instanceof Stdlib_C4_Container_Component && CONTAINER_LIKE_NAMES.has(typeName) || element instanceof Stdlib_C4_Context && CONTEXT_NAMES.has(typeName) || element instanceof Stdlib_C4_Dynamic_Rel || element instanceof Relationship) {
|
|
197
|
+
result.push(element);
|
|
198
|
+
}
|
|
199
|
+
if (element instanceof Stdlib_C4_Boundary && BOUNDARY_NAMES.has(typeName)) {
|
|
200
|
+
result.push(element, ...filterElements(element.elements));
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(element)) {
|
|
203
|
+
result.push(...filterElements(element));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const TAGS_MARKER = "__aact_tags__:";
|
|
210
|
+
const LINK_MARKER = "__aact_link__:";
|
|
211
|
+
const SPRITE_MARKER = "__aact_sprite__:";
|
|
212
|
+
const preTransformNamedArgs = (raw) => raw.replaceAll(/, \$tags="(.+?)"/g, `, "${TAGS_MARKER}$1"`).replaceAll(/, \$link="(.+?)"/g, `, "${LINK_MARKER}$1"`).replaceAll(/, \$sprite="(.+?)"/g, `, "${SPRITE_MARKER}$1"`).replaceAll('""', '" "');
|
|
213
|
+
const stripMarker = (value, marker) => value && value.startsWith(marker) ? value.slice(marker.length) : void 0;
|
|
214
|
+
const extractMarked = (marker, ...slots) => {
|
|
215
|
+
for (const slot of slots) {
|
|
216
|
+
const v = stripMarker(slot, marker);
|
|
217
|
+
if (v !== void 0) return v;
|
|
218
|
+
}
|
|
219
|
+
return void 0;
|
|
220
|
+
};
|
|
221
|
+
const cleanSlot = (value, ...markers) => {
|
|
222
|
+
if (!value) return void 0;
|
|
223
|
+
if (markers.some((m) => value.startsWith(m))) return void 0;
|
|
224
|
+
return value;
|
|
225
|
+
};
|
|
226
|
+
const normalizeRelBack = (elements) => {
|
|
227
|
+
for (const element of elements) {
|
|
228
|
+
if (element instanceof Comment) continue;
|
|
229
|
+
if (!(element instanceof Stdlib_C4_Dynamic_Rel)) continue;
|
|
230
|
+
if (element.type_.name.startsWith("Rel_Back")) {
|
|
231
|
+
const from = element.from;
|
|
232
|
+
element.from = element.to;
|
|
233
|
+
element.to = from;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const ALL_MARKERS = [TAGS_MARKER, LINK_MARKER, SPRITE_MARKER];
|
|
238
|
+
const buildContainer = (el) => {
|
|
239
|
+
const macroKind = parseC4MacroKind(el.type_.name);
|
|
240
|
+
const kind = macroKind?.kind ?? "Container";
|
|
241
|
+
const external = macroKind?.external ?? false;
|
|
242
|
+
const rawTechn = "techn" in el && typeof el.techn === "string" ? el.techn : void 0;
|
|
243
|
+
const taggedValue = extractMarked(
|
|
244
|
+
TAGS_MARKER,
|
|
245
|
+
rawTechn,
|
|
246
|
+
el.descr,
|
|
247
|
+
el.sprite,
|
|
248
|
+
el.tags,
|
|
249
|
+
el.link
|
|
250
|
+
);
|
|
251
|
+
const linkValue = extractMarked(
|
|
252
|
+
LINK_MARKER,
|
|
253
|
+
rawTechn,
|
|
254
|
+
el.descr,
|
|
255
|
+
el.sprite,
|
|
256
|
+
el.tags,
|
|
257
|
+
el.link
|
|
258
|
+
);
|
|
259
|
+
const spriteNamedValue = extractMarked(
|
|
260
|
+
SPRITE_MARKER,
|
|
261
|
+
rawTechn,
|
|
262
|
+
el.descr,
|
|
263
|
+
el.sprite,
|
|
264
|
+
el.tags,
|
|
265
|
+
el.link
|
|
266
|
+
);
|
|
267
|
+
return {
|
|
268
|
+
name: el.alias,
|
|
269
|
+
label: el.label,
|
|
270
|
+
kind,
|
|
271
|
+
external,
|
|
272
|
+
description: cleanSlot(el.descr, ...ALL_MARKERS) ?? "",
|
|
273
|
+
technology: cleanSlot(rawTechn, ...ALL_MARKERS),
|
|
274
|
+
tags: taggedValue === void 0 ? parseCsvTags(cleanSlot(el.tags, ...ALL_MARKERS)) : parseCsvTags(taggedValue),
|
|
275
|
+
sprite: spriteNamedValue ?? cleanSlot(el.sprite, ...ALL_MARKERS),
|
|
276
|
+
relations: [],
|
|
277
|
+
link: linkValue ?? cleanSlot(el.link, ...ALL_MARKERS)
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
const buildRelation = (rel) => {
|
|
281
|
+
const taggedValue = extractMarked(
|
|
282
|
+
TAGS_MARKER,
|
|
283
|
+
rel.techn,
|
|
284
|
+
rel.descr,
|
|
285
|
+
rel.sprite,
|
|
286
|
+
rel.tags,
|
|
287
|
+
rel.link
|
|
288
|
+
);
|
|
289
|
+
const linkValue = extractMarked(
|
|
290
|
+
LINK_MARKER,
|
|
291
|
+
rel.techn,
|
|
292
|
+
rel.descr,
|
|
293
|
+
rel.sprite,
|
|
294
|
+
rel.tags,
|
|
295
|
+
rel.link
|
|
296
|
+
);
|
|
297
|
+
const spriteNamedValue = extractMarked(
|
|
298
|
+
SPRITE_MARKER,
|
|
299
|
+
rel.techn,
|
|
300
|
+
rel.descr,
|
|
301
|
+
rel.sprite,
|
|
302
|
+
rel.tags,
|
|
303
|
+
rel.link
|
|
304
|
+
);
|
|
305
|
+
return {
|
|
306
|
+
to: rel.to,
|
|
307
|
+
description: cleanSlot(rel.label, ...ALL_MARKERS) || void 0,
|
|
308
|
+
technology: cleanSlot(rel.techn, ...ALL_MARKERS),
|
|
309
|
+
tags: taggedValue === void 0 ? parseCsvTags(
|
|
310
|
+
cleanSlot(rel.descr, ...ALL_MARKERS) || cleanSlot(rel.tags, ...ALL_MARKERS)
|
|
311
|
+
) : parseCsvTags(taggedValue),
|
|
312
|
+
sprite: spriteNamedValue ?? cleanSlot(rel.sprite, ...ALL_MARKERS),
|
|
313
|
+
link: linkValue ?? cleanSlot(rel.link, ...ALL_MARKERS)
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
const buildBoundary = (el, childContainers, childBoundaries) => {
|
|
317
|
+
const taggedValue = extractMarked(TAGS_MARKER, el.tags, el.link);
|
|
318
|
+
const linkValue = extractMarked(LINK_MARKER, el.tags, el.link);
|
|
319
|
+
return {
|
|
320
|
+
name: el.alias,
|
|
321
|
+
label: el.label,
|
|
322
|
+
kind: parseBoundaryMacro(el.type_.name),
|
|
323
|
+
tags: taggedValue === void 0 ? parseCsvTags(cleanSlot(el.tags, ...ALL_MARKERS)) : parseCsvTags(taggedValue),
|
|
324
|
+
containerNames: childContainers,
|
|
325
|
+
boundaryNames: childBoundaries,
|
|
326
|
+
link: linkValue ?? cleanSlot(el.link, ...ALL_MARKERS)
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
const isC4Element = (el) => el instanceof Stdlib_C4_Context || el instanceof Stdlib_C4_Container_Component;
|
|
330
|
+
const collectBoundaryChildren = (el) => {
|
|
331
|
+
const containers = [];
|
|
332
|
+
const boundaries = [];
|
|
333
|
+
for (const child of el.elements) {
|
|
334
|
+
if (isC4Element(child)) containers.push(child.alias);
|
|
335
|
+
else if (child instanceof Stdlib_C4_Boundary) boundaries.push(child.alias);
|
|
336
|
+
}
|
|
337
|
+
return { containers, boundaries };
|
|
338
|
+
};
|
|
339
|
+
const pushRelation = (acc, sourceName, relation) => {
|
|
340
|
+
const source = acc[sourceName];
|
|
341
|
+
if (!source) return;
|
|
342
|
+
acc[sourceName] = {
|
|
343
|
+
...source,
|
|
344
|
+
relations: [...source.relations, relation]
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
const populateRelations = (elements, acc) => {
|
|
348
|
+
for (const el of elements) {
|
|
349
|
+
if (!(el instanceof Stdlib_C4_Dynamic_Rel)) continue;
|
|
350
|
+
pushRelation(acc, el.from, buildRelation(el));
|
|
351
|
+
if (el.type_.name.startsWith("BiRel")) {
|
|
352
|
+
pushRelation(
|
|
353
|
+
acc,
|
|
354
|
+
el.to,
|
|
355
|
+
buildRelation({ ...el, from: el.to, to: el.from })
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const collectChildBoundaryNames = (boundaryElements) => {
|
|
361
|
+
const childOfBoundary = /* @__PURE__ */ new Set();
|
|
362
|
+
for (const b of boundaryElements) {
|
|
363
|
+
for (const child of b.elements) {
|
|
364
|
+
if (child instanceof Stdlib_C4_Boundary) {
|
|
365
|
+
childOfBoundary.add(child.alias);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return childOfBoundary;
|
|
370
|
+
};
|
|
371
|
+
const load = async (filePath) => {
|
|
372
|
+
const filepath = path.resolve(filePath);
|
|
373
|
+
const raw = await fs.readFile(filepath, "utf8");
|
|
374
|
+
const transformed = preTransformNamedArgs(raw);
|
|
375
|
+
const [{ elements: rawElements }] = parse(transformed);
|
|
376
|
+
const elements = filterElements(rawElements);
|
|
377
|
+
normalizeRelBack(elements);
|
|
378
|
+
const containerByAlias = /* @__PURE__ */ Object.create(
|
|
379
|
+
null
|
|
380
|
+
);
|
|
381
|
+
for (const el of elements) {
|
|
382
|
+
if (isC4Element(el)) containerByAlias[el.alias] = buildContainer(el);
|
|
383
|
+
}
|
|
384
|
+
populateRelations(elements, containerByAlias);
|
|
385
|
+
const boundaryElements = elements.filter(
|
|
386
|
+
(el) => el instanceof Stdlib_C4_Boundary
|
|
387
|
+
);
|
|
388
|
+
const childOfBoundary = collectChildBoundaryNames(boundaryElements);
|
|
389
|
+
const boundaries = boundaryElements.map((b) => {
|
|
390
|
+
const { containers, boundaries: childBoundaries } = collectBoundaryChildren(b);
|
|
391
|
+
return buildBoundary(b, containers, childBoundaries);
|
|
392
|
+
});
|
|
393
|
+
const rootBoundaryNames = boundaries.map((b) => b.name).filter((name) => !childOfBoundary.has(name));
|
|
394
|
+
return buildModel({
|
|
395
|
+
containers: Object.values(containerByAlias),
|
|
396
|
+
boundaries,
|
|
397
|
+
rootBoundaryNames
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const plantumlSyntax = {
|
|
402
|
+
containerPattern: (name) => `(${name},`,
|
|
403
|
+
containerDecl: (name, label, tags) => {
|
|
404
|
+
const tagsPart = tags ? `, "", "", $tags="${tags}"` : "";
|
|
405
|
+
return `Container(${name}, "${label}"${tagsPart})`;
|
|
406
|
+
},
|
|
407
|
+
relationPattern: (from, to) => `Rel(${from}, ${to}`,
|
|
408
|
+
relationDecl: (from, to, tech, tags) => {
|
|
409
|
+
const tagsPart = tags ? `, $tags="${tags}"` : "";
|
|
410
|
+
return `Rel(${from}, ${to}, "${tech ?? ""}"${tagsPart})`;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const plantumlFormat = {
|
|
415
|
+
name: "plantuml",
|
|
416
|
+
defaultPattern: "*.puml",
|
|
417
|
+
load,
|
|
418
|
+
generate,
|
|
419
|
+
fix: { syntax: plantumlSyntax }
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
export { plantumlFormat };
|