@likec4/core 1.34.1 → 1.35.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/builder/index.d.mts +2 -2
- package/dist/builder/index.mjs +11 -10
- package/dist/compute-view/index.d.mts +2 -2
- package/dist/compute-view/index.mjs +12 -11
- package/dist/compute-view/relationships-view/index.d.mts +2 -2
- package/dist/compute-view/relationships-view/index.mjs +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +9 -8
- package/dist/model/connection/deployment/index.d.mts +4 -4
- package/dist/model/connection/deployment/index.mjs +3 -3
- package/dist/model/connection/index.d.mts +5 -5
- package/dist/model/connection/index.mjs +4 -4
- package/dist/model/connection/model/index.d.mts +4 -4
- package/dist/model/connection/model/index.mjs +3 -3
- package/dist/model/index.d.mts +31 -7
- package/dist/model/index.mjs +9 -9
- package/dist/shared/{core.D3xEWj5B.d.mts → core.72IoI17N.d.mts} +1 -1
- package/dist/shared/{core.DXRAwwDB.mjs → core.B1ZN723l.mjs} +1 -1
- package/dist/shared/core.BpJu1RrF.mjs +5 -0
- package/dist/shared/{core.C80Z37Eu.mjs → core.C1GV0WWV.mjs} +1 -1
- package/dist/shared/{core.D5qVzkws.mjs → core.CQS11dNK.mjs} +2 -2
- package/dist/shared/{core.C_C-3c1S.mjs → core.DQj9iv_y.mjs} +3 -5
- package/dist/shared/{core.IWLie-Ma.d.mts → core.Ddua-wiI.d.mts} +3 -0
- package/dist/shared/{core.CJSFKrYf.d.mts → core.DhTO8rgm.d.mts} +2 -2
- package/dist/shared/{core.CtKXPqN_.mjs → core.HUONfQfd.mjs} +1 -1
- package/dist/shared/{core.Di7__sMg.d.mts → core.NBNeVyag.d.mts} +2 -2
- package/dist/shared/{core.PqNe5WxQ.mjs → core.RnUJywKk.mjs} +94 -92
- package/dist/shared/{core.DrS4Bf4y.d.mts → core.SciGxb--.d.mts} +183 -8
- package/dist/shared/{core.BTnf14gD.mjs → core.Y3j3s-oq.mjs} +2 -2
- package/dist/shared/{core.XptQjhMA.mjs → core.b2BzqGz0.mjs} +2 -2
- package/dist/shared/{core.4SpVCM9m.mjs → core.rpF8esVq.mjs} +354 -28
- package/dist/shared/{core.BMCb0wJI.mjs → core.yaRvRnMW.mjs} +1 -1
- package/dist/theme/index.mjs +33 -3
- package/dist/types/index.d.mts +2 -2
- package/dist/types/index.mjs +2 -2
- package/dist/utils/index.d.mts +6 -8
- package/dist/utils/index.mjs +5 -5
- package/dist/utils/iterable/index.d.mts +18 -1
- package/dist/utils/iterable/index.mjs +22 -2
- package/package.json +6 -5
- package/src/compute-view/element-view/predicates/relation-direct.ts +8 -2
- package/src/model/DeploymentModel.ts +1 -1
- package/src/model/LikeC4Model.ts +146 -7
- package/src/model/index.ts +8 -0
- package/src/model/types.ts +1 -8
- package/src/model/utils.ts +66 -0
- package/src/model/view/LikeC4ViewModel.ts +62 -7
- package/src/model/view/LikeC4ViewsFolder.ts +151 -0
- package/src/theme/__test__/theme-index.ts +29 -0
- package/src/theme/index.ts +49 -3
- package/src/types/model-spec.ts +4 -0
- package/src/utils/compare-natural.ts +14 -12
- package/src/utils/index.ts +1 -0
- package/src/utils/iterable/first.ts +48 -0
- package/src/utils/iterable/index.ts +1 -0
- package/dist/shared/{core.Br7Fab9l.mjs → core.BajmN8zR.mjs} +6 -6
package/dist/utils/index.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ export { D as DefaultMap } from '../shared/core.CywrQs86.mjs';
|
|
|
2
2
|
export { I as IterableContainer, R as ReorderedArray, a as ancestorsFqn, c as commonAncestor, b as compareByFqnHierarchically, d as compareFqnHierarchically, h as hierarchyDistance, e as hierarchyLevel, i as isAncestor, f as isDescendantOf, g as isSameHierarchy, n as nameFromFqn, p as parentFqn, s as sortByFqnHierarchically, j as sortNaturalByFqn, k as sortParentsFirst } from '../shared/core.CF82MWyh.mjs';
|
|
3
3
|
import { N as NonEmptyArray } from '../shared/core.DuSLYTG6.mjs';
|
|
4
4
|
import { Tagged, Simplify, Or, IfNever, IsLiteral, And, GreaterThan, IsNever, IsEqual, ReadonlyTuple, IsNumericLiteral } from 'type-fest';
|
|
5
|
-
export { ifilter, ifind, iflat, ihead, imap, ireduce, isIterable, isome, iunique, toArray, toSet } from './iterable/index.mjs';
|
|
5
|
+
export { ifilter, ifind, ifirst, iflat, ihead, imap, ireduce, isIterable, isome, iunique, toArray, toSet } from './iterable/index.mjs';
|
|
6
6
|
import '../shared/core.668kXpZq.mjs';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -231,22 +231,20 @@ declare function commonHead<T>(sources: ReadonlyArray<T>, targets: ReadonlyArray
|
|
|
231
231
|
|
|
232
232
|
declare function compareNatural(a: string | undefined, b: string | undefined): -1 | 0 | 1;
|
|
233
233
|
/**
|
|
234
|
-
* Compares two strings hierarchically based on their depth
|
|
234
|
+
* Compares two strings lexicographically first, then hierarchically based on their depth.\
|
|
235
235
|
* From parent nodes to leaves
|
|
236
236
|
*
|
|
237
237
|
* @example
|
|
238
238
|
* const lines = [
|
|
239
|
+
* 'b.c',
|
|
240
|
+
* 'b',
|
|
239
241
|
* 'a.b.c',
|
|
240
|
-
* 'a',
|
|
241
|
-
* 'a.b',
|
|
242
|
-
* 'a.c.c',
|
|
243
242
|
* ]
|
|
244
243
|
* lines.sort(compareNaturalHierarchically('.'))
|
|
245
244
|
* // [
|
|
246
|
-
* // 'a',
|
|
247
|
-
* // 'a.b',
|
|
248
245
|
* // 'a.b.c',
|
|
249
|
-
* // '
|
|
246
|
+
* // 'b',
|
|
247
|
+
* // 'b.c',
|
|
250
248
|
* // ]
|
|
251
249
|
*/
|
|
252
250
|
declare function compareNaturalHierarchically(separator?: string): (a: string | undefined, b: string | undefined) => number;
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
export { i as invariant, a as nonNullable, n as nonexhaustive } from '../shared/core.D4npX2q8.mjs';
|
|
2
|
-
export { S as Stack, c as commonHead, o as objectHash } from '../shared/core.
|
|
3
|
-
export { b as ancestorsFqn, d as commonAncestor, e as compareByFqnHierarchically, f as compareFqnHierarchically, c as compareNatural, a as compareNaturalHierarchically, h as hierarchyDistance, g as hierarchyLevel, i as isAncestor, j as isDescendantOf, o as isNonEmptyArray, k as isSameHierarchy, q as isString, n as nameFromFqn, p as parentFqn, s as sortByFqnHierarchically, l as sortNaturalByFqn, m as sortParentsFirst } from '../shared/core.
|
|
2
|
+
export { S as Stack, c as commonHead, o as objectHash } from '../shared/core.HUONfQfd.mjs';
|
|
3
|
+
export { b as ancestorsFqn, d as commonAncestor, e as compareByFqnHierarchically, f as compareFqnHierarchically, c as compareNatural, a as compareNaturalHierarchically, h as hierarchyDistance, g as hierarchyLevel, i as isAncestor, j as isDescendantOf, o as isNonEmptyArray, k as isSameHierarchy, q as isString, n as nameFromFqn, p as parentFqn, s as sortByFqnHierarchically, l as sortNaturalByFqn, m as sortParentsFirst } from '../shared/core.DQj9iv_y.mjs';
|
|
4
4
|
export { g as getOrCreate } from '../shared/core.DcS-0zys.mjs';
|
|
5
5
|
export { a as markdownToHtml, b as markdownToText, m as memoizeProp } from '../shared/core.B0oEIXqd.mjs';
|
|
6
|
-
export { b as BiMap, d as DefaultWeakMap, l as LinkedList, m as MultiMap, q as Queue, c as compareRelations, a as delay, p as promiseNextTick } from '../shared/core.
|
|
6
|
+
export { b as BiMap, d as DefaultWeakMap, l as LinkedList, m as MultiMap, q as Queue, c as compareRelations, a as delay, p as promiseNextTick } from '../shared/core.b2BzqGz0.mjs';
|
|
7
7
|
export { d as difference, e as equalsSet, i as hasAtLeast, a as intersection, b as stringHash, s as symmetricDifference, u as union } from '../shared/core.CpwrZi3D.mjs';
|
|
8
|
-
export { D as DefaultMap } from '../shared/core.
|
|
8
|
+
export { D as DefaultMap } from '../shared/core.yaRvRnMW.mjs';
|
|
9
9
|
export { i as ifilter } from '../shared/core.BH9nN-_I.mjs';
|
|
10
10
|
export { i as ifind, a as ihead } from '../shared/core.DsMrjY2q.mjs';
|
|
11
|
+
export { ifirst, ireduce } from './iterable/index.mjs';
|
|
11
12
|
export { i as iflat, a as isIterable, b as iunique } from '../shared/core.8KM0D6va.mjs';
|
|
12
13
|
export { i as imap, t as toArray, a as toSet } from '../shared/core.Btl47GOn.mjs';
|
|
13
|
-
export { ireduce } from './iterable/index.mjs';
|
|
14
14
|
export { i as isome } from '../shared/core.BoyJPEen.mjs';
|
|
15
15
|
import '../shared/core.DbRvwARP.mjs';
|
|
16
16
|
import '../shared/core.ByrmCl9I.mjs';
|
|
@@ -39,6 +39,23 @@ declare function ifind<T>(predicate: (item: T) => boolean): (iterable: Iterable<
|
|
|
39
39
|
declare function ifind<T, S extends T>(iterable: Iterable<T>, predicate: (item: T) => item is S): S | undefined;
|
|
40
40
|
declare function ifind<T>(iterable: Iterable<T>, predicate: (item: T) => boolean): T | undefined;
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Takes the first N elements from an iterable.
|
|
44
|
+
* Composable first version of `ifirst`.
|
|
45
|
+
* @signature
|
|
46
|
+
* ifirst(count)(data)
|
|
47
|
+
*/
|
|
48
|
+
declare function ifirst<T>(count: number): (iterable: Iterable<T>) => IteratorLike<T>;
|
|
49
|
+
/**
|
|
50
|
+
* Takes the first N elements from an iterable.
|
|
51
|
+
* Data first version of `ifirst`.
|
|
52
|
+
* @signature
|
|
53
|
+
* ifirst(data, count)
|
|
54
|
+
* @example
|
|
55
|
+
* ifirst([1, 2, 3, 4, 5], 3) // => Iterable<[1, 2, 3]>
|
|
56
|
+
*/
|
|
57
|
+
declare function ifirst<T>(iterable: Iterable<T>, count: number): IteratorLike<T>;
|
|
58
|
+
|
|
42
59
|
declare function iflat(): <T>(iterable: Iterable<IteratorLike<T>>) => IteratorLike<T>;
|
|
43
60
|
declare function iflat<T>(iterable: Iterable<IteratorLike<T>>): IteratorLike<T>;
|
|
44
61
|
|
|
@@ -97,4 +114,4 @@ declare function toSet<T>(iterable: Iterable<T>): Set<T>;
|
|
|
97
114
|
declare function iunique(): <T>(iterable: Iterable<T>) => IteratorLike<T>;
|
|
98
115
|
declare function iunique<T>(iterable: Iterable<T>): IteratorLike<T>;
|
|
99
116
|
|
|
100
|
-
export { ifilter, ifind, iflat, ihead, imap, ireduce, isIterable, isome, iunique, toArray, toSet };
|
|
117
|
+
export { ifilter, ifind, ifirst, iflat, ihead, imap, ireduce, isIterable, isome, iunique, toArray, toSet };
|
|
@@ -3,10 +3,30 @@ export { i as iflat, a as isIterable, b as iunique } from '../../shared/core.8KM
|
|
|
3
3
|
import { t } from '../../shared/core.BH9nN-_I.mjs';
|
|
4
4
|
export { i as ifilter } from '../../shared/core.BH9nN-_I.mjs';
|
|
5
5
|
export { i as ifind, a as ihead } from '../../shared/core.DsMrjY2q.mjs';
|
|
6
|
-
export { i as imap, t as toArray, a as toSet } from '../../shared/core.Btl47GOn.mjs';
|
|
7
6
|
import { i as invariant } from '../../shared/core.D4npX2q8.mjs';
|
|
7
|
+
export { i as imap, t as toArray, a as toSet } from '../../shared/core.Btl47GOn.mjs';
|
|
8
8
|
export { i as isome } from '../../shared/core.BoyJPEen.mjs';
|
|
9
9
|
|
|
10
|
+
function ifirst(arg1, arg2) {
|
|
11
|
+
const count = arg2 ?? arg1;
|
|
12
|
+
invariant(typeof count === "number" && count >= 0, "Count must be a non-negative number");
|
|
13
|
+
function* _first(iter) {
|
|
14
|
+
let taken = 0;
|
|
15
|
+
for (const value of iter) {
|
|
16
|
+
if (taken >= count) {
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
yield value;
|
|
20
|
+
taken++;
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (arg2 === void 0) {
|
|
25
|
+
return (iterable) => _first(iterable);
|
|
26
|
+
}
|
|
27
|
+
return _first(arg1);
|
|
28
|
+
}
|
|
29
|
+
|
|
10
30
|
function ireduce(arg1, arg2, arg3) {
|
|
11
31
|
const reducer = n(arg3) ? arg2 : arg1;
|
|
12
32
|
const initialValue = arg3 ?? arg2;
|
|
@@ -21,4 +41,4 @@ function ireduce(arg1, arg2, arg3) {
|
|
|
21
41
|
return n(arg3) ? _reduce(arg1) : _reduce;
|
|
22
42
|
}
|
|
23
43
|
|
|
24
|
-
export { ireduce };
|
|
44
|
+
export { ifirst, ireduce };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@likec4/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://likec4.dev",
|
|
6
6
|
"author": "Denis Davydkov <denis@davydkov.com>",
|
|
@@ -106,15 +106,16 @@
|
|
|
106
106
|
"unified": "11.0.5"
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
|
+
"@types/chroma-js": "^3.1.1",
|
|
109
110
|
"@dagrejs/dagre": "1.1.5",
|
|
110
|
-
"@mantine/colors-generator": "8.1
|
|
111
|
+
"@mantine/colors-generator": "8.2.1",
|
|
111
112
|
"@types/extend": "3.0.4",
|
|
112
113
|
"@types/hast": "3.0.4",
|
|
113
114
|
"@types/mdast": "4.0.4",
|
|
114
115
|
"@types/natural-compare-lite": "^1.4.2",
|
|
115
116
|
"@types/object-hash": "^3.0.6",
|
|
116
117
|
"@types/unist": "3.0.3",
|
|
117
|
-
"@types/node": "~20.19.
|
|
118
|
+
"@types/node": "~20.19.2",
|
|
118
119
|
"chroma-js": "^3.1.2",
|
|
119
120
|
"defu": "^6.1.4",
|
|
120
121
|
"fdir": "6.4.6",
|
|
@@ -126,11 +127,11 @@
|
|
|
126
127
|
"object-hash": "^3.0.0",
|
|
127
128
|
"obliterator": "^2.0.5",
|
|
128
129
|
"remeda": "^2.23.1",
|
|
129
|
-
"turbo": "2.5.
|
|
130
|
+
"turbo": "2.5.5",
|
|
130
131
|
"typescript": "5.8.3",
|
|
131
132
|
"unbuild": "3.5.0",
|
|
132
133
|
"vitest": "3.2.4",
|
|
133
|
-
"@likec4/tsconfig": "1.
|
|
134
|
+
"@likec4/tsconfig": "1.35.0"
|
|
134
135
|
},
|
|
135
136
|
"scripts": {
|
|
136
137
|
"typecheck": "tsc -b --verbose",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { concat, constant, flatMap, hasAtLeast, map, partition, pipe, piped, prop, when } from 'remeda'
|
|
2
|
-
import type { RelationshipModel } from '../../../model'
|
|
1
|
+
import { concat, constant, filter, flatMap, hasAtLeast, map, partition, pipe, piped, prop, when } from 'remeda'
|
|
2
|
+
import type { ElementModel, RelationshipModel } from '../../../model'
|
|
3
3
|
import { ConnectionModel } from '../../../model'
|
|
4
4
|
import { type AnyAux, type ModelRelationExpr, FqnRef, ModelFqnExpr } from '../../../types'
|
|
5
5
|
import { invariant, isSameHierarchy } from '../../../utils'
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
} from './_utils'
|
|
17
17
|
|
|
18
18
|
const isWildcard = ModelFqnExpr.isWildcard
|
|
19
|
+
const isAncestorOrDescendantOf = (a: ElementModel<AnyAux>, b: ElementModel<AnyAux>) =>
|
|
20
|
+
a.isAncestorOf(b) || a.isDescendantOf(b)
|
|
19
21
|
|
|
20
22
|
export const DirectRelationExprPredicate: PredicateExecutor<ModelRelationExpr.Direct<AnyAux>> = {
|
|
21
23
|
include: ({ expr: { source, target, isBidirectional = false }, memory, model, stage, where, filterWhere }) => {
|
|
@@ -75,6 +77,7 @@ export const DirectRelationExprPredicate: PredicateExecutor<ModelRelationExpr.Di
|
|
|
75
77
|
concat(
|
|
76
78
|
pipe(
|
|
77
79
|
outgoing,
|
|
80
|
+
filter(r => !isAncestorOrDescendantOf(source, r.target)),
|
|
78
81
|
map(outgoing =>
|
|
79
82
|
new ConnectionModel(
|
|
80
83
|
source,
|
|
@@ -85,6 +88,7 @@ export const DirectRelationExprPredicate: PredicateExecutor<ModelRelationExpr.Di
|
|
|
85
88
|
),
|
|
86
89
|
pipe(
|
|
87
90
|
incoming,
|
|
91
|
+
filter(r => !isAncestorOrDescendantOf(source, r.source)),
|
|
88
92
|
map(incoming =>
|
|
89
93
|
new ConnectionModel(
|
|
90
94
|
incoming.source,
|
|
@@ -133,6 +137,7 @@ export const DirectRelationExprPredicate: PredicateExecutor<ModelRelationExpr.Di
|
|
|
133
137
|
concat(
|
|
134
138
|
pipe(
|
|
135
139
|
outgoing,
|
|
140
|
+
filter(r => !isAncestorOrDescendantOf(target, r.target)),
|
|
136
141
|
map(outgoing =>
|
|
137
142
|
new ConnectionModel(
|
|
138
143
|
target,
|
|
@@ -143,6 +148,7 @@ export const DirectRelationExprPredicate: PredicateExecutor<ModelRelationExpr.Di
|
|
|
143
148
|
),
|
|
144
149
|
pipe(
|
|
145
150
|
incoming,
|
|
151
|
+
filter(r => !isAncestorOrDescendantOf(target, r.source)),
|
|
146
152
|
map(incoming =>
|
|
147
153
|
new ConnectionModel(
|
|
148
154
|
incoming.source,
|
|
@@ -30,8 +30,8 @@ import {
|
|
|
30
30
|
type IncomingFilter,
|
|
31
31
|
type OutgoingFilter,
|
|
32
32
|
type RelationOrId,
|
|
33
|
-
getId,
|
|
34
33
|
} from './types'
|
|
34
|
+
import { getId } from './utils'
|
|
35
35
|
import type { LikeC4ViewModel } from './view/LikeC4ViewModel'
|
|
36
36
|
|
|
37
37
|
export class LikeC4DeploymentModel<A extends Any = Any> {
|
package/src/model/LikeC4Model.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { entries, map, pipe, prop, sort, sortBy, values } from 'remeda'
|
|
1
|
+
import { entries, hasAtLeast, map, pipe, prop, sort, sortBy, split, values } from 'remeda'
|
|
2
2
|
import type {
|
|
3
3
|
Any,
|
|
4
4
|
Aux,
|
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
} from '../types'
|
|
18
18
|
import { type ProjectId, _stage, GlobalFqn, isGlobalFqn, isOnStage, whereOperatorAsPredicate } from '../types'
|
|
19
19
|
import type * as aux from '../types/_aux'
|
|
20
|
-
import { compareNatural, ifilter, invariant, memoizeProp, nonNullable } from '../utils'
|
|
20
|
+
import { compareNatural, compareNaturalHierarchically, ifilter, invariant, memoizeProp, nonNullable } from '../utils'
|
|
21
21
|
import { ancestorsFqn, commonAncestor, parentFqn, sortParentsFirst } from '../utils/fqn'
|
|
22
22
|
import { DefaultMap } from '../utils/mnemonist'
|
|
23
23
|
import type {
|
|
@@ -36,9 +36,10 @@ import {
|
|
|
36
36
|
type IncomingFilter,
|
|
37
37
|
type OutgoingFilter,
|
|
38
38
|
type RelationOrId,
|
|
39
|
-
getId,
|
|
40
39
|
} from './types'
|
|
40
|
+
import { getId, getViewFolderPath, normalizeViewPath, VIEW_FOLDERS_SEPARATOR } from './utils'
|
|
41
41
|
import { LikeC4ViewModel } from './view/LikeC4ViewModel'
|
|
42
|
+
import { LikeC4ViewsFolder } from './view/LikeC4ViewsFolder'
|
|
42
43
|
import type { NodeModel } from './view/NodeModel'
|
|
43
44
|
|
|
44
45
|
export class LikeC4Model<A extends Any = aux.Unknown> {
|
|
@@ -68,6 +69,16 @@ export class LikeC4Model<A extends Any = aux.Unknown> {
|
|
|
68
69
|
|
|
69
70
|
protected readonly _views = new Map<aux.ViewId<A>, LikeC4ViewModel<A>>()
|
|
70
71
|
|
|
72
|
+
protected readonly _rootViewFolder: LikeC4ViewsFolder<A>
|
|
73
|
+
protected readonly _viewFolders = new Map<string, LikeC4ViewsFolder<A>>()
|
|
74
|
+
|
|
75
|
+
protected readonly _viewFolderItems = new DefaultMap<
|
|
76
|
+
string,
|
|
77
|
+
Set<LikeC4ViewsFolder<A> | LikeC4ViewModel<A>>
|
|
78
|
+
>(
|
|
79
|
+
() => new Set(),
|
|
80
|
+
)
|
|
81
|
+
|
|
71
82
|
protected readonly _allTags = new DefaultMap<
|
|
72
83
|
aux.Tag<A>,
|
|
73
84
|
Set<ElementModel<A> | RelationshipModel<A> | LikeC4ViewModel<A>>
|
|
@@ -165,17 +176,65 @@ export class LikeC4Model<A extends Any = aux.Unknown> {
|
|
|
165
176
|
this.deployment = new LikeC4DeploymentModel(this)
|
|
166
177
|
|
|
167
178
|
if (isOnStage($data, 'computed') || isOnStage($data, 'layouted')) {
|
|
179
|
+
const compare = compareNaturalHierarchically(VIEW_FOLDERS_SEPARATOR)
|
|
168
180
|
const views = pipe(
|
|
169
181
|
values($data.views as Record<string, $View<A>>),
|
|
170
|
-
|
|
182
|
+
map(view => ({
|
|
183
|
+
view,
|
|
184
|
+
path: normalizeViewPath(view.title ?? view.id),
|
|
185
|
+
folderPath: view.title && getViewFolderPath(view.title) || '',
|
|
186
|
+
})),
|
|
187
|
+
// Sort hierarchically by groups, but keep same order within groups
|
|
188
|
+
sort((a, b) => compare(a.folderPath, b.folderPath)),
|
|
171
189
|
)
|
|
172
|
-
|
|
173
|
-
|
|
190
|
+
|
|
191
|
+
const getOrCreateFolder = (path: string) => {
|
|
192
|
+
let folder = this._viewFolders.get(path)
|
|
193
|
+
if (!folder) {
|
|
194
|
+
const segments = split(path, VIEW_FOLDERS_SEPARATOR)
|
|
195
|
+
invariant(hasAtLeast(segments, 1), `View group path "${path}" must have at least one element`)
|
|
196
|
+
let defaultView
|
|
197
|
+
// Root group has "index" as default view
|
|
198
|
+
if (path === '') {
|
|
199
|
+
defaultView = views.find(view => view.view.id === 'index')
|
|
200
|
+
} else {
|
|
201
|
+
defaultView = views.find(view => view.path === path)
|
|
202
|
+
}
|
|
203
|
+
folder = new LikeC4ViewsFolder(this, segments, defaultView?.view.id)
|
|
204
|
+
this._viewFolders.set(path, folder)
|
|
205
|
+
}
|
|
206
|
+
return folder
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this._rootViewFolder = getOrCreateFolder('')
|
|
210
|
+
|
|
211
|
+
// Process view groups
|
|
212
|
+
// Sort in natural order to preserve hierarchy
|
|
213
|
+
for (const { folderPath } of views) {
|
|
214
|
+
if (this._viewFolders.has(folderPath)) {
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
// Create groups for each segment of the path
|
|
218
|
+
split(folderPath, VIEW_FOLDERS_SEPARATOR).reduce((parent, segment) => {
|
|
219
|
+
const path = [...parent, segment]
|
|
220
|
+
const folder = getOrCreateFolder(path.join(VIEW_FOLDERS_SEPARATOR))
|
|
221
|
+
this._viewFolderItems.get(parent.join(VIEW_FOLDERS_SEPARATOR)).add(folder)
|
|
222
|
+
return path
|
|
223
|
+
}, [] as string[])
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const { view, folderPath } of views) {
|
|
227
|
+
const vm = new LikeC4ViewModel(this, view, getOrCreateFolder(folderPath))
|
|
228
|
+
this._viewFolderItems.get(folderPath).add(vm)
|
|
174
229
|
this._views.set(view.id, vm)
|
|
175
230
|
for (const tag of vm.tags) {
|
|
176
231
|
this._allTags.get(tag).add(vm)
|
|
177
232
|
}
|
|
178
233
|
}
|
|
234
|
+
} else {
|
|
235
|
+
// Model is not computed or layouted, but we still need to create root folder
|
|
236
|
+
this._rootViewFolder = new LikeC4ViewsFolder(this, [''], undefined)
|
|
237
|
+
this._viewFolders.set(this._rootViewFolder.path, this._rootViewFolder)
|
|
179
238
|
}
|
|
180
239
|
}
|
|
181
240
|
|
|
@@ -254,6 +313,21 @@ export class LikeC4Model<A extends Any = aux.Unknown> {
|
|
|
254
313
|
}))
|
|
255
314
|
}
|
|
256
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Returns the element with the given FQN.
|
|
318
|
+
*
|
|
319
|
+
* @throws Error if element is not found\
|
|
320
|
+
* Use {@link findElement} if you don't want to throw an error
|
|
321
|
+
*
|
|
322
|
+
* @note Method is type-safe for typed model
|
|
323
|
+
|
|
324
|
+
* @example
|
|
325
|
+
* model.element('cloud.frontend')
|
|
326
|
+
* // or object with id property of scalar.Fqn
|
|
327
|
+
* model.element({
|
|
328
|
+
* id: 'dashboard',
|
|
329
|
+
* })
|
|
330
|
+
*/
|
|
257
331
|
public element(el: ElementOrFqn<A>): ElementModel<A> {
|
|
258
332
|
if (el instanceof ElementModel) {
|
|
259
333
|
return el
|
|
@@ -261,6 +335,16 @@ export class LikeC4Model<A extends Any = aux.Unknown> {
|
|
|
261
335
|
const id = getId(el)
|
|
262
336
|
return nonNullable(this._elements.get(id), `Element ${id} not found`)
|
|
263
337
|
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Returns the element with the given FQN.
|
|
341
|
+
*
|
|
342
|
+
* @returns Element if found, null otherwise
|
|
343
|
+
* @note Method is not type-safe as {@link element}
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* model.findElement('cloud.frontend')
|
|
347
|
+
*/
|
|
264
348
|
public findElement(el: aux.LooseElementId<A>): ElementModel<A> | null {
|
|
265
349
|
return this._elements.get(getId(el)) ?? null
|
|
266
350
|
}
|
|
@@ -342,15 +426,69 @@ export class LikeC4Model<A extends Any = aux.Unknown> {
|
|
|
342
426
|
|
|
343
427
|
/**
|
|
344
428
|
* Returns a specific view by its ID.
|
|
429
|
+
* @note Method is type-safe for typed model
|
|
430
|
+
* @throws Error if view is not found\
|
|
431
|
+
* Use {@link findView} if you don't want to throw an error
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* model.view('index')
|
|
435
|
+
* // or object with id property of scalar.ViewId
|
|
436
|
+
* model.view({
|
|
437
|
+
* id: 'index',
|
|
438
|
+
* })
|
|
345
439
|
*/
|
|
346
440
|
public view(viewId: aux.ViewId<A> | { id: scalar.ViewId<aux.ViewId<A>> }): LikeC4ViewModel<A, $View<A>> {
|
|
347
441
|
const id = getId(viewId)
|
|
348
442
|
return nonNullable(this._views.get(id), `View ${id} not found`)
|
|
349
443
|
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Returns a specific view by its ID.
|
|
447
|
+
* @note Method is not type-safe as {@link view}
|
|
448
|
+
*
|
|
449
|
+
* @example
|
|
450
|
+
* model.findView('index')
|
|
451
|
+
*/
|
|
350
452
|
public findView(viewId: aux.LooseViewId<A>): LikeC4ViewModel<A, $View<A>> | null {
|
|
351
453
|
return this._views.get(viewId as aux.ViewId<A>) ?? null
|
|
352
454
|
}
|
|
353
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Returns a view folder by its path.
|
|
458
|
+
* Path is extracted from the view title, e.g. "Group 1/Group 2/View" -> "Group 1/Group 2"
|
|
459
|
+
* @throws Error if view folder is not found.
|
|
460
|
+
*/
|
|
461
|
+
public viewFolder(path: string): LikeC4ViewsFolder<A> {
|
|
462
|
+
return nonNullable(this._viewFolders.get(path), `View folder ${path} not found`)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Root folder is a special one with an empty path and used only for internal purposes.
|
|
467
|
+
* It is not visible to the user and should be used only to get top-level folders and views.
|
|
468
|
+
*/
|
|
469
|
+
get rootViewFolder(): LikeC4ViewsFolder<A> {
|
|
470
|
+
return this._rootViewFolder
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Whether the model has any view folders.
|
|
475
|
+
*/
|
|
476
|
+
get hasViewFolders(): boolean {
|
|
477
|
+
// Root view folder is always present
|
|
478
|
+
return this._viewFolders.size > 1
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Returns all children of a view folder.
|
|
483
|
+
* Path is extracted from the view title, e.g. "Group 1/Group 2/View" -> "Group 1/Group 2"
|
|
484
|
+
*
|
|
485
|
+
* @throws Error if view folder is not found.
|
|
486
|
+
*/
|
|
487
|
+
public viewFolderItems(path: string): ReadonlySet<LikeC4ViewsFolder<A> | LikeC4ViewModel<A>> {
|
|
488
|
+
invariant(this._viewFolders.has(path), `View folder ${path} not found`)
|
|
489
|
+
return this._viewFolderItems.get(path)
|
|
490
|
+
}
|
|
491
|
+
|
|
354
492
|
/**
|
|
355
493
|
* Returns the parent element of given element.
|
|
356
494
|
* @see ancestors
|
|
@@ -452,7 +590,8 @@ export class LikeC4Model<A extends Any = aux.Unknown> {
|
|
|
452
590
|
}
|
|
453
591
|
|
|
454
592
|
/**
|
|
455
|
-
* Returns all tags used in the model, sorted
|
|
593
|
+
* Returns array of all tags used in the model, sorted naturally.\
|
|
594
|
+
* Use {@link specification.tags} to get all defined tags
|
|
456
595
|
*/
|
|
457
596
|
get tags(): aux.Tags<A> {
|
|
458
597
|
return memoizeProp(this, 'tags', () => sort([...this._allTags.keys()], compareNatural))
|
package/src/model/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export { LikeC4DeploymentModel } from './DeploymentModel'
|
|
|
36
36
|
|
|
37
37
|
export { EdgeModel } from './view/EdgeModel'
|
|
38
38
|
export { LikeC4ViewModel } from './view/LikeC4ViewModel'
|
|
39
|
+
export { LikeC4ViewsFolder } from './view/LikeC4ViewsFolder'
|
|
39
40
|
export { NodeModel } from './view/NodeModel'
|
|
40
41
|
|
|
41
42
|
export type {
|
|
@@ -62,3 +63,10 @@ export type {
|
|
|
62
63
|
} from '../types'
|
|
63
64
|
|
|
64
65
|
export { RichText, type RichTextEmpty, type RichTextOrEmpty } from '../types'
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
extractViewTitleFromPath,
|
|
69
|
+
getViewFolderPath,
|
|
70
|
+
normalizeViewPath,
|
|
71
|
+
VIEW_FOLDERS_SEPARATOR,
|
|
72
|
+
} from './utils'
|
package/src/model/types.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { LikeC4ViewModel } from './view/LikeC4ViewModel'
|
|
|
16
16
|
export type IncomingFilter = 'all' | 'direct' | 'to-descendants'
|
|
17
17
|
export type OutgoingFilter = 'all' | 'direct' | 'from-descendants'
|
|
18
18
|
|
|
19
|
-
type AnyScalar<V extends string> =
|
|
19
|
+
export type AnyScalar<V extends string> =
|
|
20
20
|
| Tagged<V, 'Fqn'>
|
|
21
21
|
| Tagged<V, 'DeploymentFqn'>
|
|
22
22
|
| Tagged<V, 'ViewId'>
|
|
@@ -24,13 +24,6 @@ type AnyScalar<V extends string> =
|
|
|
24
24
|
| Tagged<V, 'EdgeId'>
|
|
25
25
|
| Tagged<V, 'RelationId'>
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* Utility function to extract `id` from the given element.
|
|
29
|
-
*/
|
|
30
|
-
export function getId<Id extends string, Scalar extends AnyScalar<Id>>(element: Id | { id: Scalar }): Scalar {
|
|
31
|
-
return typeof element === 'string' ? element as unknown as Scalar : element.id
|
|
32
|
-
}
|
|
33
|
-
|
|
34
27
|
export type ElementOrFqn<A extends AnyAux> = aux.ElementId<A> | { id: aux.Fqn<A> }
|
|
35
28
|
|
|
36
29
|
export type DeploymentOrFqn<A extends AnyAux> = aux.DeploymentId<A> | { id: aux.DeploymentFqn<A> }
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { hasAtLeast } from 'remeda'
|
|
2
|
+
import type { NonEmptyArray } from '../types'
|
|
3
|
+
import { invariant } from '../utils'
|
|
4
|
+
import type { AnyScalar } from './types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Utility function to extract `id` from the given element.
|
|
8
|
+
*/
|
|
9
|
+
export const getId = <Id extends string, Scalar extends AnyScalar<Id>>(element: Id | { id: Scalar }): Scalar => {
|
|
10
|
+
return typeof element === 'string' ? element as unknown as Scalar : element.id
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const VIEW_FOLDERS_SEPARATOR = '/'
|
|
14
|
+
|
|
15
|
+
const splitViewTitle = (title: string): NonEmptyArray<string> => {
|
|
16
|
+
invariant(!title.includes('\n'), 'View title cannot contain newlines')
|
|
17
|
+
if (title.includes(VIEW_FOLDERS_SEPARATOR)) {
|
|
18
|
+
const segments = title
|
|
19
|
+
.split(VIEW_FOLDERS_SEPARATOR)
|
|
20
|
+
.map(s => s.trim())
|
|
21
|
+
.filter(s => s.length > 0)
|
|
22
|
+
if (hasAtLeast(segments, 1)) {
|
|
23
|
+
return segments
|
|
24
|
+
}
|
|
25
|
+
return ['']
|
|
26
|
+
}
|
|
27
|
+
return [title.trim()]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalizes view path by removing spaces from segments, removing empty segments,
|
|
32
|
+
* and removing leading/trailing slashes
|
|
33
|
+
* @example
|
|
34
|
+
* normalizeViewPath('One / Tw o / Thre e') === 'One/Tw o/Thre e'
|
|
35
|
+
*/
|
|
36
|
+
export const normalizeViewPath = (title: string): string => {
|
|
37
|
+
return splitViewTitle(title).join(VIEW_FOLDERS_SEPARATOR)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns view group path if it is used as a path
|
|
42
|
+
* Returns empty string if it is not a path
|
|
43
|
+
* @example
|
|
44
|
+
* getViewFolderPath('One / Tw o / Thre e') === 'One/Tw o'
|
|
45
|
+
* getViewFolderPath('One') === ''
|
|
46
|
+
*/
|
|
47
|
+
export const getViewFolderPath = (title: string): string | null => {
|
|
48
|
+
const segments = splitViewTitle(title)
|
|
49
|
+
if (!hasAtLeast(segments, 2)) {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
return segments.slice(0, -1).join(VIEW_FOLDERS_SEPARATOR)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns view title if it is used as a path
|
|
57
|
+
* @example
|
|
58
|
+
* getViewTitleFromPath('One / Tw o / Thre e') === 'Thre e'
|
|
59
|
+
* getViewTitleFromPath('One') === 'One'
|
|
60
|
+
*/
|
|
61
|
+
export const extractViewTitleFromPath = (title: string): string => {
|
|
62
|
+
if (!title.includes(VIEW_FOLDERS_SEPARATOR)) {
|
|
63
|
+
return title.trim()
|
|
64
|
+
}
|
|
65
|
+
return splitViewTitle(title).pop() ?? title
|
|
66
|
+
}
|
|
@@ -7,10 +7,9 @@ import type {
|
|
|
7
7
|
scalar,
|
|
8
8
|
ViewWithType,
|
|
9
9
|
} from '../../types'
|
|
10
|
-
import { _stage, _type } from '../../types'
|
|
11
|
-
import { type RichTextOrEmpty, RichText } from '../../types'
|
|
10
|
+
import { type RichTextOrEmpty, _stage, _type, RichText } from '../../types'
|
|
12
11
|
import type * as aux from '../../types/_aux'
|
|
13
|
-
import { DefaultMap, ifind, nonNullable } from '../../utils'
|
|
12
|
+
import { DefaultMap, ifind, memoizeProp, nonNullable } from '../../utils'
|
|
14
13
|
import type { ElementModel } from '../ElementModel'
|
|
15
14
|
import type { LikeC4Model } from '../LikeC4Model'
|
|
16
15
|
import {
|
|
@@ -18,9 +17,10 @@ import {
|
|
|
18
17
|
type EdgeOrId,
|
|
19
18
|
type NodeOrId,
|
|
20
19
|
type WithTags,
|
|
21
|
-
getId,
|
|
22
20
|
} from '../types'
|
|
21
|
+
import { extractViewTitleFromPath, getId, normalizeViewPath } from '../utils'
|
|
23
22
|
import { type EdgesIterator, EdgeModel } from './EdgeModel'
|
|
23
|
+
import type { LikeC4ViewsFolder } from './LikeC4ViewsFolder'
|
|
24
24
|
import { type NodesIterator, NodeModel } from './NodeModel'
|
|
25
25
|
|
|
26
26
|
export type ViewsIterator<A extends Any, V extends $View<A> = $View<A>> = IteratorLike<LikeC4ViewModel<A, V>>
|
|
@@ -44,9 +44,29 @@ export class LikeC4ViewModel<A extends Any = Any, V extends $View<A> = $View<A>>
|
|
|
44
44
|
public readonly $view: V
|
|
45
45
|
public readonly $model: LikeC4Model<A>
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
public readonly title: string | null
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* View folder this view belongs to.
|
|
51
|
+
* If view is top-level, this is the root folder.
|
|
52
|
+
*/
|
|
53
|
+
public readonly folder: LikeC4ViewsFolder<A>
|
|
54
|
+
/**
|
|
55
|
+
* Path to this view, processed by {@link normalizeViewPath}
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* "Group 1/Group 2/View"
|
|
59
|
+
*/
|
|
60
|
+
public readonly viewPath: string
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
model: LikeC4Model<A>,
|
|
64
|
+
view: V,
|
|
65
|
+
folder: LikeC4ViewsFolder<A>,
|
|
66
|
+
) {
|
|
48
67
|
this.$model = model
|
|
49
68
|
this.$view = view
|
|
69
|
+
this.folder = folder
|
|
50
70
|
for (const node of view.nodes) {
|
|
51
71
|
const el = new NodeModel<A, V>(this, Object.freeze(node))
|
|
52
72
|
this.#nodes.set(node.id, el)
|
|
@@ -79,6 +99,9 @@ export class LikeC4ViewModel<A extends Any = Any, V extends $View<A> = $View<A>>
|
|
|
79
99
|
}
|
|
80
100
|
this.#edges.set(edge.id, edgeModel)
|
|
81
101
|
}
|
|
102
|
+
|
|
103
|
+
this.title = this.$view.title ? extractViewTitleFromPath(this.$view.title) : null
|
|
104
|
+
this.viewPath = this.$view.title ? normalizeViewPath(this.$view.title) : this.$view.id
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
get _type(): V[_type] {
|
|
@@ -89,8 +112,40 @@ export class LikeC4ViewModel<A extends Any = Any, V extends $View<A> = $View<A>>
|
|
|
89
112
|
return this.$view.id
|
|
90
113
|
}
|
|
91
114
|
|
|
92
|
-
|
|
93
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Returns title if defined, otherwise returns its {@link id}
|
|
117
|
+
*/
|
|
118
|
+
get titleOrId(): string {
|
|
119
|
+
return this.title ?? this.id
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns title if defined, otherwise returns `Untitled`.
|
|
124
|
+
*/
|
|
125
|
+
get titleOrUntitled(): string {
|
|
126
|
+
return this.title ?? 'Untitled'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Returns path to this view as an array of groups and this view as the last element
|
|
131
|
+
* If view is top-level, returns only this view.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* viewPath = "Group 1/Group 2/View"
|
|
135
|
+
*
|
|
136
|
+
* breadcrumbs = [
|
|
137
|
+
* "Group 1", // folder
|
|
138
|
+
* "Group 1/Group 2", // folder
|
|
139
|
+
* "Group 1/Group 2/View" // view
|
|
140
|
+
* ]
|
|
141
|
+
*/
|
|
142
|
+
get breadcrumbs(): [...LikeC4ViewsFolder<A>[], this] {
|
|
143
|
+
return memoizeProp(this, 'breadcrumbs', () => {
|
|
144
|
+
if (!this.folder.isRoot) {
|
|
145
|
+
return [...this.folder.breadcrumbs, this]
|
|
146
|
+
}
|
|
147
|
+
return [this]
|
|
148
|
+
})
|
|
94
149
|
}
|
|
95
150
|
|
|
96
151
|
get description(): RichTextOrEmpty {
|