@agilewallaby/c4-model 1.1.0 → 2.1.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/package.json +9 -4
- package/src/index.js +587 -12
- package/CLAUDE.md +0 -45
- package/src/component.js +0 -15
- package/src/component.js.map +0 -1
- package/src/container.js +0 -59
- package/src/container.js.map +0 -1
- package/src/core.js +0 -59
- package/src/core.js.map +0 -1
- package/src/generateDiagrams.js +0 -55
- package/src/generateDiagrams.js.map +0 -1
- package/src/index.js.map +0 -1
- package/src/model.js +0 -128
- package/src/model.js.map +0 -1
- package/src/person.js +0 -15
- package/src/person.js.map +0 -1
- package/src/softwareSystem.js +0 -59
- package/src/softwareSystem.js.map +0 -1
- package/src/structurizrDslWriter.js +0 -165
- package/src/structurizrDslWriter.js.map +0 -1
- package/src/views.js +0 -76
- package/src/views.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agilewallaby/c4-model",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -14,8 +14,13 @@
|
|
|
14
14
|
"testcontainers": "^10.28.0",
|
|
15
15
|
"tslib": "^2.3.0"
|
|
16
16
|
},
|
|
17
|
-
"type": "
|
|
17
|
+
"type": "module",
|
|
18
18
|
"main": "./src/index.js",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
19
|
+
"types": "./src/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./src/index.js",
|
|
23
|
+
"types": "./src/index.d.ts"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
21
26
|
}
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,587 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
// libs/c4-model/src/model.ts
|
|
2
|
+
import { glob } from "glob";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
// libs/c4-model/src/core.ts
|
|
7
|
+
import { camelCase } from "change-case";
|
|
8
|
+
var Element = class {
|
|
9
|
+
constructor(name, defaultTags = [], definition) {
|
|
10
|
+
this.name = name;
|
|
11
|
+
this.description = definition?.description;
|
|
12
|
+
this.tags = (definition?.tags ?? []).concat(["Element"]).concat(defaultTags);
|
|
13
|
+
}
|
|
14
|
+
description;
|
|
15
|
+
tags;
|
|
16
|
+
_relationships = [];
|
|
17
|
+
get canonicalName() {
|
|
18
|
+
return camelCase(this.name);
|
|
19
|
+
}
|
|
20
|
+
uses(otherElement, definition) {
|
|
21
|
+
const relationship = new Relationship(this, otherElement, definition);
|
|
22
|
+
this._relationships.push(relationship);
|
|
23
|
+
}
|
|
24
|
+
get relationships() {
|
|
25
|
+
return this._relationships;
|
|
26
|
+
}
|
|
27
|
+
getRelationshipsInHierarchy() {
|
|
28
|
+
return this._relationships.concat(this.getChildElements().flatMap((element) => element.getRelationshipsInHierarchy()));
|
|
29
|
+
}
|
|
30
|
+
getChildElementNames(path2) {
|
|
31
|
+
const result = Array.from(this.getChildElements()).flatMap((reference) => {
|
|
32
|
+
const currentPath = `${path2 ? path2 : "" + this.name}.${reference.name}`;
|
|
33
|
+
return [currentPath, ...reference.getChildElementNames(currentPath)];
|
|
34
|
+
});
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var TechnicalElement = class extends Element {
|
|
39
|
+
technology;
|
|
40
|
+
constructor(name, defaultTags = [], definition) {
|
|
41
|
+
super(name, defaultTags, definition);
|
|
42
|
+
this.technology = definition?.technology;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var Relationship = class {
|
|
46
|
+
constructor(source, destination, definition) {
|
|
47
|
+
this.source = source;
|
|
48
|
+
this.destination = destination;
|
|
49
|
+
this.description = definition?.description;
|
|
50
|
+
this.technology = definition?.technology;
|
|
51
|
+
this.tags = (definition?.tags ?? []).concat(["Relationship"]);
|
|
52
|
+
}
|
|
53
|
+
description;
|
|
54
|
+
tags;
|
|
55
|
+
technology;
|
|
56
|
+
};
|
|
57
|
+
var Group = class {
|
|
58
|
+
constructor(name) {
|
|
59
|
+
this.name = name;
|
|
60
|
+
}
|
|
61
|
+
// TODO: Implement this in some useful way?
|
|
62
|
+
// public addToGroup(groupCollection: string, groupMember: T) {}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// libs/c4-model/src/component.ts
|
|
66
|
+
var Component = class extends TechnicalElement {
|
|
67
|
+
constructor(name, definition) {
|
|
68
|
+
super(name, ["Component"], definition);
|
|
69
|
+
this.name = name;
|
|
70
|
+
}
|
|
71
|
+
getChildElements() {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// libs/c4-model/src/container.ts
|
|
77
|
+
var ContainerGroup = class extends Group {
|
|
78
|
+
constructor(name, container) {
|
|
79
|
+
super(name);
|
|
80
|
+
this.name = name;
|
|
81
|
+
this.container = container;
|
|
82
|
+
}
|
|
83
|
+
_components = /* @__PURE__ */ new Map();
|
|
84
|
+
defineComponent(name, definition) {
|
|
85
|
+
const component = this.container.defineComponent(name, definition);
|
|
86
|
+
this._components.set(name, component);
|
|
87
|
+
return component;
|
|
88
|
+
}
|
|
89
|
+
getComponents() {
|
|
90
|
+
return Array.from(this._components.values());
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var Container = class extends TechnicalElement {
|
|
94
|
+
constructor(name, definition) {
|
|
95
|
+
super(name, ["Container"], definition);
|
|
96
|
+
this.name = name;
|
|
97
|
+
}
|
|
98
|
+
_components = /* @__PURE__ */ new Map();
|
|
99
|
+
_groups = /* @__PURE__ */ new Map();
|
|
100
|
+
defineComponent(name, definition) {
|
|
101
|
+
if (this._components.has(name)) {
|
|
102
|
+
throw Error(`A Component named '${name}' is defined elsewhere in this Container. A Component can be defined only once.`);
|
|
103
|
+
}
|
|
104
|
+
const component = new Component(name, definition);
|
|
105
|
+
this._components.set(name, component);
|
|
106
|
+
return component;
|
|
107
|
+
}
|
|
108
|
+
addGroup(groupName) {
|
|
109
|
+
let group = this._groups.get(groupName);
|
|
110
|
+
if (!group) {
|
|
111
|
+
group = new ContainerGroup(groupName, this);
|
|
112
|
+
this._groups.set(groupName, group);
|
|
113
|
+
}
|
|
114
|
+
return group;
|
|
115
|
+
}
|
|
116
|
+
getGroups() {
|
|
117
|
+
return Array.from(this._groups.values());
|
|
118
|
+
}
|
|
119
|
+
getComponentsNotInGroups() {
|
|
120
|
+
const componentsInGroups = this.getGroups().flatMap((group) => group.getComponents());
|
|
121
|
+
return Array.from(this._components.values()).filter((component) => !componentsInGroups.includes(component));
|
|
122
|
+
}
|
|
123
|
+
getChildElements() {
|
|
124
|
+
return Array.from(this._components.values());
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// libs/c4-model/src/softwareSystem.ts
|
|
129
|
+
var SoftwareSystemGroup = class extends Group {
|
|
130
|
+
constructor(name, softwareSystem) {
|
|
131
|
+
super(name);
|
|
132
|
+
this.name = name;
|
|
133
|
+
this.softwareSystem = softwareSystem;
|
|
134
|
+
}
|
|
135
|
+
_containers = /* @__PURE__ */ new Map();
|
|
136
|
+
defineContainer(name, definition) {
|
|
137
|
+
const container = this.softwareSystem.defineContainer(name, definition);
|
|
138
|
+
this._containers.set(name, container);
|
|
139
|
+
return container;
|
|
140
|
+
}
|
|
141
|
+
getContainers() {
|
|
142
|
+
return Array.from(this._containers.values());
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var SoftwareSystem = class extends Element {
|
|
146
|
+
constructor(name, definition) {
|
|
147
|
+
super(name, ["Software System"], definition);
|
|
148
|
+
this.name = name;
|
|
149
|
+
}
|
|
150
|
+
_containers = /* @__PURE__ */ new Map();
|
|
151
|
+
_groups = /* @__PURE__ */ new Map();
|
|
152
|
+
defineContainer(name, definition) {
|
|
153
|
+
if (this._containers.has(name)) {
|
|
154
|
+
throw Error(`A Container named '${name}' is defined elsewhere in this SoftwareSystem. A Container can be defined only once.`);
|
|
155
|
+
}
|
|
156
|
+
const container = new Container(name, definition);
|
|
157
|
+
this._containers.set(name, container);
|
|
158
|
+
return container;
|
|
159
|
+
}
|
|
160
|
+
addGroup(groupName) {
|
|
161
|
+
let group = this._groups.get(groupName);
|
|
162
|
+
if (!group) {
|
|
163
|
+
group = new SoftwareSystemGroup(groupName, this);
|
|
164
|
+
this._groups.set(groupName, group);
|
|
165
|
+
}
|
|
166
|
+
return group;
|
|
167
|
+
}
|
|
168
|
+
getGroups() {
|
|
169
|
+
return Array.from(this._groups.values());
|
|
170
|
+
}
|
|
171
|
+
getChildElements() {
|
|
172
|
+
return Array.from(this._containers.values());
|
|
173
|
+
}
|
|
174
|
+
getContainersNotInGroups() {
|
|
175
|
+
const containersInGroups = Array.from(this._groups.values()).flatMap((group) => group.getContainers());
|
|
176
|
+
return Array.from(this._containers.values()).filter((container) => !containersInGroups.includes(container));
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// libs/c4-model/src/person.ts
|
|
181
|
+
var Person = class extends Element {
|
|
182
|
+
constructor(name, definition) {
|
|
183
|
+
super(name, ["Person"], definition);
|
|
184
|
+
this.name = name;
|
|
185
|
+
}
|
|
186
|
+
getChildElements() {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// libs/c4-model/src/model.ts
|
|
192
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
193
|
+
var ModelGroup = class extends Group {
|
|
194
|
+
constructor(name, model) {
|
|
195
|
+
super(name);
|
|
196
|
+
this.name = name;
|
|
197
|
+
this.model = model;
|
|
198
|
+
}
|
|
199
|
+
softwareSystems = /* @__PURE__ */ new Map();
|
|
200
|
+
people = /* @__PURE__ */ new Map();
|
|
201
|
+
defineSoftwareSystem(name, definition) {
|
|
202
|
+
const softwareSystem = this.model.defineSoftwareSystem(name, definition);
|
|
203
|
+
this.softwareSystems.set(name, softwareSystem);
|
|
204
|
+
return softwareSystem;
|
|
205
|
+
}
|
|
206
|
+
definePerson(name, definition) {
|
|
207
|
+
const person = this.model.definePerson(name, definition);
|
|
208
|
+
this.people.set(name, person);
|
|
209
|
+
return person;
|
|
210
|
+
}
|
|
211
|
+
getSoftwareSystems() {
|
|
212
|
+
return Array.from(this.softwareSystems.values());
|
|
213
|
+
}
|
|
214
|
+
getPeople() {
|
|
215
|
+
return Array.from(this.people.values());
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
var Model = class {
|
|
219
|
+
constructor(name) {
|
|
220
|
+
this.name = name;
|
|
221
|
+
}
|
|
222
|
+
softwareSystems = /* @__PURE__ */ new Map();
|
|
223
|
+
people = /* @__PURE__ */ new Map();
|
|
224
|
+
groups = /* @__PURE__ */ new Map();
|
|
225
|
+
defineSoftwareSystem(name, definition) {
|
|
226
|
+
if (this.softwareSystems.has(name)) {
|
|
227
|
+
throw Error(`A SoftwareSystem named '${name}' is defined elsewhere in this Model. A SoftwareSystem can be defined only once.`);
|
|
228
|
+
}
|
|
229
|
+
const system = new SoftwareSystem(name, definition);
|
|
230
|
+
this.softwareSystems.set(name, system);
|
|
231
|
+
return system;
|
|
232
|
+
}
|
|
233
|
+
// TODO:Should be a Group<SoftwareSystem | Person> if that is added back in
|
|
234
|
+
addGroup(groupName) {
|
|
235
|
+
let group = this.groups.get(groupName);
|
|
236
|
+
if (!group) {
|
|
237
|
+
group = new ModelGroup(groupName, this);
|
|
238
|
+
this.groups.set(groupName, group);
|
|
239
|
+
}
|
|
240
|
+
return group;
|
|
241
|
+
}
|
|
242
|
+
definePerson(name, definition) {
|
|
243
|
+
if (this.people.has(name)) {
|
|
244
|
+
throw Error(`A Person named '${name}' is defined elsewhere in this Model. A Person can be defined only once.`);
|
|
245
|
+
}
|
|
246
|
+
const person = new Person(name, definition);
|
|
247
|
+
this.people.set(name, person);
|
|
248
|
+
return person;
|
|
249
|
+
}
|
|
250
|
+
validate() {
|
|
251
|
+
}
|
|
252
|
+
getPeople() {
|
|
253
|
+
return Array.from(this.people.values());
|
|
254
|
+
}
|
|
255
|
+
getSoftwareSystems() {
|
|
256
|
+
return Array.from(this.softwareSystems.values());
|
|
257
|
+
}
|
|
258
|
+
getPeopleNotInGroups() {
|
|
259
|
+
const peopleInGroups = Array.from(this.groups.values()).flatMap((group) => group.getPeople());
|
|
260
|
+
return Array.from(this.people.values()).filter((person) => !peopleInGroups.includes(person));
|
|
261
|
+
}
|
|
262
|
+
getSoftwareSystemsNotInGroups() {
|
|
263
|
+
const systemsInGroups = Array.from(this.groups.values()).flatMap((group) => group.getSoftwareSystems());
|
|
264
|
+
return Array.from(this.softwareSystems.values()).filter((system) => !systemsInGroups.includes(system));
|
|
265
|
+
}
|
|
266
|
+
getGroups() {
|
|
267
|
+
return Array.from(this.groups.values());
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
async function buildModelWithCatalog(options = {}) {
|
|
271
|
+
const { modelName = "model", globPath = "c4.dsl.ts", searchRoot = __dirname } = options;
|
|
272
|
+
const model = new Model(modelName);
|
|
273
|
+
const result = await glob(`**/${globPath}`, { cwd: searchRoot });
|
|
274
|
+
if (result.length === 0) {
|
|
275
|
+
throw new Error(`No ${globPath} files found`);
|
|
276
|
+
}
|
|
277
|
+
const modules = await Promise.all(result.map((file) => import(join(searchRoot, file))));
|
|
278
|
+
const registrations = [];
|
|
279
|
+
const rootCatalog = {};
|
|
280
|
+
for (const module of modules) {
|
|
281
|
+
if (!module.c4Module) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const instance = module.c4Module;
|
|
285
|
+
const local = instance.registerDefinitions(model);
|
|
286
|
+
rootCatalog[instance.key] = local;
|
|
287
|
+
registrations.push({ instance, key: instance.key, local });
|
|
288
|
+
}
|
|
289
|
+
if (registrations.length === 0) {
|
|
290
|
+
throw new Error(`No c4Module exports found in any ${globPath} files`);
|
|
291
|
+
}
|
|
292
|
+
for (const { instance, key, local } of registrations) {
|
|
293
|
+
const dependencies = Object.fromEntries(Object.entries(rootCatalog).filter(([k]) => k !== key));
|
|
294
|
+
instance.buildRelationships(local, dependencies);
|
|
295
|
+
}
|
|
296
|
+
return { model, catalog: rootCatalog };
|
|
297
|
+
}
|
|
298
|
+
async function buildModel(options = {}) {
|
|
299
|
+
return (await buildModelWithCatalog(options)).model;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// libs/c4-model/src/views.ts
|
|
303
|
+
var View = class {
|
|
304
|
+
constructor(key, viewDefinition) {
|
|
305
|
+
this.key = key;
|
|
306
|
+
this.description = viewDefinition.description;
|
|
307
|
+
this.subject = viewDefinition.subject;
|
|
308
|
+
this.title = viewDefinition.title;
|
|
309
|
+
}
|
|
310
|
+
subject;
|
|
311
|
+
description;
|
|
312
|
+
title;
|
|
313
|
+
_scopes = [];
|
|
314
|
+
includeAll() {
|
|
315
|
+
this._scopes.push("include *");
|
|
316
|
+
}
|
|
317
|
+
includeElement(element) {
|
|
318
|
+
this._scopes.push(`include ${element.canonicalName}`);
|
|
319
|
+
}
|
|
320
|
+
includeExpression(expression) {
|
|
321
|
+
this._scopes.push(`include ${expression}`);
|
|
322
|
+
}
|
|
323
|
+
excludeAll() {
|
|
324
|
+
this._scopes.push("exclude *");
|
|
325
|
+
}
|
|
326
|
+
excludeElement(element) {
|
|
327
|
+
this._scopes.push(`exclude ${element.canonicalName}`);
|
|
328
|
+
}
|
|
329
|
+
excludeExpression(expression) {
|
|
330
|
+
this._scopes.push(`exclude ${expression}`);
|
|
331
|
+
}
|
|
332
|
+
get scopes() {
|
|
333
|
+
return this._scopes;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
var Views = class {
|
|
337
|
+
_systemLandscapeViews = /* @__PURE__ */ new Map();
|
|
338
|
+
_systemContextViews = /* @__PURE__ */ new Map();
|
|
339
|
+
_containerViews = /* @__PURE__ */ new Map();
|
|
340
|
+
_componentViews = /* @__PURE__ */ new Map();
|
|
341
|
+
addSystemLandscapeView(key, definition) {
|
|
342
|
+
const view = new View(key, { subject: void 0, description: definition.description, title: definition.title });
|
|
343
|
+
this._systemLandscapeViews.set(key, view);
|
|
344
|
+
return view;
|
|
345
|
+
}
|
|
346
|
+
addSystemContextView(key, definition) {
|
|
347
|
+
const view = new View(key, definition);
|
|
348
|
+
this._systemContextViews.set(key, view);
|
|
349
|
+
return view;
|
|
350
|
+
}
|
|
351
|
+
addContainerView(key, definition) {
|
|
352
|
+
const view = new View(key, definition);
|
|
353
|
+
this._containerViews.set(key, view);
|
|
354
|
+
return view;
|
|
355
|
+
}
|
|
356
|
+
addComponentView(key, definition) {
|
|
357
|
+
const view = new View(key, definition);
|
|
358
|
+
this._componentViews.set(key, view);
|
|
359
|
+
return view;
|
|
360
|
+
}
|
|
361
|
+
get systemLandscapeViews() {
|
|
362
|
+
return Array.from(this._systemLandscapeViews.values());
|
|
363
|
+
}
|
|
364
|
+
get systemContextViews() {
|
|
365
|
+
return Array.from(this._systemContextViews.values());
|
|
366
|
+
}
|
|
367
|
+
get containerViews() {
|
|
368
|
+
return Array.from(this._containerViews.values());
|
|
369
|
+
}
|
|
370
|
+
get componentViews() {
|
|
371
|
+
return Array.from(this._componentViews.values());
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// libs/c4-model/src/structurizrDslWriter.ts
|
|
376
|
+
var INDENT_SIZE = 2;
|
|
377
|
+
var StructurizrDSLWriter = class {
|
|
378
|
+
constructor(model, views) {
|
|
379
|
+
this.model = model;
|
|
380
|
+
this.views = views;
|
|
381
|
+
}
|
|
382
|
+
writeElement(elementType, element, level, closeElement = true) {
|
|
383
|
+
let elementDsl = "";
|
|
384
|
+
elementDsl += this.writeLine(`${element.canonicalName} = ${elementType} "${element.name}" {`, level);
|
|
385
|
+
if (element.description) {
|
|
386
|
+
elementDsl += this.writeLine(`description "${element.description}"`, level + 1);
|
|
387
|
+
}
|
|
388
|
+
elementDsl += this.writeLine(`tags ${element.tags.map((tag) => `"${tag}"`).join(" ")}`, level + 1);
|
|
389
|
+
if (closeElement) {
|
|
390
|
+
elementDsl += this.writeLine(`}`, level);
|
|
391
|
+
}
|
|
392
|
+
return elementDsl;
|
|
393
|
+
}
|
|
394
|
+
writeComponent(component, level) {
|
|
395
|
+
let componentDsl = "";
|
|
396
|
+
componentDsl += this.writeElement("component", component, level, false);
|
|
397
|
+
componentDsl += this.writeLine(`technology "${component.technology}"`, level + 1);
|
|
398
|
+
componentDsl += this.writeLine(`}`, level);
|
|
399
|
+
return componentDsl;
|
|
400
|
+
}
|
|
401
|
+
writeContainerGroup(group, level) {
|
|
402
|
+
let containerGroupDsl = "";
|
|
403
|
+
containerGroupDsl += this.writeLine(`${group.name} = group "${group.name}" {`, level);
|
|
404
|
+
group.getComponents().forEach((component) => {
|
|
405
|
+
containerGroupDsl += this.writeComponent(component, level + 1);
|
|
406
|
+
});
|
|
407
|
+
containerGroupDsl += this.writeLine(`}`, level);
|
|
408
|
+
return containerGroupDsl;
|
|
409
|
+
}
|
|
410
|
+
writeContainer(container, level) {
|
|
411
|
+
let containerDsl = "";
|
|
412
|
+
containerDsl += this.writeElement("container", container, level, false);
|
|
413
|
+
containerDsl += this.writeLine(`technology "${container.technology}"`, level + 1);
|
|
414
|
+
container.getComponentsNotInGroups().forEach((component) => {
|
|
415
|
+
containerDsl += this.writeComponent(component, level + 1);
|
|
416
|
+
});
|
|
417
|
+
container.getGroups().forEach((group) => {
|
|
418
|
+
containerDsl += this.writeContainerGroup(group, level + 1);
|
|
419
|
+
});
|
|
420
|
+
containerDsl += this.writeLine(`}`, level);
|
|
421
|
+
return containerDsl;
|
|
422
|
+
}
|
|
423
|
+
writeSoftwareSystemGroup(group, level) {
|
|
424
|
+
let softwareSystemGroupDsl = "";
|
|
425
|
+
softwareSystemGroupDsl += this.writeLine(`${group.name} = group "${group.name}" {`, level);
|
|
426
|
+
group.getContainers().forEach((container) => {
|
|
427
|
+
softwareSystemGroupDsl += this.writeContainer(container, level + 1);
|
|
428
|
+
});
|
|
429
|
+
softwareSystemGroupDsl += this.writeLine(`}`, level);
|
|
430
|
+
return softwareSystemGroupDsl;
|
|
431
|
+
}
|
|
432
|
+
writeSoftwareSystem(softwareSystem, level) {
|
|
433
|
+
let softwareSystemDsl = "";
|
|
434
|
+
softwareSystemDsl += this.writeElement("softwareSystem", softwareSystem, level, false);
|
|
435
|
+
softwareSystem.getContainersNotInGroups().forEach((container) => {
|
|
436
|
+
softwareSystemDsl += this.writeContainer(container, level + 1);
|
|
437
|
+
});
|
|
438
|
+
softwareSystem.getGroups().forEach((group) => {
|
|
439
|
+
softwareSystemDsl += this.writeSoftwareSystemGroup(group, level + 1);
|
|
440
|
+
});
|
|
441
|
+
softwareSystemDsl += this.writeLine(`}`, level);
|
|
442
|
+
return softwareSystemDsl;
|
|
443
|
+
}
|
|
444
|
+
writeRelationships(elements, level) {
|
|
445
|
+
let relationshipsDsl = "";
|
|
446
|
+
elements.forEach((element) => {
|
|
447
|
+
element.getRelationshipsInHierarchy().forEach((relationship) => {
|
|
448
|
+
const tech = relationship.technology ? ` "${relationship.technology}"` : "";
|
|
449
|
+
relationshipsDsl += this.writeLine(
|
|
450
|
+
`${relationship.source.canonicalName} -> ${relationship.destination.canonicalName} "${relationship.description ?? "uses"}"${tech} {`,
|
|
451
|
+
level
|
|
452
|
+
);
|
|
453
|
+
relationshipsDsl += this.writeLine(`tags ${relationship.tags.map((tag) => `"${tag}"`).join(" ")}`, level + 1);
|
|
454
|
+
relationshipsDsl += this.writeLine(`}`, level);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
return relationshipsDsl;
|
|
458
|
+
}
|
|
459
|
+
writeModelGroup(group, level) {
|
|
460
|
+
let modelGroupDsl = "";
|
|
461
|
+
modelGroupDsl += this.writeLine(`${group.name} = group "${group.name}" {`, level);
|
|
462
|
+
group.getPeople().forEach((person) => {
|
|
463
|
+
modelGroupDsl += this.writeElement("person", person, level + 1);
|
|
464
|
+
});
|
|
465
|
+
group.getSoftwareSystems().forEach((softwareSystem) => {
|
|
466
|
+
modelGroupDsl += this.writeSoftwareSystem(softwareSystem, level + 1);
|
|
467
|
+
});
|
|
468
|
+
modelGroupDsl += this.writeLine(`}`, level);
|
|
469
|
+
return modelGroupDsl;
|
|
470
|
+
}
|
|
471
|
+
writeModel(model, level) {
|
|
472
|
+
let modelDsl = "";
|
|
473
|
+
modelDsl += this.writeLine(`model {`, level);
|
|
474
|
+
modelDsl += this.writeLine("// Elements", level + 1);
|
|
475
|
+
model.getPeopleNotInGroups().forEach((person) => {
|
|
476
|
+
modelDsl += this.writeElement("person", person, level + 1);
|
|
477
|
+
});
|
|
478
|
+
model.getSoftwareSystemsNotInGroups().forEach((softwareSystem) => {
|
|
479
|
+
modelDsl += this.writeSoftwareSystem(softwareSystem, level + 1);
|
|
480
|
+
});
|
|
481
|
+
model.getGroups().forEach((group) => {
|
|
482
|
+
modelDsl += this.writeModelGroup(group, level + 1);
|
|
483
|
+
});
|
|
484
|
+
modelDsl += this.writeLine("// Relationships", level + 1);
|
|
485
|
+
modelDsl += this.writeRelationships(model.getPeople().concat(model.getSoftwareSystems()), level + 1);
|
|
486
|
+
modelDsl += this.writeLine(`}`, level);
|
|
487
|
+
return modelDsl;
|
|
488
|
+
}
|
|
489
|
+
writeView(view, viewType, level) {
|
|
490
|
+
let viewDsl = this.writeLine(
|
|
491
|
+
`${viewType}${view.subject ? ' "' + view.subject.canonicalName + '"' : ""} "${view.key}" {`,
|
|
492
|
+
level
|
|
493
|
+
);
|
|
494
|
+
viewDsl += this.writeLine(`description "${view.description}"`, level + 1);
|
|
495
|
+
if (view.title) {
|
|
496
|
+
viewDsl += this.writeLine(`title "${view.title}"`, level + 1);
|
|
497
|
+
}
|
|
498
|
+
view.scopes.forEach((scope) => {
|
|
499
|
+
viewDsl += this.writeLine(`${scope}`, level + 1);
|
|
500
|
+
});
|
|
501
|
+
viewDsl += this.writeLine(`}`, level);
|
|
502
|
+
return viewDsl;
|
|
503
|
+
}
|
|
504
|
+
writeViews(views, level) {
|
|
505
|
+
let viewDsl = "";
|
|
506
|
+
viewDsl += this.writeLine(`views {`, level);
|
|
507
|
+
viewDsl += this.writeLine("// System Landscape Views", level + 1);
|
|
508
|
+
views.systemLandscapeViews.forEach((view) => {
|
|
509
|
+
viewDsl += this.writeView(view, "systemLandscape", level + 1);
|
|
510
|
+
});
|
|
511
|
+
viewDsl += this.writeLine("// System Context Views", level + 1);
|
|
512
|
+
views.systemContextViews.forEach((view) => {
|
|
513
|
+
viewDsl += this.writeView(view, "systemContext", level + 1);
|
|
514
|
+
});
|
|
515
|
+
viewDsl += this.writeLine("// Container Views", level + 1);
|
|
516
|
+
views.containerViews.forEach((view) => {
|
|
517
|
+
viewDsl += this.writeView(view, "container", level + 1);
|
|
518
|
+
});
|
|
519
|
+
viewDsl += this.writeLine("// Component Views", level + 1);
|
|
520
|
+
views.componentViews.forEach((view) => {
|
|
521
|
+
viewDsl += this.writeView(view, "component", level + 1);
|
|
522
|
+
});
|
|
523
|
+
viewDsl += this.writeLine(`theme default`, level + 1);
|
|
524
|
+
viewDsl += this.writeLine(`}`, level);
|
|
525
|
+
return viewDsl;
|
|
526
|
+
}
|
|
527
|
+
write() {
|
|
528
|
+
let dsl = "";
|
|
529
|
+
this.model.validate();
|
|
530
|
+
dsl += this.writeLine(`workspace "${this.model.name}" {`, 0);
|
|
531
|
+
dsl += this.writeModel(this.model, 1);
|
|
532
|
+
dsl += this.writeViews(this.views, 1);
|
|
533
|
+
dsl += this.writeLine(`}`, 0);
|
|
534
|
+
return dsl;
|
|
535
|
+
}
|
|
536
|
+
writeLine(line, level) {
|
|
537
|
+
const indent = " ".repeat(level * INDENT_SIZE);
|
|
538
|
+
return `${indent}${line}
|
|
539
|
+
`;
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// libs/c4-model/src/generateDiagrams.ts
|
|
544
|
+
import * as fs from "fs";
|
|
545
|
+
import * as os from "os";
|
|
546
|
+
import * as path from "path";
|
|
547
|
+
import { GenericContainer, Wait } from "testcontainers";
|
|
548
|
+
async function generateDiagrams(options) {
|
|
549
|
+
const { views: viewsFactory, outputDir, ...buildOptions } = options;
|
|
550
|
+
const { model, catalog } = await buildModelWithCatalog(buildOptions);
|
|
551
|
+
const views = viewsFactory(catalog);
|
|
552
|
+
const dsl = new StructurizrDSLWriter(model, views).write();
|
|
553
|
+
const tmpDir = await fs.promises.mkdtemp(path.join(fs.realpathSync(os.tmpdir()), "c4-diagrams-"));
|
|
554
|
+
await fs.promises.writeFile(path.join(tmpDir, "workspace.dsl"), dsl, "utf8");
|
|
555
|
+
await new GenericContainer("structurizr/structurizr").withBindMounts([{ source: tmpDir, target: "/workspace", mode: "rw" }]).withCommand(["export", "-w", "/workspace/workspace.dsl", "-f", "mermaid", "-o", "/workspace"]).withWaitStrategy(Wait.forOneShotStartup()).start();
|
|
556
|
+
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
557
|
+
const tmpFiles = await fs.promises.readdir(tmpDir);
|
|
558
|
+
const mmdFiles = tmpFiles.filter((f) => f.endsWith(".mmd"));
|
|
559
|
+
for (const file of mmdFiles) {
|
|
560
|
+
await fs.promises.copyFile(path.join(tmpDir, file), path.join(outputDir, file));
|
|
561
|
+
}
|
|
562
|
+
const generatedFiles = mmdFiles.map((f) => path.join(outputDir, f));
|
|
563
|
+
for (const file of mmdFiles) {
|
|
564
|
+
const baseName = path.basename(file, ".mmd");
|
|
565
|
+
const pngFile = `${baseName}.png`;
|
|
566
|
+
await new GenericContainer("minlag/mermaid-cli").withBindMounts([{ source: tmpDir, target: "/data", mode: "rw" }]).withCommand(["-i", `/data/${file}`, "-o", `/data/${pngFile}`]).withWaitStrategy(Wait.forOneShotStartup()).start();
|
|
567
|
+
await fs.promises.copyFile(path.join(tmpDir, pngFile), path.join(outputDir, pngFile));
|
|
568
|
+
generatedFiles.push(path.join(outputDir, pngFile));
|
|
569
|
+
}
|
|
570
|
+
return generatedFiles;
|
|
571
|
+
}
|
|
572
|
+
export {
|
|
573
|
+
Component,
|
|
574
|
+
Container,
|
|
575
|
+
ContainerGroup,
|
|
576
|
+
Model,
|
|
577
|
+
ModelGroup,
|
|
578
|
+
Person,
|
|
579
|
+
SoftwareSystem,
|
|
580
|
+
SoftwareSystemGroup,
|
|
581
|
+
StructurizrDSLWriter,
|
|
582
|
+
View,
|
|
583
|
+
Views,
|
|
584
|
+
buildModel,
|
|
585
|
+
buildModelWithCatalog,
|
|
586
|
+
generateDiagrams
|
|
587
|
+
};
|
package/CLAUDE.md
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# c4-model
|
|
2
|
-
|
|
3
|
-
## About
|
|
4
|
-
|
|
5
|
-
TypeScript library for defining C4 architecture models and generating Structurizr DSL.
|
|
6
|
-
|
|
7
|
-
## Structurizr DSL Reference
|
|
8
|
-
|
|
9
|
-
Structurizr DSL docs are available locally at `docs/structurizr/` (run
|
|
10
|
-
`pnpm fetch-structurizr-docs` to populate). When working on `structurizrDslWriter.ts`,
|
|
11
|
-
read the relevant files from that directory. If not present locally, source is:
|
|
12
|
-
https://github.com/structurizr/structurizr.github.io/tree/main/dsl
|
|
13
|
-
|
|
14
|
-
## Running tasks
|
|
15
|
-
|
|
16
|
-
```sh
|
|
17
|
-
pnpm exec nx run c4-model:test
|
|
18
|
-
pnpm exec nx run c4-model:build
|
|
19
|
-
pnpm exec nx run c4-model:lint
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Architecture
|
|
23
|
-
|
|
24
|
-
- `core.ts` — Base classes: `Element`, `TechnicalElement`, `Relationship`, `Reference`, `Group`
|
|
25
|
-
- `model.ts` — `Model` class, `C4Module` interface, `buildModel()` two-phase loader
|
|
26
|
-
- `person.ts` / `softwareSystem.ts` / `container.ts` / `component.ts` — C4 element types
|
|
27
|
-
- `views.ts` — View definitions (system landscape, context, container, component)
|
|
28
|
-
- `structurizrDslWriter.ts` — Generates Structurizr DSL from a Model + Views
|
|
29
|
-
|
|
30
|
-
## C4Module pattern
|
|
31
|
-
|
|
32
|
-
Modules are defined in `c4.dsl.ts` files discovered by `buildModel()` via glob. Each exports a `c4Module`:
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
export const c4Module: C4Module<RootCatalog, LocalCatalog> = {
|
|
36
|
-
key: 'myModule',
|
|
37
|
-
registerDefinitions(model) { /* define elements, return catalog */ },
|
|
38
|
-
buildRelationships(local, dependencies) { /* wire up cross-module relationships */ },
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
- **Phase 1**: All modules register definitions; results stored in a root catalog keyed by module key
|
|
43
|
-
- **Phase 2**: Each module receives its own catalog and all other modules' catalogs to build relationships
|
|
44
|
-
- `CatalogKeyOf` and `Dependencies` utility types ensure type-safe keys and dependency access
|
|
45
|
-
- See `libs/c4-model-examples` for integration tests using `buildModel()`
|
package/src/component.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Component = void 0;
|
|
4
|
-
const core_1 = require("./core");
|
|
5
|
-
class Component extends core_1.TechnicalElement {
|
|
6
|
-
constructor(name, definition) {
|
|
7
|
-
super(name, ['Component'], definition);
|
|
8
|
-
this.name = name;
|
|
9
|
-
}
|
|
10
|
-
getChildElements() {
|
|
11
|
-
return [];
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
exports.Component = Component;
|
|
15
|
-
//# sourceMappingURL=component.js.map
|
package/src/component.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"component.js","sourceRoot":"","sources":["../../../../libs/c4-model/src/component.ts"],"names":[],"mappings":";;;AAAA,iCAAwE;AAIxE,MAAa,SAAU,SAAQ,uBAAgB;IAC3C,YAC6B,IAAY,EACrC,UAAgC;QAEhC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,EAAE,UAAU,CAAC,CAAA;QAHb,SAAI,GAAJ,IAAI,CAAQ;IAIzC,CAAC;IAEM,gBAAgB;QACnB,OAAO,EAAE,CAAA;IACb,CAAC;CACJ;AAXD,8BAWC"}
|