@likec4/core 1.12.2 → 1.13.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.
Files changed (35) hide show
  1. package/dist/index.cjs +777 -251
  2. package/dist/index.d.cts +301 -32
  3. package/dist/index.d.mts +301 -32
  4. package/dist/index.d.ts +301 -32
  5. package/dist/index.mjs +774 -252
  6. package/dist/shared/{core.BvFch1jG.mjs → core.BJDxVHFh.mjs} +19 -19
  7. package/dist/shared/{core.dY8efyk0.d.ts → core.C4jDMQ1i.d.cts} +242 -238
  8. package/dist/shared/{core.dY8efyk0.d.cts → core.C4jDMQ1i.d.mts} +242 -238
  9. package/dist/shared/{core.dY8efyk0.d.mts → core.C4jDMQ1i.d.ts} +242 -238
  10. package/dist/shared/{core.CTiE4teS.cjs → core.IR7XOEkQ.cjs} +19 -18
  11. package/dist/types/index.cjs +1 -1
  12. package/dist/types/index.d.cts +1 -1
  13. package/dist/types/index.d.mts +1 -1
  14. package/dist/types/index.d.ts +1 -1
  15. package/dist/types/index.mjs +1 -1
  16. package/package.json +4 -3
  17. package/src/builder/Builder.element.ts +221 -0
  18. package/src/builder/Builder.model.ts +346 -0
  19. package/src/builder/Builder.test-d.ts +69 -0
  20. package/src/builder/Builder.ts +574 -0
  21. package/src/builder/Builder.view.ts +358 -0
  22. package/src/builder/Builder.views.ts +318 -0
  23. package/src/builder/_types.ts +195 -0
  24. package/src/builder/index.ts +5 -0
  25. package/src/index.ts +4 -0
  26. package/src/model/LikeC4DiagramModel.ts +5 -5
  27. package/src/model/LikeC4Model.ts +63 -46
  28. package/src/model/LikeC4ViewModel.ts +7 -7
  29. package/src/model/__test__/fixture.ts +5 -0
  30. package/src/types/element.ts +19 -11
  31. package/src/types/model.ts +13 -8
  32. package/src/types/relation.ts +1 -1
  33. package/src/types/view-changes.ts +6 -2
  34. package/src/types/view.ts +53 -22
  35. package/src/utils/fqn.ts +12 -11
@@ -0,0 +1,574 @@
1
+ import defu from 'defu'
2
+ import { fromEntries, hasAtLeast, isFunction, isNonNullish, map, mapToObj, mapValues, pickBy } from 'remeda'
3
+ import type { Writable } from 'type-fest'
4
+ import { invariant } from '../errors'
5
+ import {
6
+ type Color,
7
+ DefaultElementShape,
8
+ DefaultThemeColor,
9
+ type Element,
10
+ type ElementShape,
11
+ type ElementView,
12
+ type Fqn,
13
+ type IconUrl,
14
+ isScopedElementView,
15
+ type LikeC4View,
16
+ type Link,
17
+ type NonEmptyArray,
18
+ type Relation,
19
+ type RelationID,
20
+ type Tag
21
+ } from '../types'
22
+ import type { ParsedLikeC4Model } from '../types/model'
23
+ import { isSameHierarchy, nameFromFqn, parentFqn } from '../utils/fqn'
24
+ import type { AnyTypes, BuilderSpecification, Types } from './_types'
25
+ import type { AddElement, AddElementHelpers } from './Builder.element'
26
+ import { type ModelBuilder, type ModelHelpers } from './Builder.model'
27
+ import {
28
+ $autoLayout,
29
+ $exclude,
30
+ $expr,
31
+ $include,
32
+ $rules,
33
+ $style,
34
+ type ViewBuilder,
35
+ type ViewHelpers,
36
+ type ViewRuleBuilderOp
37
+ } from './Builder.view'
38
+ import { type ViewsBuilder } from './Builder.views'
39
+
40
+ export interface Builder<T extends AnyTypes> {
41
+ /**
42
+ * Only available in compile time
43
+ */
44
+ readonly Types: T
45
+
46
+ clone(): Builder<T>
47
+
48
+ /**
49
+ * Builders for each element kind
50
+ */
51
+ helpers(): {
52
+ model: ModelHelpers<T>
53
+ views: ViewHelpers<T['NewViewProps']>
54
+ }
55
+
56
+ __model(): ModelBuilder<T>
57
+ __views(): ViewsBuilder<T>
58
+
59
+ build(): ParsedLikeC4Model<
60
+ T['ElementKind'],
61
+ T['RelationshipKind'],
62
+ T['Tag'],
63
+ T['Fqn'],
64
+ T['ViewId']
65
+ >
66
+
67
+ with<
68
+ A extends AnyTypes
69
+ >(
70
+ op1: (input: Builder<T>) => Builder<A>
71
+ ): Builder<A>
72
+
73
+ with<
74
+ A extends AnyTypes,
75
+ B extends AnyTypes
76
+ >(
77
+ op1: (input: Builder<T>) => Builder<A>,
78
+ op2: (input: Builder<A>) => Builder<B>
79
+ ): Builder<B>
80
+
81
+ with<
82
+ A extends AnyTypes,
83
+ B extends AnyTypes,
84
+ C extends AnyTypes
85
+ >(
86
+ op1: (input: Builder<T>) => Builder<A>,
87
+ op2: (input: Builder<A>) => Builder<B>,
88
+ op3: (input: Builder<B>) => Builder<C>
89
+ ): Builder<C>
90
+
91
+ with<
92
+ A extends AnyTypes,
93
+ B extends AnyTypes,
94
+ C extends AnyTypes,
95
+ D extends AnyTypes
96
+ >(
97
+ op1: (input: Builder<T>) => Builder<A>,
98
+ op2: (input: Builder<A>) => Builder<B>,
99
+ op3: (input: Builder<B>) => Builder<C>,
100
+ op4: (input: Builder<C>) => Builder<D>
101
+ ): Builder<D>
102
+
103
+ with<
104
+ A extends AnyTypes,
105
+ B extends AnyTypes,
106
+ C extends AnyTypes,
107
+ D extends AnyTypes,
108
+ E extends AnyTypes
109
+ >(
110
+ op1: (input: Builder<T>) => Builder<A>,
111
+ op2: (input: Builder<A>) => Builder<B>,
112
+ op3: (input: Builder<B>) => Builder<C>,
113
+ op4: (input: Builder<C>) => Builder<D>,
114
+ op5: (input: Builder<D>) => Builder<E>
115
+ ): Builder<E>
116
+
117
+ with<
118
+ A extends AnyTypes,
119
+ B extends AnyTypes,
120
+ C extends AnyTypes,
121
+ D extends AnyTypes,
122
+ E extends AnyTypes,
123
+ F extends AnyTypes
124
+ >(
125
+ op1: (input: Builder<T>) => Builder<A>,
126
+ op2: (input: Builder<A>) => Builder<B>,
127
+ op3: (input: Builder<B>) => Builder<C>,
128
+ op4: (input: Builder<C>) => Builder<D>,
129
+ op5: (input: Builder<D>) => Builder<E>,
130
+ op6: (input: Builder<E>) => Builder<F>
131
+ ): Builder<F>
132
+
133
+ with<
134
+ A extends AnyTypes,
135
+ B extends AnyTypes,
136
+ C extends AnyTypes,
137
+ D extends AnyTypes,
138
+ E extends AnyTypes,
139
+ F extends AnyTypes,
140
+ G extends AnyTypes
141
+ >(
142
+ op1: (input: Builder<T>) => Builder<A>,
143
+ op2: (input: Builder<A>) => Builder<B>,
144
+ op3: (input: Builder<B>) => Builder<C>,
145
+ op4: (input: Builder<C>) => Builder<D>,
146
+ op5: (input: Builder<D>) => Builder<E>,
147
+ op6: (input: Builder<E>) => Builder<F>,
148
+ op7: (input: Builder<F>) => Builder<G>
149
+ ): Builder<G>
150
+
151
+ with<
152
+ A extends AnyTypes,
153
+ B extends AnyTypes,
154
+ C extends AnyTypes,
155
+ D extends AnyTypes,
156
+ E extends AnyTypes,
157
+ F extends AnyTypes,
158
+ G extends AnyTypes,
159
+ H extends AnyTypes
160
+ >(
161
+ op1: (input: Builder<T>) => Builder<A>,
162
+ op2: (input: Builder<A>) => Builder<B>,
163
+ op3: (input: Builder<B>) => Builder<C>,
164
+ op4: (input: Builder<C>) => Builder<D>,
165
+ op5: (input: Builder<D>) => Builder<E>,
166
+ op6: (input: Builder<E>) => Builder<F>,
167
+ op7: (input: Builder<F>) => Builder<G>,
168
+ op8: (input: Builder<G>) => Builder<H>
169
+ ): Builder<H>
170
+
171
+ with<
172
+ A extends AnyTypes,
173
+ B extends AnyTypes,
174
+ C extends AnyTypes,
175
+ D extends AnyTypes,
176
+ E extends AnyTypes,
177
+ F extends AnyTypes,
178
+ G extends AnyTypes,
179
+ H extends AnyTypes,
180
+ I extends AnyTypes
181
+ >(
182
+ op1: (input: Builder<T>) => Builder<A>,
183
+ op2: (input: Builder<A>) => Builder<B>,
184
+ op3: (input: Builder<B>) => Builder<C>,
185
+ op4: (input: Builder<C>) => Builder<D>,
186
+ op5: (input: Builder<D>) => Builder<E>,
187
+ op6: (input: Builder<E>) => Builder<F>,
188
+ op7: (input: Builder<F>) => Builder<G>,
189
+ op8: (input: Builder<G>) => Builder<H>,
190
+ op9: (input: Builder<H>) => Builder<I>
191
+ ): Builder<I>
192
+
193
+ with<
194
+ A extends AnyTypes,
195
+ B extends AnyTypes,
196
+ C extends AnyTypes,
197
+ D extends AnyTypes,
198
+ E extends AnyTypes,
199
+ F extends AnyTypes,
200
+ G extends AnyTypes,
201
+ H extends AnyTypes,
202
+ I extends AnyTypes,
203
+ J extends AnyTypes
204
+ >(
205
+ op1: (input: Builder<T>) => Builder<A>,
206
+ op2: (input: Builder<A>) => Builder<B>,
207
+ op3: (input: Builder<B>) => Builder<C>,
208
+ op4: (input: Builder<C>) => Builder<D>,
209
+ op5: (input: Builder<D>) => Builder<E>,
210
+ op6: (input: Builder<E>) => Builder<F>,
211
+ op7: (input: Builder<F>) => Builder<G>,
212
+ op8: (input: Builder<G>) => Builder<H>,
213
+ op9: (input: Builder<H>) => Builder<I>,
214
+ op10: (input: Builder<I>) => Builder<J>
215
+ ): Builder<J>
216
+ }
217
+
218
+ function builder<Spec extends BuilderSpecification, T extends AnyTypes>(
219
+ spec: Spec,
220
+ _elements = new Map<string, Element>(),
221
+ _relations = [] as Relation[],
222
+ _views = new Map<string, LikeC4View>()
223
+ ): Builder<T> {
224
+ const toLikeC4Specification = (): Types.ToParsedLikeC4Model<T>['specification'] => ({
225
+ tags: (spec.tags ?? []) as Tag<T['Tag']>[],
226
+ elements: {
227
+ ...spec.elements as any
228
+ },
229
+ relationships: {
230
+ ...spec.relationships
231
+ }
232
+ })
233
+
234
+ const mapLinks = (links?: Array<string | { title?: string; url: string }>): NonEmptyArray<Link> | null => {
235
+ if (!links || !hasAtLeast(links, 1)) {
236
+ return null
237
+ }
238
+ return map(links, l => (typeof l === 'string' ? { url: l } : l))
239
+ }
240
+
241
+ const mkViewBuilder = (view: Writable<ElementView>): ViewBuilder<T> => {
242
+ const viewBuilder: ViewBuilder<T> = {
243
+ autoLayout(autoLayout) {
244
+ view.rules.push({
245
+ direction: autoLayout
246
+ })
247
+ return viewBuilder
248
+ },
249
+ exclude(expr) {
250
+ view.rules.push({
251
+ exclude: [expr]
252
+ })
253
+ return viewBuilder
254
+ },
255
+ include(expr) {
256
+ view.rules.push({
257
+ include: [expr]
258
+ })
259
+ return viewBuilder
260
+ },
261
+ style(rule) {
262
+ view.rules.push(rule)
263
+ return viewBuilder
264
+ }
265
+ // title(title: string) {
266
+ // view.title = title
267
+ // return viewBuilder
268
+ // },
269
+ // description(description: string) {
270
+ // view.description = description
271
+ // return viewBuilder
272
+ // }
273
+ }
274
+ return viewBuilder
275
+ }
276
+
277
+ const self: Builder<T> = {
278
+ get Types(): T {
279
+ throw new Error('Types are not available in runtime')
280
+ },
281
+ clone: () =>
282
+ builder(
283
+ structuredClone(spec),
284
+ structuredClone(_elements),
285
+ structuredClone(_relations),
286
+ structuredClone(_views)
287
+ ),
288
+ __model: () => ({
289
+ addElement: (element) => {
290
+ const parent = parentFqn(element.id)
291
+ if (parent) {
292
+ invariant(
293
+ _elements.get(parent),
294
+ `Parent element with id "${parent}" not found for element with id "${element.id}"`
295
+ )
296
+ }
297
+ if (_elements.has(element.id)) {
298
+ throw new Error(`Element with id "${element.id}" already exists`)
299
+ }
300
+ _elements.set(element.id, element)
301
+ return self
302
+ },
303
+ addRelation(relation) {
304
+ const sourceEl = _elements.get(relation.source)
305
+ invariant(sourceEl, `Element with id "${relation.source}" not found`)
306
+ const targetEl = _elements.get(relation.target)
307
+ invariant(targetEl, `Element with id "${relation.target}" not found`)
308
+ invariant(
309
+ !isSameHierarchy(sourceEl, targetEl),
310
+ 'Cannot create relationship between elements in the same hierarchy'
311
+ )
312
+ _relations.push({
313
+ id: `rel${_relations.length + 1}` as RelationID,
314
+ ...relation
315
+ })
316
+ return self
317
+ },
318
+ /**
319
+ * Fully qualified name for nested elements
320
+ */
321
+ fqn(id) {
322
+ return id as Fqn
323
+ },
324
+ addSourcelessRelation() {
325
+ throw new Error('Can be called only in nested model')
326
+ }
327
+ }),
328
+ __views: () => ({
329
+ addView: (view) => {
330
+ if (isScopedElementView(view)) {
331
+ invariant(_elements.get(view.viewOf), `Invalid view ${view.id}, wlement with id "${view.viewOf}" not found`)
332
+ }
333
+ _views.set(view.id, view)
334
+ return self
335
+ }
336
+ }),
337
+ build: () => ({
338
+ specification: toLikeC4Specification(),
339
+ elements: fromEntries(Array.from(_elements.entries())) as any,
340
+ relations: mapToObj(_relations, r => [r.id, r]),
341
+ views: fromEntries(Array.from(_views.entries())) as any
342
+ }),
343
+ helpers: () => ({
344
+ model: {
345
+ model: (...ops: ((b: ModelBuilder<T>) => ModelBuilder<T>)[]) => {
346
+ return (b: Builder<T>) => {
347
+ const v = b.__model()
348
+ for (const op of ops) {
349
+ op(v)
350
+ }
351
+ return b
352
+ }
353
+ },
354
+ rel: (source: string, target: string, _props?: T['NewRelationshipProps'] | string) => {
355
+ return <T extends AnyTypes>(b: ModelBuilder<T>) => {
356
+ const {
357
+ title = '',
358
+ links: _links = [],
359
+ ...props
360
+ } = defu(
361
+ typeof _props === 'string' ? { title: _props } : { ..._props },
362
+ { title: null, links: null }
363
+ )
364
+ const links = mapLinks(_links)
365
+ b.addRelation({
366
+ source: source as any,
367
+ target: target as any,
368
+ title,
369
+ ...(links && { links }),
370
+ ...props
371
+ })
372
+ return b
373
+ }
374
+ },
375
+ relTo: (target, _props?) => {
376
+ return <T extends AnyTypes>(b: ModelBuilder<T>) => {
377
+ const {
378
+ title = '',
379
+ links: _links = [],
380
+ ...props
381
+ } = defu(
382
+ typeof _props === 'string' ? { title: _props } : { ..._props },
383
+ { title: null, links: null }
384
+ )
385
+ const links = mapLinks(_links)
386
+ b.addSourcelessRelation({
387
+ target,
388
+ title,
389
+ ...(links && { links }),
390
+ ...props
391
+ })
392
+ return b
393
+ }
394
+ },
395
+ ...mapValues(
396
+ spec.elements,
397
+ ({ style: specStyle, ...spec }, kind) =>
398
+ <Id extends string>(
399
+ id: Id,
400
+ _props?: T['NewElementProps'] | string
401
+ ): AddElement<Id> => {
402
+ const add = (<T extends AnyTypes>(b: ModelBuilder<T>) => {
403
+ const {
404
+ links: _links,
405
+ icon: _icon,
406
+ style,
407
+ title,
408
+ ...props
409
+ } = typeof _props === 'string' ? { title: _props } : { ..._props }
410
+
411
+ const links = mapLinks(_links)
412
+
413
+ const icon = _icon ?? specStyle?.icon
414
+
415
+ const _id = b.fqn(id)
416
+
417
+ b.addElement({
418
+ id: _id,
419
+ kind: kind as any,
420
+ title: title ?? nameFromFqn(_id),
421
+ description: null,
422
+ technology: null,
423
+ tags: null,
424
+ color: specStyle?.color ?? DefaultThemeColor as Color,
425
+ shape: specStyle?.shape ?? DefaultElementShape as ElementShape,
426
+ style: pickBy({
427
+ border: specStyle?.border,
428
+ opacity: specStyle?.opacity,
429
+ ...style
430
+ }, isNonNullish),
431
+ links,
432
+ ...icon && { icon: icon as IconUrl },
433
+ ...spec,
434
+ ...props
435
+ })
436
+ return b
437
+ }) as AddElement<Id>
438
+
439
+ add.with = (...ops: Array<(input: ModelBuilder<any>) => ModelBuilder<any>>) => (b: ModelBuilder<any>) => {
440
+ add(b)
441
+ const nestedBuilder: ModelBuilder<any> = {
442
+ ...b,
443
+ fqn: (child) => `${b.fqn(id)}.${child}` as Fqn,
444
+ addSourcelessRelation: (relation) => {
445
+ return b.addRelation({
446
+ ...relation,
447
+ source: b.fqn(id)
448
+ })
449
+ }
450
+ }
451
+ for (const op of ops) {
452
+ op(nestedBuilder)
453
+ }
454
+ return b
455
+ }
456
+ return add
457
+ }
458
+ ) as AddElementHelpers<T>
459
+ },
460
+ views: {
461
+ views: (...ops: ((b: ViewsBuilder<T>) => ViewsBuilder<T>)[]) => {
462
+ return (b: Builder<T>) => {
463
+ const v = b.__views()
464
+ for (const op of ops) {
465
+ op(v)
466
+ }
467
+ return b
468
+ }
469
+ },
470
+ view: (id, _props, builder?: ViewRuleBuilderOp<T>) => {
471
+ if (isFunction(_props)) {
472
+ builder = _props
473
+ _props = {}
474
+ }
475
+ return <T extends AnyTypes>(b: ViewsBuilder<T>): ViewsBuilder<T> => {
476
+ const {
477
+ links: _links = [],
478
+ title = null,
479
+ ...props
480
+ } = typeof _props === 'string' ? { title: _props } : { ..._props }
481
+
482
+ const links = mapLinks(_links)
483
+
484
+ const view: Writable<ElementView> = {
485
+ id: id as any,
486
+ __: 'element',
487
+ title,
488
+ description: null,
489
+ tags: null,
490
+ rules: [],
491
+ links,
492
+ customColorDefinitions: {},
493
+ ...props
494
+ }
495
+ b.addView(view)
496
+
497
+ if (builder) {
498
+ builder(mkViewBuilder(view))
499
+ }
500
+
501
+ return b
502
+ }
503
+ },
504
+ viewOf: (
505
+ id,
506
+ viewOf,
507
+ _props?: T['NewViewProps'] | string | ViewRuleBuilderOp<T>,
508
+ builder?: ViewRuleBuilderOp<T>
509
+ ) => {
510
+ if (isFunction(_props)) {
511
+ builder = _props
512
+ _props = {}
513
+ }
514
+ return <T extends AnyTypes>(b: ViewsBuilder<T>): ViewsBuilder<T> => {
515
+ const {
516
+ links: _links = [],
517
+ title = null,
518
+ ...props
519
+ } = typeof _props === 'string' ? { title: _props } : { ..._props }
520
+
521
+ const links = mapLinks(_links)
522
+
523
+ const view: Writable<ElementView> = {
524
+ id: id as any,
525
+ __: 'element',
526
+ viewOf,
527
+ title,
528
+ description: null,
529
+ tags: null,
530
+ rules: [],
531
+ links,
532
+ customColorDefinitions: {},
533
+ ...props
534
+ }
535
+ b.addView(view)
536
+
537
+ if (builder) {
538
+ builder(mkViewBuilder(view))
539
+ }
540
+
541
+ return b
542
+ }
543
+ },
544
+ $autoLayout,
545
+ $exclude,
546
+ $expr,
547
+ $include,
548
+ $rules,
549
+ $style
550
+ }
551
+ }),
552
+ with: (...ops: ((b: Builder<T>) => Builder<T>)[]) => {
553
+ return ops.reduce((b, op) => op(b), self).clone()
554
+ }
555
+ }
556
+
557
+ return self
558
+ }
559
+
560
+ export namespace Builder {
561
+ export function forSpecification<const Spec extends BuilderSpecification>(
562
+ spec: Spec
563
+ ): {
564
+ builder: Builder<Types.FromSpecification<Spec>>
565
+ model: ModelHelpers<Types.FromSpecification<Spec>>
566
+ views: ViewHelpers<Types.FromSpecification<Spec>['NewViewProps']>
567
+ } {
568
+ const b = builder<Spec, Types.FromSpecification<Spec>>(spec)
569
+ return {
570
+ ...b.helpers(),
571
+ builder: b
572
+ }
573
+ }
574
+ }