@likec4/core 1.17.1 → 1.19.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 (184) hide show
  1. package/LICENSE +1 -1
  2. package/dist/builder/index.d.mts +502 -0
  3. package/dist/builder/index.d.ts +502 -0
  4. package/dist/builder/index.js +871 -0
  5. package/dist/compute-view/index.d.mts +22 -0
  6. package/dist/compute-view/index.d.ts +22 -0
  7. package/dist/compute-view/index.js +78 -0
  8. package/dist/index.d.mts +10 -919
  9. package/dist/index.d.ts +10 -919
  10. package/dist/{index.mjs → index.js} +7 -1688
  11. package/dist/model/index.d.mts +254 -0
  12. package/dist/model/index.d.ts +254 -0
  13. package/dist/model/index.js +1 -0
  14. package/dist/shared/chunk-RAAYCPUM.M-JWF7SS.js +21 -0
  15. package/dist/shared/core.BIfgabEw.d.ts +719 -0
  16. package/dist/shared/core.BZ9Msq7w.d.mts +719 -0
  17. package/dist/shared/{core.CoLQkcYL.d.cts → core.C05RDLhi.d.ts} +460 -362
  18. package/dist/shared/{core.CoLQkcYL.d.mts → core.CmYMqgha.d.mts} +460 -362
  19. package/dist/shared/core.D6-fWbM3.d.ts +127 -0
  20. package/dist/shared/core.D74xkKkG.d.mts +148 -0
  21. package/dist/shared/core.D74xkKkG.d.ts +148 -0
  22. package/dist/shared/core.sLaWHBjR.d.mts +127 -0
  23. package/dist/shared/index.-BdzdI2C.js +7443 -0
  24. package/dist/shared/object_hash.CE_oF3I3.js +1222 -0
  25. package/dist/shared/{core.BXTs-8n7.mjs → view.CyZrG8BJ.js} +108 -47
  26. package/dist/types/index.d.mts +71 -1
  27. package/dist/types/index.d.ts +71 -1
  28. package/dist/types/index.js +20 -0
  29. package/dist/utils/index.d.mts +404 -0
  30. package/dist/utils/index.d.ts +404 -0
  31. package/dist/utils/index.js +330 -0
  32. package/package.json +59 -47
  33. package/src/builder/Builder-style1.test-d.ts +190 -0
  34. package/src/builder/Builder-style2.test-d.ts +175 -0
  35. package/src/builder/Builder.deployment.ts +224 -0
  36. package/src/builder/Builder.deploymentModel.ts +385 -0
  37. package/src/builder/Builder.element.ts +19 -19
  38. package/src/builder/Builder.model.ts +67 -42
  39. package/src/builder/Builder.ts +533 -367
  40. package/src/builder/Builder.view-common.ts +234 -0
  41. package/src/builder/Builder.view-deployment.ts +135 -0
  42. package/src/builder/Builder.view-element.ts +189 -0
  43. package/src/builder/Builder.views.ts +109 -33
  44. package/src/builder/Builder.with.ts +475 -0
  45. package/src/builder/__snapshots__/Builder-style1.spec.ts.snap +374 -0
  46. package/src/builder/__snapshots__/Builder-style2.spec.ts.snap +183 -0
  47. package/src/builder/__snapshots__/Builder.spec.ts.snap +374 -0
  48. package/src/builder/_types.ts +113 -18
  49. package/src/builder/index.ts +10 -4
  50. package/src/colors/index.ts +2 -2
  51. package/src/compute-view/compute-view.ts +83 -0
  52. package/src/compute-view/deployment-view/__test__/TestHelper.ts +236 -0
  53. package/src/compute-view/deployment-view/__test__/fixture.ts +198 -0
  54. package/src/compute-view/deployment-view/_types.ts +37 -0
  55. package/src/compute-view/deployment-view/clean-connections.ts +238 -0
  56. package/src/compute-view/deployment-view/compute.ts +125 -0
  57. package/src/compute-view/deployment-view/memory/index.ts +9 -0
  58. package/src/compute-view/deployment-view/memory/memory.ts +97 -0
  59. package/src/compute-view/deployment-view/predicates/elements.ts +144 -0
  60. package/src/compute-view/deployment-view/predicates/relation-direct.ts +237 -0
  61. package/src/compute-view/deployment-view/predicates/relation-in-out.ts +61 -0
  62. package/src/compute-view/deployment-view/predicates/relation-incoming.ts +95 -0
  63. package/src/compute-view/deployment-view/predicates/relation-outgoing.ts +88 -0
  64. package/src/compute-view/deployment-view/predicates/wildcard.ts +29 -0
  65. package/src/compute-view/deployment-view/stages/stage-exclude.ts +5 -0
  66. package/src/compute-view/deployment-view/stages/stage-final.ts +245 -0
  67. package/src/compute-view/deployment-view/stages/stage-include.ts +95 -0
  68. package/src/compute-view/deployment-view/utils.ts +276 -0
  69. package/src/compute-view/dynamic-view/__test__/fixture.ts +58 -0
  70. package/src/compute-view/dynamic-view/compute.ts +324 -0
  71. package/src/compute-view/element-view/__test__/TestHelper.ts +258 -0
  72. package/src/compute-view/element-view/__test__/__snapshots__/legacy.spec.ts.snap +458 -0
  73. package/src/compute-view/element-view/__test__/fixture.ts +675 -0
  74. package/src/compute-view/element-view/_types.ts +48 -0
  75. package/src/compute-view/element-view/clean-connections.ts +113 -0
  76. package/src/compute-view/element-view/compute.ts +315 -0
  77. package/src/compute-view/element-view/memory/NodeGroup.ts +31 -0
  78. package/src/compute-view/element-view/memory/index.ts +10 -0
  79. package/src/compute-view/element-view/memory/memory.ts +177 -0
  80. package/src/compute-view/element-view/memory/stage-exclude.ts +59 -0
  81. package/src/compute-view/element-view/memory/stage-final.ts +129 -0
  82. package/src/compute-view/element-view/memory/stage-include.ts +128 -0
  83. package/src/compute-view/element-view/predicates/_utils.ts +86 -0
  84. package/src/compute-view/element-view/predicates/element-expand.ts +33 -0
  85. package/src/compute-view/element-view/predicates/element-kind-tag.ts +26 -0
  86. package/src/compute-view/element-view/predicates/element-ref.ts +37 -0
  87. package/src/compute-view/element-view/predicates/relation-direct.ts +309 -0
  88. package/src/compute-view/element-view/predicates/relation-in-out.ts +66 -0
  89. package/src/compute-view/element-view/predicates/relation-in.ts +118 -0
  90. package/src/compute-view/element-view/predicates/relation-out.ts +116 -0
  91. package/src/compute-view/element-view/predicates/wildcard.ts +88 -0
  92. package/src/compute-view/element-view/utils.ts +59 -0
  93. package/src/compute-view/index.ts +4 -0
  94. package/src/compute-view/memory/AbstractMemory.ts +45 -0
  95. package/src/compute-view/memory/AbstractStageExclude.ts +216 -0
  96. package/src/compute-view/memory/AbstractStageInclude.ts +200 -0
  97. package/src/compute-view/memory/_types.ts +147 -0
  98. package/src/compute-view/memory/index.ts +5 -0
  99. package/src/compute-view/memory/ops.ts +43 -0
  100. package/src/compute-view/utils/__snapshots__/sortNodes.spec.ts.snap +113 -0
  101. package/src/compute-view/utils/ancestorsOfNode.ts +19 -0
  102. package/src/compute-view/utils/applyCustomElementProperties.ts +66 -0
  103. package/src/compute-view/utils/applyCustomRelationProperties.ts +41 -0
  104. package/src/compute-view/utils/applyViewRuleStyles.ts +53 -0
  105. package/src/compute-view/utils/buildComputedNodes.ts +148 -0
  106. package/src/compute-view/utils/buildElementNotations.ts +63 -0
  107. package/src/compute-view/utils/elementExpressionToPredicate.ts +50 -0
  108. package/src/compute-view/utils/link-nodes-with-edges.ts +54 -0
  109. package/src/compute-view/utils/merge-props-from-relationships.ts +128 -0
  110. package/src/compute-view/utils/relationExpressionToPredicates.ts +55 -0
  111. package/src/compute-view/utils/resolve-extended-views.ts +69 -0
  112. package/src/compute-view/utils/resolve-global-rules.ts +83 -0
  113. package/src/compute-view/utils/topological-sort.ts +256 -0
  114. package/src/compute-view/utils/uniqueTags.test.ts +42 -0
  115. package/src/compute-view/utils/uniqueTags.ts +21 -0
  116. package/src/compute-view/utils/view-hash.ts +27 -0
  117. package/src/compute-view/utils/with-readable-edges.ts +36 -0
  118. package/src/index.ts +22 -12
  119. package/src/model/DeploymentElementModel.ts +599 -0
  120. package/src/model/DeploymentModel.ts +370 -0
  121. package/src/model/ElementModel.ts +221 -0
  122. package/src/model/LikeC4Model.test-d.ts +152 -0
  123. package/src/model/LikeC4Model.ts +336 -457
  124. package/src/model/RelationModel.ts +85 -0
  125. package/src/model/__test__/fixture.ts +149 -338
  126. package/src/model/connection/Connection.ts +72 -0
  127. package/src/model/connection/deployment/DeploymentConnectionModel.ts +178 -0
  128. package/src/model/connection/deployment/find.ts +120 -0
  129. package/src/model/connection/deployment/index.ts +8 -0
  130. package/src/model/connection/index.ts +22 -0
  131. package/src/model/connection/model/ConnectionModel.ts +157 -0
  132. package/src/model/connection/model/find.ts +117 -0
  133. package/src/model/connection/model/index.ts +8 -0
  134. package/src/model/connection/ops.ts +274 -0
  135. package/src/model/guards.ts +9 -0
  136. package/src/model/index.ts +46 -2
  137. package/src/model/types.ts +112 -15
  138. package/src/model/view/EdgeModel.ts +103 -0
  139. package/src/model/view/LikeC4ViewModel.ts +226 -0
  140. package/src/model/view/NodeModel.ts +229 -0
  141. package/src/types/_common.ts +55 -1
  142. package/src/types/deployments.ts +169 -0
  143. package/src/types/element.ts +9 -5
  144. package/src/types/expression-v2.ts +172 -0
  145. package/src/types/expression.ts +17 -11
  146. package/src/types/global.ts +3 -3
  147. package/src/types/index.ts +220 -12
  148. package/src/types/model.ts +75 -12
  149. package/src/types/operators.ts +1 -1
  150. package/src/types/overview-graph.ts +2 -2
  151. package/src/types/relation.ts +18 -14
  152. package/src/types/theme.ts +0 -1
  153. package/src/types/view-notation.ts +2 -2
  154. package/src/types/view.ts +117 -41
  155. package/src/utils/{commonHead.ts → common-head.ts} +2 -0
  156. package/src/utils/const.ts +1 -0
  157. package/src/utils/fqn.ts +203 -56
  158. package/src/utils/getOrCreate.ts +42 -0
  159. package/src/utils/graphlib.ts +10 -0
  160. package/src/utils/index.ts +74 -6
  161. package/src/utils/iterable/_types.ts +5 -0
  162. package/src/utils/iterable/filter.ts +55 -0
  163. package/src/utils/iterable/flat.ts +17 -0
  164. package/src/utils/iterable/index.ts +12 -0
  165. package/src/utils/iterable/map.ts +40 -0
  166. package/src/utils/iterable/reduce.ts +24 -0
  167. package/src/utils/iterable/some.ts +36 -0
  168. package/src/utils/iterable/to.ts +19 -0
  169. package/src/utils/iterable/unique.ts +29 -0
  170. package/src/utils/object-hash.ts +3 -0
  171. package/src/utils/relations.ts +3 -64
  172. package/src/utils/set.ts +78 -0
  173. package/src/utils/string-hash.ts +16 -0
  174. package/dist/index.cjs +0 -5452
  175. package/dist/index.d.cts +0 -1053
  176. package/dist/shared/core.CoLQkcYL.d.ts +0 -734
  177. package/dist/shared/core.DIYZvpVg.cjs +0 -353
  178. package/dist/types/index.cjs +0 -70
  179. package/dist/types/index.d.cts +0 -2
  180. package/dist/types/index.mjs +0 -1
  181. package/src/builder/Builder.test-d.ts +0 -69
  182. package/src/builder/Builder.view.ts +0 -358
  183. package/src/model/LikeC4DiagramModel.ts +0 -338
  184. package/src/model/LikeC4ViewModel.ts +0 -320
@@ -0,0 +1,128 @@
1
+ import { isTruthy, only, pickBy, pipe, reduce, unique } from 'remeda'
2
+ import type { Color, DeploymentRelation, Link, Tag, ViewId } from '../../types'
3
+ import type { ModelRelation, RelationshipArrowType, RelationshipKind, RelationshipLineType } from '../../types/relation'
4
+ import { isNonEmptyArray } from '../../utils'
5
+
6
+ function pickRelationshipProps(relation: ModelRelation | DeploymentRelation) {
7
+ const {
8
+ title,
9
+ description = null,
10
+ technology = null,
11
+ kind = null,
12
+ color = null,
13
+ line = null,
14
+ head = null,
15
+ tail = null,
16
+ navigateTo = null
17
+ } = relation
18
+ return {
19
+ // Pick description only if title is present
20
+ ...(title && {
21
+ title,
22
+ description
23
+ }),
24
+ technology,
25
+ kind,
26
+ color,
27
+ line,
28
+ head,
29
+ tail,
30
+ navigateTo
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Merges properties from multiple relationships into a single object.
36
+ * @param relations - The relationships to merge.
37
+ * @param prefer - The relationship to prefer when merging.
38
+ */
39
+ export function mergePropsFromRelationships(
40
+ relations: Array<ModelRelation | DeploymentRelation>,
41
+ prefer?: ModelRelation | DeploymentRelation
42
+ ) {
43
+ const allprops = pipe(
44
+ relations,
45
+ reduce(
46
+ (acc, r) => {
47
+ if (isTruthy(r.title) && !acc.title.includes(r.title)) {
48
+ acc.title.push(r.title)
49
+ }
50
+ if (isTruthy(r.description) && !acc.description.includes(r.description)) {
51
+ acc.description.push(r.description)
52
+ }
53
+ if (isTruthy(r.technology) && !acc.technology.includes(r.technology)) {
54
+ acc.technology.push(r.technology)
55
+ }
56
+ if (isTruthy(r.kind) && !acc.kind.includes(r.kind)) {
57
+ acc.kind.push(r.kind)
58
+ }
59
+ if (isTruthy(r.color) && !acc.color.includes(r.color)) {
60
+ acc.color.push(r.color)
61
+ }
62
+ if (isTruthy(r.line) && !acc.line.includes(r.line)) {
63
+ acc.line.push(r.line)
64
+ }
65
+ if (isTruthy(r.head) && !acc.head.includes(r.head)) {
66
+ acc.head.push(r.head)
67
+ }
68
+ if (isTruthy(r.tail) && !acc.tail.includes(r.tail)) {
69
+ acc.tail.push(r.tail)
70
+ }
71
+ if (isTruthy(r.navigateTo) && !acc.navigateTo.includes(r.navigateTo)) {
72
+ acc.navigateTo.push(r.navigateTo)
73
+ }
74
+ if (r.tags) {
75
+ acc.tags.push(...r.tags)
76
+ }
77
+ if (r.links) {
78
+ acc.links.push(...r.links)
79
+ }
80
+ return acc
81
+ },
82
+ {
83
+ title: [] as string[],
84
+ description: [] as string[],
85
+ technology: [] as string[],
86
+ kind: [] as RelationshipKind[],
87
+ head: [] as RelationshipArrowType[],
88
+ tail: [] as RelationshipArrowType[],
89
+ color: [] as Color[],
90
+ tags: [] as Tag[],
91
+ links: [] as Link[],
92
+ line: [] as RelationshipLineType[],
93
+ navigateTo: [] as ViewId[]
94
+ }
95
+ )
96
+ )
97
+
98
+ let technology = only(allprops.technology)
99
+ let title = only(allprops.title) ?? (allprops.title.length > 1 ? '[...]' : null)
100
+
101
+ const tags = unique(allprops.tags)
102
+ let merged = pickBy(
103
+ {
104
+ // If there is no title, but there is technology, use technology as title
105
+ title: title ?? (technology ? `[${technology}]` : null),
106
+ description: only(allprops.description),
107
+ technology,
108
+ kind: only(allprops.kind),
109
+ head: only(allprops.head),
110
+ tail: only(allprops.tail),
111
+ color: only(allprops.color),
112
+ line: only(allprops.line),
113
+ navigateTo: only(allprops.navigateTo),
114
+ ...isNonEmptyArray(allprops.links) && { links: allprops.links },
115
+ ...isNonEmptyArray(tags) && { tags }
116
+ },
117
+ isTruthy
118
+ )
119
+
120
+ if (prefer) {
121
+ return {
122
+ ...merged,
123
+ ...pickBy(pickRelationshipProps(prefer), isTruthy)
124
+ }
125
+ }
126
+ return merged
127
+ }
128
+ export type MergedRelationshipProps = ReturnType<typeof mergePropsFromRelationships>
@@ -0,0 +1,55 @@
1
+ import { nonexhaustive } from '../../errors'
2
+ import type { Element } from '../../types/element'
3
+ import {
4
+ isIncoming,
5
+ isInOut,
6
+ isOutgoing,
7
+ isRelation,
8
+ isRelationWhere,
9
+ type RelationExpression,
10
+ type RelationWhereExpr
11
+ } from '../../types/expression'
12
+ import { whereOperatorAsPredicate } from '../../types/operators'
13
+ import type { ModelRelation } from '../../types/relation'
14
+ import type { ComputedNode } from '../../types/view'
15
+ import { elementExprToPredicate } from './elementExpressionToPredicate'
16
+
17
+ type Predicate<T> = (x: T) => boolean
18
+ export type FilterableEdge = Pick<ModelRelation, 'kind' | 'tags'> & {
19
+ source: ComputedNode
20
+ target: ComputedNode
21
+ }
22
+
23
+ export function relationExpressionToPredicates<T extends FilterableEdge>(
24
+ expr: RelationExpression | RelationWhereExpr
25
+ ): Predicate<T> {
26
+ switch (true) {
27
+ case isRelationWhere(expr): {
28
+ const predicate = relationExpressionToPredicates(expr.where.expr)
29
+ const where = whereOperatorAsPredicate(expr.where.condition)
30
+ return e => predicate(e) && where(e)
31
+ }
32
+ case isRelation(expr): {
33
+ const isSource = elementExprToPredicate(expr.source)
34
+ const isTarget = elementExprToPredicate(expr.target)
35
+ return edge => {
36
+ return (isSource(edge.source) && isTarget(edge.target))
37
+ || (!!expr.isBidirectional && isSource(edge.target) && isTarget(edge.source))
38
+ }
39
+ }
40
+ case isInOut(expr): {
41
+ const isInOut = elementExprToPredicate(expr.inout)
42
+ return edge => isInOut(edge.source) || isInOut(edge.target)
43
+ }
44
+ case isIncoming(expr): {
45
+ const isTarget = elementExprToPredicate(expr.incoming)
46
+ return edge => isTarget(edge.target)
47
+ }
48
+ case isOutgoing(expr): {
49
+ const isSource = elementExprToPredicate(expr.outgoing)
50
+ return edge => isSource(edge.source)
51
+ }
52
+ default:
53
+ nonexhaustive(expr)
54
+ }
55
+ }
@@ -0,0 +1,69 @@
1
+ import { values } from 'remeda'
2
+ import { type ExtendsElementView, isElementView, isExtendsElementView, type LikeC4View } from '../../types/view'
3
+
4
+ import Graph from 'graphology'
5
+ import { topologicalSort } from 'graphology-dag/topological-sort'
6
+ import willCreateCycle from 'graphology-dag/will-create-cycle'
7
+ /**
8
+ * Resolve rules of extended views
9
+ * (Removes invalid views)
10
+ */
11
+ export function resolveRulesExtendedViews<V extends Record<any, LikeC4View>>(
12
+ unresolvedViews: V
13
+ ): V {
14
+ const g = new Graph<{ view: LikeC4View }>({
15
+ type: 'directed'
16
+ })
17
+ const extendedViews = [] as ExtendsElementView<any, any>[]
18
+ for (const view of values(unresolvedViews)) {
19
+ g.addNode(view.id, { view })
20
+ if (isExtendsElementView(view)) {
21
+ extendedViews.push(view)
22
+ }
23
+ }
24
+
25
+ if (extendedViews.length === 0) {
26
+ return unresolvedViews
27
+ }
28
+
29
+ for (const view of extendedViews) {
30
+ if (!g.hasNode(view.extends)) {
31
+ console.warn(`View "${view.id}" extends from "${view.extends}" which does not exist`)
32
+ continue
33
+ }
34
+ if (willCreateCycle(g, view.id, view.extends)) {
35
+ console.warn(`View "${view.id}" extends from "${view.extends}" which creates a cycle`)
36
+ continue
37
+ }
38
+
39
+ // view -> parent
40
+ g.addDirectedEdge(view.id, view.extends)
41
+ }
42
+
43
+ const sorted = topologicalSort(g).reverse()
44
+ return sorted.reduce((acc, id) => {
45
+ const view = g.getNodeAttribute(id, 'view')
46
+ if (!isExtendsElementView(view)) {
47
+ acc[view.id] = view
48
+ return acc
49
+ }
50
+
51
+ const extendsFrom = acc[view.extends]
52
+ if (!extendsFrom || !isElementView(extendsFrom)) {
53
+ return acc
54
+ }
55
+
56
+ acc[view.id] = {
57
+ ...extendsFrom,
58
+ ...view,
59
+ rules: [...extendsFrom.rules, ...view.rules]
60
+ }
61
+ return acc
62
+ }, {} as Record<string, LikeC4View>) as V
63
+
64
+ // forEachNodeInTopologicalOrder(g, (_id, { view }) => {
65
+
66
+ // })
67
+
68
+ // return ordered as V
69
+ }
@@ -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.rules, globals)
25
+ }
26
+ } else if (isDynamicView(view)) {
27
+ return {
28
+ ...view,
29
+ rules: resolveGlobalRulesInDynamicView(view.rules, globals)
30
+ }
31
+ }
32
+ nonexhaustive(view)
33
+ }
34
+
35
+ type ViewRuleGlobal = ViewRuleGlobalPredicateRef | ViewRuleGlobalStyle
36
+
37
+ export function resolveGlobalRulesInElementView(
38
+ rules: ViewRule[],
39
+ globals: ModelGlobals
40
+ ): Array<Exclude<ViewRule, ViewRuleGlobal>> {
41
+ return 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
+ rules: DynamicViewRule[],
63
+ globals: ModelGlobals
64
+ ): Array<Exclude<DynamicViewRule, ViewRuleGlobal>> {
65
+ return 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,256 @@
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, partition, pipe, takeWhile } from 'remeda'
5
+ import { invariant, nonNullable } from '../../errors'
6
+ import type { ComputedEdge, ComputedNode, Fqn, NodeId } from '../../types'
7
+ import { ancestorsOfNode } from './ancestorsOfNode'
8
+
9
+ /**
10
+ * Keeps initial order of the elements, but ensures parents are placed before children
11
+ */
12
+ function ensureParentsFirst<T extends { id: string; parent: string | null }>(
13
+ array: ReadonlyArray<T>,
14
+ ): Array<T> {
15
+ const result = [] as T[]
16
+ const items = [...array]
17
+ let item
18
+ while ((item = items.shift())) {
19
+ let parent = item.parent
20
+ if (parent) {
21
+ const ancestors = [] as T[]
22
+ while (parent) {
23
+ const parentIndx = items.findIndex(i => i.id === parent)
24
+ if (parentIndx < 0) {
25
+ break
26
+ }
27
+ const [parentItem] = items.splice(parentIndx, 1)
28
+ if (!parentItem) {
29
+ throw new Error('Invalid state, should not happen')
30
+ }
31
+ ancestors.unshift(parentItem)
32
+ parent = parentItem.parent
33
+ }
34
+ result.push(...ancestors)
35
+ }
36
+ result.push(item)
37
+ }
38
+ return result as Array<T>
39
+ }
40
+
41
+ /**
42
+ * Side effect, mutates node.children field to preserve same order as in the input
43
+ */
44
+ function updateChildren(nodes: ComputedNode[]) {
45
+ nodes.forEach(parent => {
46
+ if (parent.children.length > 0) {
47
+ parent.children = nodes.reduce((acc, n) => {
48
+ if (n.parent === parent.id) {
49
+ acc.push(n.id)
50
+ }
51
+ return acc
52
+ }, [] as NodeId[])
53
+ }
54
+ })
55
+ return nodes
56
+ }
57
+
58
+ type TopologicalSortParam = {
59
+ nodes: ReadonlyMap<Fqn, ComputedNode>
60
+ edges: Iterable<ComputedEdge>
61
+ }
62
+ export function topologicalSort(
63
+ param: TopologicalSortParam,
64
+ ): {
65
+ nodes: ComputedNode[]
66
+ edges: ComputedEdge[]
67
+ } {
68
+ let nodes = ensureParentsFirst([...param.nodes.values()])
69
+ let edges = [...param.edges]
70
+ if (nodes.length < 2 || edges.length === 0) {
71
+ return {
72
+ nodes,
73
+ edges,
74
+ }
75
+ }
76
+
77
+ const getNode = (id: Fqn) => nonNullable(param.nodes.get(id))
78
+ // const nodeLevel = (id: string) => getNode(id).level + 1
79
+ //
80
+
81
+ const g = new Graph({
82
+ multi: true,
83
+ allowSelfLoops: true,
84
+ type: 'directed',
85
+ })
86
+
87
+ const enrichedEdges = pipe(
88
+ edges,
89
+ map((edge, __dirname) => {
90
+ const source = getNode(edge.source),
91
+ target = getNode(edge.target),
92
+ parent = edge.parent ? getNode(edge.parent) : null
93
+ return ({
94
+ id: edge.id,
95
+ edge,
96
+ parent,
97
+ source,
98
+ target,
99
+ })
100
+ }),
101
+ // sortBy(
102
+ // [prop('sourceIndex'), 'asc'],
103
+ // // [prop('sourceInCount'), 'asc'],
104
+ // // [prop('sourceLevel'), 'asc'],
105
+ // ),
106
+ )
107
+
108
+ const [edgesBetweenLeafs, edgesWithCompounds] = partition(
109
+ enrichedEdges,
110
+ ({ source, target }) => source.children.length === 0 && target.children.length === 0,
111
+ )
112
+
113
+ // edgesBetweenLeafs.sort((a, b) => {
114
+ // if (a.source === b.source) {
115
+ // return 0
116
+ // }
117
+ // if (a.source.level === 0 && b.source.level === 0) {
118
+ // return 0
119
+ // }
120
+ // if (isSameHierarchy(a.source, b.target) || isSameHierarchy(a.target, b.source)) {
121
+ // return edges.indexOf(a.edge) - edges.indexOf(b.edge)
122
+ // }
123
+ // const aLevel = a.parent ? a.parent.level + 1 - a.source.level : a.source.level
124
+ // const bLevel = b.parent ? b.parent.level + 1 - b.source.level : b.source.level
125
+ // return aLevel - bLevel
126
+ // // if (a.source.level === 0 && b.source.level === 0) {
127
+
128
+ // // // if (a.source.depth)
129
+ // // return nodes.indexOf(a.target) - nodes.indexOf(b.target)
130
+ // // }
131
+ // // if (a.source.level === 0 && b.source.level > 0) {
132
+ // // return -1
133
+ // // }
134
+ // // if (a.parent && b.parent && a.parent !== b.parent) {
135
+ // // if (a.parent.level !== b.parent.level) {
136
+ // // return b.parent.level - a.parent.level
137
+ // // } else {
138
+ // // return nodes.indexOf(a.parent) - nodes.indexOf(b.parent)
139
+ // // }
140
+ // // }
141
+ // // return Math.min(nodes.indexOf(a.source), nodes.indexOf(a.target)) - Math.min(nodes.indexOf(b.source), nodes.indexOf(b.target))
142
+ // })
143
+ const sortedEdges = [] as ComputedEdge[]
144
+
145
+ const addEdgeToGraph = (edge: ComputedEdge) => {
146
+ g.mergeNode(edge.source)
147
+ g.mergeNode(edge.target)
148
+ sortedEdges.push(edge)
149
+ if (!willCreateCycle(g, edge.source, edge.target)) {
150
+ g.mergeDirectedEdge(edge.source, edge.target)
151
+ }
152
+ }
153
+
154
+ for (const { edge, source, target } of edgesBetweenLeafs) {
155
+ addEdgeToGraph(edge)
156
+ // Strengthen the graph by adding edges to parents
157
+ if (target.parent && target.parent !== edge.parent) {
158
+ pipe(
159
+ ancestorsOfNode(target, param.nodes),
160
+ takeWhile(ancestor => ancestor.inEdges.includes(edge.id)),
161
+ forEach(ancestor => {
162
+ g.mergeNode(ancestor.id)
163
+ if (!willCreateCycle(g, edge.source, ancestor.id)) {
164
+ g.mergeDirectedEdge(edge.source, ancestor.id)
165
+ }
166
+ if (!willCreateCycle(g, ancestor.id, edge.target)) {
167
+ g.mergeDirectedEdge(ancestor.id, edge.target)
168
+ }
169
+ }),
170
+ )
171
+ }
172
+ if (source.parent) {
173
+ const sourceParent = getNode(source.parent)
174
+ g.mergeNode(sourceParent.id)
175
+ if (!willCreateCycle(g, sourceParent.id, source.id)) {
176
+ g.mergeDirectedEdge(sourceParent.id, source.id)
177
+ }
178
+ if (target.parent && target.parent !== source.parent) {
179
+ if (!willCreateCycle(g, sourceParent.id, target.parent)) {
180
+ g.mergeDirectedEdge(sourceParent.id, target.parent)
181
+ }
182
+ }
183
+ }
184
+
185
+ // addEdgeToGraph(edge)
186
+ }
187
+ for (const { edge } of edgesWithCompounds) {
188
+ addEdgeToGraph(edge)
189
+ }
190
+
191
+ invariant(sortedEdges.length === edges.length, 'Not all edges were added to the graph')
192
+
193
+ // for (const compound of nodes) {
194
+ // if (compound.children.length === 0 || compound.inEdges.length === 0) {
195
+ // continue
196
+ // }
197
+ // // g.mergeNode(compound.id)
198
+ // for (const inEdge of compound.inEdges) {
199
+ // const { edge, source } = getEdge(inEdge)
200
+ // // ignore if this edge is coming from compound node
201
+ // if (source.children.length > 0) {
202
+ // continue
203
+ // }
204
+ // // if this edge is coming to the compound node directly
205
+ // if (edge.target === compound.id) {
206
+ // // for (const child of compound.children) {
207
+ // // // g.mergeNode(child)
208
+ // // if (!willCreateCycle(g, edge.source, child)) {
209
+ // // g.mergeDirectedEdge(edge.source, child)
210
+ // // }
211
+ // // }
212
+ // } else {
213
+ // if (!willCreateCycle(g, edge.source, compound.id)) {
214
+ // g.mergeDirectedEdge(edge.source, compound.id)
215
+ // }
216
+ // if (!willCreateCycle(g, compound.id, edge.target)) {
217
+ // g.mergeDirectedEdge(compound.id, edge.target)
218
+ // }
219
+ // }
220
+ // }
221
+ // }
222
+
223
+ const sortedIds = topsort(g)
224
+ let sorted = [] as ComputedNode[]
225
+ let unsorted = nodes.slice()
226
+ for (const sortedId of sortedIds) {
227
+ const indx = unsorted.findIndex(n => n.id === sortedId)
228
+ invariant(indx >= 0, `Node "${sortedId}" not found`)
229
+ sorted.push(...unsorted.splice(indx, 1))
230
+ }
231
+ // Merge unsorted nodes, keeping their initial order
232
+ if (unsorted.length > 0 && sorted.length > 0) {
233
+ sorted = sorted.flatMap(node => {
234
+ if (unsorted.length === 0) {
235
+ return node
236
+ }
237
+ const wereBefore = nodes
238
+ .slice(0, nodes.indexOf(node))
239
+ .filter(n => unsorted.includes(n))
240
+ if (wereBefore.length > 0) {
241
+ unsorted = unsorted.filter(n => !wereBefore.includes(n))
242
+ return [...wereBefore, node]
243
+ }
244
+
245
+ return node
246
+ })
247
+ }
248
+ // Add remaining unsorted nodes
249
+ sorted.push(...unsorted)
250
+ return {
251
+ nodes: updateChildren(
252
+ ensureParentsFirst(sorted),
253
+ ),
254
+ edges: sortedEdges,
255
+ }
256
+ }
@@ -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
+ }