@likec4/core 1.17.0 → 1.18.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/dist/compute-view/index.d.mts +91 -0
- package/dist/compute-view/index.d.ts +91 -0
- package/dist/compute-view/index.mjs +79 -0
- package/dist/index.d.mts +248 -435
- package/dist/index.d.ts +248 -435
- package/dist/index.mjs +418 -1322
- package/dist/model/index.d.mts +642 -0
- package/dist/model/index.d.ts +642 -0
- package/dist/model/index.mjs +2 -0
- package/dist/shared/core.BBgiAAv1.mjs +144 -0
- package/dist/shared/core.BPyJvuMU.mjs +2420 -0
- package/dist/shared/core.BQ4AbfvB.mjs +9228 -0
- package/dist/shared/{core.CoLQkcYL.d.ts → core.DMYY7C8s.d.mts} +398 -249
- package/dist/shared/{core.CoLQkcYL.d.cts → core.DMYY7C8s.d.ts} +398 -249
- package/dist/shared/core.k12s_keD.mjs +203 -0
- package/dist/types/index.d.mts +69 -1
- package/dist/types/index.d.ts +69 -1
- package/dist/types/index.mjs +21 -1
- package/package.json +40 -42
- package/src/builder/Builder.deployment.ts +219 -0
- package/src/builder/Builder.deploymentModel.ts +360 -0
- package/src/builder/Builder.element.ts +17 -19
- package/src/builder/Builder.model.ts +13 -2
- package/src/builder/Builder.test-d.ts +59 -4
- package/src/builder/Builder.ts +238 -53
- package/src/builder/Builder.view.ts +238 -53
- package/src/builder/_types.ts +87 -15
- package/src/compute-view/LikeC4ModelGraph.ts +330 -0
- package/src/compute-view/compute-view.ts +92 -0
- package/src/compute-view/deployment-view/Memory.ts +108 -0
- package/src/compute-view/deployment-view/Stage.ts +145 -0
- package/src/compute-view/deployment-view/__test__/fixture.ts +210 -0
- package/src/compute-view/deployment-view/_types.ts +29 -0
- package/src/compute-view/deployment-view/clean-connections.ts +229 -0
- package/src/compute-view/deployment-view/compute.ts +111 -0
- package/src/compute-view/deployment-view/predicates/deployment-ref.ts +146 -0
- package/src/compute-view/deployment-view/predicates/relation-direct.ts +130 -0
- package/src/compute-view/deployment-view/predicates/relation-in-out.ts +43 -0
- package/src/compute-view/deployment-view/predicates/relation-incoming.ts +46 -0
- package/src/compute-view/deployment-view/predicates/relation-outgoing.ts +49 -0
- package/src/compute-view/deployment-view/predicates/wildcard.ts +29 -0
- package/src/compute-view/deployment-view/utils.ts +212 -0
- package/src/compute-view/dynamic-view/__test__/fixture.ts +58 -0
- package/src/compute-view/dynamic-view/compute.ts +310 -0
- package/src/compute-view/element-view/__test__/__snapshots__/legacy.spec.ts.snap +456 -0
- package/src/compute-view/element-view/__test__/fixture.ts +645 -0
- package/src/compute-view/element-view/compute.ts +734 -0
- package/src/compute-view/element-view/predicates.ts +517 -0
- package/src/compute-view/index.ts +5 -0
- package/src/compute-view/utils/__snapshots__/sortNodes.spec.ts.snap +113 -0
- package/src/compute-view/utils/ancestorsOfNode.ts +19 -0
- package/src/compute-view/utils/applyCustomElementProperties.ts +66 -0
- package/src/compute-view/utils/applyCustomRelationProperties.ts +41 -0
- package/src/compute-view/utils/applyViewRuleStyles.ts +53 -0
- package/src/compute-view/utils/buildComputedNodes.ts +148 -0
- package/src/compute-view/utils/buildElementNotations.ts +63 -0
- package/src/compute-view/utils/derive-edge-props-from-relationships.ts +97 -0
- package/src/compute-view/utils/elementExpressionToPredicate.ts +52 -0
- package/src/compute-view/utils/link-nodes-with-edges.ts +58 -0
- package/src/compute-view/utils/relationExpressionToPredicates.ts +55 -0
- package/src/compute-view/utils/resolve-extended-views.ts +65 -0
- package/src/compute-view/utils/resolve-global-rules.ts +83 -0
- package/src/compute-view/utils/sortNodes.ts +101 -0
- package/src/compute-view/utils/topological-sort.ts +160 -0
- package/src/compute-view/utils/uniqueTags.test.ts +42 -0
- package/src/compute-view/utils/uniqueTags.ts +21 -0
- package/src/compute-view/utils/view-hash.ts +27 -0
- package/src/compute-view/utils/with-readable-edges.ts +31 -0
- package/src/index.ts +5 -1
- package/src/model/DeploymentElementModel.ts +534 -0
- package/src/model/DeploymentModel.ts +364 -0
- package/src/model/ElementModel.ts +195 -0
- package/src/model/LikeC4Model.test-d.ts +151 -0
- package/src/model/LikeC4Model.ts +280 -457
- package/src/model/RelationModel.ts +84 -0
- package/src/model/__test__/fixture.ts +153 -338
- package/src/model/connection/ConnectionModel.ts +94 -0
- package/src/model/connection/DeploymentConnectionModel.ts +99 -0
- package/src/model/connection/deployment.ts +124 -0
- package/src/model/connection/model.ts +123 -0
- package/src/model/index.ts +4 -2
- package/src/model/types.ts +95 -15
- package/src/model/view/EdgeModel.ts +103 -0
- package/src/model/view/LikeC4ViewModel.ts +220 -0
- package/src/model/view/NodeModel.ts +228 -0
- package/src/types/_common.ts +2 -0
- package/src/types/deployments.ts +237 -0
- package/src/types/element.ts +7 -3
- package/src/types/expression.ts +8 -6
- package/src/types/index.ts +1 -0
- package/src/types/model.ts +68 -9
- package/src/types/overview-graph.ts +2 -2
- package/src/types/relation.ts +4 -4
- package/src/types/view-notation.ts +2 -2
- package/src/types/view.ts +104 -30
- package/src/utils/{commonHead.ts → common-head.ts} +2 -0
- package/src/utils/fqn.ts +83 -27
- package/src/utils/getOrCreate.ts +42 -0
- package/src/utils/graphlib.ts +10 -0
- package/src/utils/index.ts +4 -1
- package/src/utils/object-hash.ts +3 -0
- package/src/utils/set.ts +76 -0
- package/src/utils/string-hash.ts +16 -0
- package/dist/index.cjs +0 -5452
- package/dist/index.d.cts +0 -1052
- package/dist/shared/core.CoLQkcYL.d.mts +0 -734
- package/dist/shared/core.v5DxGcX6.mjs +0 -293
- package/dist/shared/core.zN6ZscFQ.cjs +0 -353
- package/dist/types/index.cjs +0 -70
- package/dist/types/index.d.cts +0 -2
- package/src/model/LikeC4DiagramModel.ts +0 -338
- package/src/model/LikeC4ViewModel.ts +0 -320
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { first, last, values } from 'remeda'
|
|
2
|
+
import { isExtendsElementView, type LikeC4View } from '../../types/view'
|
|
3
|
+
import { findCycles, Graph, isAcyclic, postorder } from '../../utils/graphlib'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve rules of extended views
|
|
7
|
+
* (Removes invalid views)
|
|
8
|
+
*/
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
export function resolveRulesExtendedViews<V extends Record<any, LikeC4View>>(
|
|
11
|
+
unresolvedViews: V
|
|
12
|
+
): V {
|
|
13
|
+
const g = new Graph({
|
|
14
|
+
directed: true,
|
|
15
|
+
multigraph: false,
|
|
16
|
+
compound: false
|
|
17
|
+
})
|
|
18
|
+
for (const view of values(unresolvedViews)) {
|
|
19
|
+
g.setNode(view.id, view.id)
|
|
20
|
+
if (isExtendsElementView(view)) {
|
|
21
|
+
// view -> parent
|
|
22
|
+
g.setEdge(view.id, view.extends)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (g.edgeCount() === 0) {
|
|
26
|
+
return unresolvedViews
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Remove circular dependencies
|
|
30
|
+
while (!isAcyclic(g)) {
|
|
31
|
+
const firstCycle = first(findCycles(g))
|
|
32
|
+
if (!firstCycle) {
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
const cycledNode = last(firstCycle)
|
|
36
|
+
if (!cycledNode) {
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
g.removeNode(cycledNode)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ordered = postorder(g, g.sources() as unknown as string[])
|
|
43
|
+
|
|
44
|
+
return ordered.reduce((acc, id) => {
|
|
45
|
+
const view = unresolvedViews[id]
|
|
46
|
+
if (!view) {
|
|
47
|
+
return acc
|
|
48
|
+
}
|
|
49
|
+
if (isExtendsElementView(view)) {
|
|
50
|
+
const extendsFrom = acc[view.extends]
|
|
51
|
+
if (!extendsFrom) {
|
|
52
|
+
console.warn(`View "${view.id}" extends from "${view.extends}" which does not exist`)
|
|
53
|
+
return acc
|
|
54
|
+
}
|
|
55
|
+
return Object.assign(acc, {
|
|
56
|
+
[view.id]: {
|
|
57
|
+
...extendsFrom,
|
|
58
|
+
...view,
|
|
59
|
+
rules: [...extendsFrom.rules, ...view.rules]
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
return Object.assign(acc, { [view.id]: view })
|
|
64
|
+
}, {} as V)
|
|
65
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isNullish } from 'remeda'
|
|
2
|
+
import { nonexhaustive } from '../../errors'
|
|
3
|
+
import {
|
|
4
|
+
type DynamicView,
|
|
5
|
+
type DynamicViewRule,
|
|
6
|
+
type ElementView,
|
|
7
|
+
isDynamicView,
|
|
8
|
+
isElementView,
|
|
9
|
+
isViewRuleGlobalPredicateRef,
|
|
10
|
+
isViewRuleGlobalStyle,
|
|
11
|
+
type ModelGlobals,
|
|
12
|
+
type ViewRule,
|
|
13
|
+
type ViewRuleGlobalPredicateRef,
|
|
14
|
+
type ViewRuleGlobalStyle
|
|
15
|
+
} from '../../types'
|
|
16
|
+
|
|
17
|
+
export function resolveGlobalRules<V extends DynamicView | ElementView>(
|
|
18
|
+
view: V,
|
|
19
|
+
globals: ModelGlobals
|
|
20
|
+
): V {
|
|
21
|
+
if (isElementView(view)) {
|
|
22
|
+
return {
|
|
23
|
+
...view,
|
|
24
|
+
rules: resolveGlobalRulesInElementView(view, globals)
|
|
25
|
+
}
|
|
26
|
+
} else if (isDynamicView(view)) {
|
|
27
|
+
return {
|
|
28
|
+
...view,
|
|
29
|
+
rules: resolveGlobalRulesInDynamicView(view, globals)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
nonexhaustive(view)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type ViewRuleGlobal = ViewRuleGlobalPredicateRef | ViewRuleGlobalStyle
|
|
36
|
+
|
|
37
|
+
export function resolveGlobalRulesInElementView(
|
|
38
|
+
view: ElementView,
|
|
39
|
+
globals: ModelGlobals
|
|
40
|
+
): Array<Exclude<ViewRule, ViewRuleGlobal>> {
|
|
41
|
+
return view.rules.reduce((acc, rule) => {
|
|
42
|
+
if (isViewRuleGlobalPredicateRef(rule)) {
|
|
43
|
+
const globalPredicates = globals.predicates[rule.predicateId]
|
|
44
|
+
if (isNullish(globalPredicates)) {
|
|
45
|
+
return acc
|
|
46
|
+
}
|
|
47
|
+
return acc.concat(globalPredicates)
|
|
48
|
+
}
|
|
49
|
+
if (isViewRuleGlobalStyle(rule)) {
|
|
50
|
+
const globalStyles = globals.styles[rule.styleId]
|
|
51
|
+
if (isNullish(globalStyles)) {
|
|
52
|
+
return acc
|
|
53
|
+
}
|
|
54
|
+
return acc.concat(globalStyles)
|
|
55
|
+
}
|
|
56
|
+
acc.push(rule)
|
|
57
|
+
return acc
|
|
58
|
+
}, [] as Array<Exclude<ViewRule, ViewRuleGlobal>>)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveGlobalRulesInDynamicView(
|
|
62
|
+
view: DynamicView,
|
|
63
|
+
globals: ModelGlobals
|
|
64
|
+
): Array<Exclude<DynamicViewRule, ViewRuleGlobal>> {
|
|
65
|
+
return view.rules.reduce((acc, rule) => {
|
|
66
|
+
if (isViewRuleGlobalPredicateRef(rule)) {
|
|
67
|
+
const globalPredicates = globals.dynamicPredicates[rule.predicateId]
|
|
68
|
+
if (isNullish(globalPredicates)) {
|
|
69
|
+
return acc
|
|
70
|
+
}
|
|
71
|
+
return acc.concat(globalPredicates)
|
|
72
|
+
}
|
|
73
|
+
if (isViewRuleGlobalStyle(rule)) {
|
|
74
|
+
const globalStyles = globals.styles[rule.styleId]
|
|
75
|
+
if (isNullish(globalStyles)) {
|
|
76
|
+
return acc
|
|
77
|
+
}
|
|
78
|
+
return acc.concat(globalStyles)
|
|
79
|
+
}
|
|
80
|
+
acc.push(rule)
|
|
81
|
+
return acc
|
|
82
|
+
}, [] as Array<Exclude<DynamicViewRule, ViewRuleGlobal>>)
|
|
83
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { difference, filter, map, pipe, sort, tap } from 'remeda'
|
|
2
|
+
import { invariant, nonNullable } from '../../errors'
|
|
3
|
+
import type { Fqn } from '../../types/element'
|
|
4
|
+
import type { ComputedEdge, ComputedNode, EdgeId } from '../../types/view'
|
|
5
|
+
import { compareByFqnHierarchically } from '../../utils/fqn'
|
|
6
|
+
import { Graph, postorder } from '../../utils/graphlib'
|
|
7
|
+
import { compareRelations } from '../../utils/relations'
|
|
8
|
+
|
|
9
|
+
// side effect
|
|
10
|
+
function sortChildren(nodes: readonly ComputedNode[]) {
|
|
11
|
+
nodes.forEach(parent => {
|
|
12
|
+
if (parent.children.length > 0) {
|
|
13
|
+
parent.children = nodes.flatMap(n => (n.parent === parent.id ? n.id : []))
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function sortNodes({
|
|
19
|
+
nodes,
|
|
20
|
+
edges
|
|
21
|
+
}: {
|
|
22
|
+
nodes: ComputedNode[]
|
|
23
|
+
edges: ComputedEdge[]
|
|
24
|
+
}): ComputedNode[] {
|
|
25
|
+
if (nodes.length < 2) {
|
|
26
|
+
return nodes
|
|
27
|
+
}
|
|
28
|
+
if (edges.length === 0) {
|
|
29
|
+
return pipe(
|
|
30
|
+
nodes,
|
|
31
|
+
sort(compareByFqnHierarchically),
|
|
32
|
+
tap(sortChildren)
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const g = new Graph({
|
|
37
|
+
compound: false,
|
|
38
|
+
directed: true,
|
|
39
|
+
multigraph: false
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const getNode = (id: Fqn) =>
|
|
43
|
+
nonNullable(
|
|
44
|
+
nodes.find(n => n.id === id),
|
|
45
|
+
'Edge not found'
|
|
46
|
+
)
|
|
47
|
+
const getEdge = (id: EdgeId) =>
|
|
48
|
+
nonNullable(
|
|
49
|
+
edges.find(edge => edge.id === id),
|
|
50
|
+
'Edge not found'
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
for (const e of [...edges].sort(compareRelations)) {
|
|
54
|
+
g.setEdge(e.source, e.target)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const n of nodes) {
|
|
58
|
+
g.setNode(n.id, n.id)
|
|
59
|
+
if (n.children.length > 0) {
|
|
60
|
+
n.inEdges.forEach(e => {
|
|
61
|
+
const edge = getEdge(e)
|
|
62
|
+
// if this edge from leaf to the child of this node
|
|
63
|
+
if (edge.target !== n.id && getNode(edge.source).children.length === 0) {
|
|
64
|
+
// const id = `${edge.source}:${n.id}`
|
|
65
|
+
g.setEdge(edge.source, n.id)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
// n.outEdges.forEach(e => {
|
|
69
|
+
// const edge = getEdge(e)
|
|
70
|
+
// if (edge.source !== n.id) {
|
|
71
|
+
// const id = `${n.id}:${edge.target}`
|
|
72
|
+
// g.setEdge(n.id, edge.target, undefined, id)
|
|
73
|
+
// }
|
|
74
|
+
// })
|
|
75
|
+
}
|
|
76
|
+
if (n.parent) {
|
|
77
|
+
g.setEdge(n.parent, n.id)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let sources = g.sources() as unknown as Fqn[]
|
|
82
|
+
if (sources.length === 0) {
|
|
83
|
+
sources = pipe(
|
|
84
|
+
nodes,
|
|
85
|
+
filter(n => n.inEdges.length === 0 || n.parent === null),
|
|
86
|
+
sort(compareByFqnHierarchically),
|
|
87
|
+
map(n => n.id)
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
// sources = sources.sort(compareFqnHierarchically)
|
|
91
|
+
const orderedIds = postorder(g, sources).reverse() as Fqn[]
|
|
92
|
+
const sorted = orderedIds.map(getNode)
|
|
93
|
+
if (sorted.length < nodes.length) {
|
|
94
|
+
const unsorted = difference(nodes, sorted).sort(compareByFqnHierarchically)
|
|
95
|
+
sorted.push(...unsorted)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
invariant(sorted.length === nodes.length, 'Not all nodes were processed by graphlib')
|
|
99
|
+
sortChildren(sorted)
|
|
100
|
+
return sorted
|
|
101
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import Graph from 'graphology'
|
|
2
|
+
import { topologicalSort as topsort } from 'graphology-dag/topological-sort'
|
|
3
|
+
import willCreateCycle from 'graphology-dag/will-create-cycle'
|
|
4
|
+
import { forEach, map, pipe, prop, sortBy } from 'remeda'
|
|
5
|
+
import { invariant, nonNullable } from '../../errors'
|
|
6
|
+
import type { ComputedEdge, ComputedNode, NodeId } from '../../types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Keeps initial order of the elements, but ensures that parents are before children
|
|
10
|
+
*/
|
|
11
|
+
function ensureParentsFirst<T extends { id: string; parent: string | null }>(
|
|
12
|
+
array: ReadonlyArray<T>
|
|
13
|
+
): Array<T> {
|
|
14
|
+
const result = [] as T[]
|
|
15
|
+
const items = [...array]
|
|
16
|
+
let item
|
|
17
|
+
while ((item = items.shift())) {
|
|
18
|
+
let parent = item.parent
|
|
19
|
+
if (parent) {
|
|
20
|
+
const ancestors = [] as T[]
|
|
21
|
+
while (parent) {
|
|
22
|
+
const parentIndx = items.findIndex(i => i.id === parent)
|
|
23
|
+
if (parentIndx < 0) {
|
|
24
|
+
break
|
|
25
|
+
}
|
|
26
|
+
const [parentItem] = items.splice(parentIndx, 1)
|
|
27
|
+
if (!parentItem) {
|
|
28
|
+
throw new Error('Invalid state, should not happen')
|
|
29
|
+
}
|
|
30
|
+
ancestors.unshift(parentItem)
|
|
31
|
+
parent = parentItem.parent
|
|
32
|
+
}
|
|
33
|
+
result.push(...ancestors)
|
|
34
|
+
}
|
|
35
|
+
result.push(item)
|
|
36
|
+
}
|
|
37
|
+
return result as Array<T>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Side effect, mutates node.children field to preserve same order as in the input
|
|
42
|
+
*/
|
|
43
|
+
function updateChildren(nodes: ComputedNode[]) {
|
|
44
|
+
nodes.forEach(parent => {
|
|
45
|
+
if (parent.children.length > 0) {
|
|
46
|
+
parent.children = nodes.reduce((acc, n) => {
|
|
47
|
+
if (n.parent === parent.id) {
|
|
48
|
+
acc.push(n.id)
|
|
49
|
+
}
|
|
50
|
+
return acc
|
|
51
|
+
}, [] as NodeId[])
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
return nodes
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type TopologicalSortParam = {
|
|
58
|
+
nodes: Iterable<ComputedNode>
|
|
59
|
+
edges: Iterable<ComputedEdge>
|
|
60
|
+
}
|
|
61
|
+
export function topologicalSort(
|
|
62
|
+
param: TopologicalSortParam
|
|
63
|
+
): {
|
|
64
|
+
nodes: ComputedNode[]
|
|
65
|
+
edges: ComputedEdge[]
|
|
66
|
+
} {
|
|
67
|
+
let nodes = [...param.nodes]
|
|
68
|
+
let edges = [...param.edges]
|
|
69
|
+
if (nodes.length < 2) {
|
|
70
|
+
return {
|
|
71
|
+
nodes,
|
|
72
|
+
edges
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
nodes = ensureParentsFirst(nodes)
|
|
76
|
+
|
|
77
|
+
if (edges.length === 0) {
|
|
78
|
+
return {
|
|
79
|
+
nodes,
|
|
80
|
+
edges
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const getNode = (id: string) => nonNullable(nodes.find(n => n.id === id))
|
|
85
|
+
const nodeLevel = (id: string) => getNode(id).level + 1
|
|
86
|
+
const getEdge = (id: string) => nonNullable(edges.find(edge => edge.id === id))
|
|
87
|
+
|
|
88
|
+
const g = new Graph({
|
|
89
|
+
multi: true,
|
|
90
|
+
allowSelfLoops: true,
|
|
91
|
+
type: 'directed'
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
for (const n of nodes) {
|
|
95
|
+
g.addNode(n.id)
|
|
96
|
+
}
|
|
97
|
+
const sortedEdges = [] as ComputedEdge[]
|
|
98
|
+
|
|
99
|
+
pipe(
|
|
100
|
+
edges,
|
|
101
|
+
map(e => ({
|
|
102
|
+
e,
|
|
103
|
+
parentLevel: e.parent ? nodeLevel(e.parent) : 0,
|
|
104
|
+
sourceLevel: nodeLevel(e.source),
|
|
105
|
+
targetLevel: nodeLevel(e.target),
|
|
106
|
+
sourceIndex: nodes.findIndex(n => n.id === e.source),
|
|
107
|
+
targetIndex: nodes.findIndex(n => n.id === e.target)
|
|
108
|
+
})),
|
|
109
|
+
sortBy(
|
|
110
|
+
[prop('parentLevel'), 'desc'],
|
|
111
|
+
[prop('sourceLevel'), 'desc'],
|
|
112
|
+
[prop('targetLevel'), 'desc'],
|
|
113
|
+
[prop('sourceIndex'), 'asc'],
|
|
114
|
+
[prop('targetIndex'), 'asc']
|
|
115
|
+
),
|
|
116
|
+
forEach(({ e }, idx, _all) => {
|
|
117
|
+
sortedEdges.push(e)
|
|
118
|
+
if (idx === 0 || !willCreateCycle(g, e.source, e.target)) {
|
|
119
|
+
g.mergeDirectedEdge(e.source, e.target)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
// Strengthen the graph by adding edges to compound nodes
|
|
125
|
+
for (const n of nodes) {
|
|
126
|
+
if (n.children.length > 0) {
|
|
127
|
+
n.inEdges.forEach(e => {
|
|
128
|
+
const edge = getEdge(e)
|
|
129
|
+
// if this edge from leaf to the child of this node
|
|
130
|
+
if (edge.target !== n.id && getNode(edge.source).children.length === 0) {
|
|
131
|
+
if (!willCreateCycle(g, edge.source, n.id)) {
|
|
132
|
+
g.mergeDirectedEdge(edge.source, n.id)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
if (n.parent && !willCreateCycle(g, n.parent, n.id)) {
|
|
138
|
+
g.mergeDirectedEdge(n.parent, n.id)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const sortedIds = topsort(g)
|
|
143
|
+
const sorted = [] as ComputedNode[]
|
|
144
|
+
for (const sortedId of sortedIds) {
|
|
145
|
+
const indx = nodes.findIndex(n => n.id === sortedId)
|
|
146
|
+
invariant(indx >= 0, `Node "${sortedId}" not found`)
|
|
147
|
+
sorted.push(...nodes.splice(indx, 1))
|
|
148
|
+
}
|
|
149
|
+
if (nodes.length > 0) {
|
|
150
|
+
sorted.push(...nodes)
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
nodes: pipe(
|
|
154
|
+
sorted,
|
|
155
|
+
ensureParentsFirst,
|
|
156
|
+
updateChildren
|
|
157
|
+
),
|
|
158
|
+
edges: sortedEdges
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { uniqueTags } from './uniqueTags'
|
|
3
|
+
|
|
4
|
+
describe('uniqueTags function', () => {
|
|
5
|
+
it('returns unique tags from an array of elements', () => {
|
|
6
|
+
const input = [
|
|
7
|
+
{ tags: ['tag1', 'tag2', 'tag3'] },
|
|
8
|
+
{ tags: ['tag2', 'tag3', 'tag4'] },
|
|
9
|
+
{ tags: ['tag3', 'tag4', 'tag5'] }
|
|
10
|
+
] as const
|
|
11
|
+
const result = uniqueTags(input)
|
|
12
|
+
expect(result).toEqual(['tag1', 'tag2', 'tag3', 'tag4', 'tag5'])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should return unique tags naturally sorted', () => {
|
|
16
|
+
const input = [
|
|
17
|
+
{ tags: ['tag1', 'tag20', 'tag30'] },
|
|
18
|
+
{ tags: ['tag2', 'tag23', 'tag34'] },
|
|
19
|
+
{ tags: ['tag3'] }
|
|
20
|
+
] as const
|
|
21
|
+
const result = uniqueTags(input)
|
|
22
|
+
expect(result).toEqual([
|
|
23
|
+
'tag1',
|
|
24
|
+
'tag2',
|
|
25
|
+
'tag3',
|
|
26
|
+
'tag20',
|
|
27
|
+
'tag23',
|
|
28
|
+
'tag30',
|
|
29
|
+
'tag34'
|
|
30
|
+
])
|
|
31
|
+
// ].sort(compareNatural))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('returns null if the tags array is null', () => {
|
|
35
|
+
const input = [
|
|
36
|
+
{ tags: null },
|
|
37
|
+
{}
|
|
38
|
+
]
|
|
39
|
+
const result = uniqueTags(input)
|
|
40
|
+
expect(result).toBeNull()
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { flatMap, hasAtLeast, pipe, sort, unique } from 'remeda'
|
|
2
|
+
import type { LiteralUnion } from 'type-fest'
|
|
3
|
+
import type { NonEmptyReadonlyArray } from '../../types/_common'
|
|
4
|
+
import type { Tag } from '../../types/element'
|
|
5
|
+
import { compareNatural } from '../../utils/compare-natural'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extracts unique tags from an array of elements.
|
|
9
|
+
* and sort in natural order; returns null if no tags are present.
|
|
10
|
+
*/
|
|
11
|
+
export function uniqueTags<T extends { tags?: NonEmptyReadonlyArray<LiteralUnion<Tag, string>> | null }>(
|
|
12
|
+
elements: ReadonlyArray<T>
|
|
13
|
+
) {
|
|
14
|
+
const tags = pipe(
|
|
15
|
+
elements,
|
|
16
|
+
flatMap(e => e.tags ?? []),
|
|
17
|
+
unique(),
|
|
18
|
+
sort(compareNatural)
|
|
19
|
+
)
|
|
20
|
+
return hasAtLeast(tags, 1) ? tags as NonEmptyReadonlyArray<Tag> : null
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isTruthy, map, mapToObj, pick, pipe } from 'remeda'
|
|
2
|
+
import type { SetOptional } from 'type-fest'
|
|
3
|
+
import type { ComputedView } from '../../types'
|
|
4
|
+
import { objectHash } from '../../utils/object-hash'
|
|
5
|
+
|
|
6
|
+
export function calcViewLayoutHash<V extends ComputedView>(view: SetOptional<V, 'hash'>): V {
|
|
7
|
+
const tohash = {
|
|
8
|
+
id: view.id,
|
|
9
|
+
__: view.__ ?? 'element',
|
|
10
|
+
autoLayout: view.autoLayout,
|
|
11
|
+
nodes: pipe(
|
|
12
|
+
view.nodes,
|
|
13
|
+
map(pick(['id', 'title', 'description', 'technology', 'shape', 'icon', 'children'])),
|
|
14
|
+
mapToObj(({ id, icon, ...node }) => [id, { ...node, icon: isTruthy(icon) ? 'Y' : 'N' }])
|
|
15
|
+
),
|
|
16
|
+
edges: pipe(
|
|
17
|
+
view.edges,
|
|
18
|
+
map(pick(['source', 'target', 'label', 'description', 'technology', 'dir', 'head', 'tail', 'line'])),
|
|
19
|
+
mapToObj(({ source, target, ...edge }) => [`${source}:${target}`, edge])
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
view.hash = objectHash(tohash, {
|
|
23
|
+
ignoreUnknown: true,
|
|
24
|
+
respectType: false
|
|
25
|
+
})
|
|
26
|
+
return view as V
|
|
27
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { map, mapToObj, mapValues } from 'remeda'
|
|
2
|
+
import type { ComputedLikeC4Model } from '../../types'
|
|
3
|
+
import type { ComputedView, EdgeId } from '../../types/view'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert hashed edge ids to human-readable
|
|
7
|
+
* Mostly for testing purposes
|
|
8
|
+
*/
|
|
9
|
+
export function withReadableEdges<T extends ComputedView>({ edges, nodes, ...view }: T): T {
|
|
10
|
+
const edgeids = mapToObj(edges, e => [e.id, `${e.source}:${e.target}` as EdgeId])
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
...view,
|
|
14
|
+
edges: edges.map(e => ({
|
|
15
|
+
...e,
|
|
16
|
+
id: edgeids[e.id]!
|
|
17
|
+
})),
|
|
18
|
+
nodes: nodes.map(n => ({
|
|
19
|
+
...n,
|
|
20
|
+
inEdges: map(n.inEdges, e => edgeids[e]!),
|
|
21
|
+
outEdges: map(n.outEdges, e => edgeids[e]!)
|
|
22
|
+
}))
|
|
23
|
+
} as T
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function viewsWithReadableEdges<M extends ComputedLikeC4Model>({ views, ...model }: M): M {
|
|
27
|
+
return {
|
|
28
|
+
...model,
|
|
29
|
+
views: mapValues(views, withReadableEdges)
|
|
30
|
+
} as M
|
|
31
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,11 +6,15 @@ export * from './errors'
|
|
|
6
6
|
export * from './model'
|
|
7
7
|
|
|
8
8
|
export * from './types'
|
|
9
|
-
export * from './utils/
|
|
9
|
+
export * from './utils/common-head'
|
|
10
10
|
export * from './utils/compare-natural'
|
|
11
11
|
export * from './utils/fqn'
|
|
12
|
+
export * from './utils/getOrCreate'
|
|
12
13
|
export * from './utils/guards'
|
|
14
|
+
export * from './utils/object-hash'
|
|
13
15
|
export * from './utils/promises'
|
|
14
16
|
export { compareRelations } from './utils/relations'
|
|
17
|
+
export * from './utils/set'
|
|
18
|
+
export * from './utils/string-hash'
|
|
15
19
|
|
|
16
20
|
export * as Expr from './types/expression'
|