@likec4/core 1.1.1 → 1.2.1
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/colors/element.d.ts +1 -0
- package/dist/colors/index.d.ts +1 -0
- package/dist/colors/relationships.d.ts +1 -0
- package/dist/errors/errors.spec.d.ts +2 -0
- package/dist/errors/errors.spec.js +69 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +7 -1
- package/dist/index.d.ts +1 -0
- package/dist/types/_common.d.ts +1 -0
- package/dist/types/element.d.ts +1 -0
- package/dist/types/expression.d.ts +1 -0
- package/dist/types/index.d.ts +1 -2
- package/dist/types/index.js +2 -3
- package/dist/types/model.d.ts +3 -3
- package/dist/types/opaque.d.ts +1 -0
- package/dist/types/relation.d.ts +1 -0
- package/dist/types/theme.d.ts +1 -0
- package/dist/types/view.d.ts +133 -8
- package/dist/types/view.js +31 -3
- package/dist/utils/fqn.d.ts +1 -0
- package/dist/utils/fqn.spec.d.ts +2 -0
- package/dist/utils/fqn.spec.js +82 -0
- package/dist/utils/guards.d.ts +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/promises.d.ts +1 -0
- package/dist/utils/relations.d.ts +1 -0
- package/dist/utils/relations.spec.d.ts +2 -0
- package/dist/utils/relations.spec.js +210 -0
- package/package.json +6 -6
- package/src/colors/element.ts +80 -0
- package/src/colors/index.ts +12 -0
- package/src/colors/relationships.ts +54 -0
- package/src/errors/errors.spec.ts +88 -0
- package/src/errors/index.ts +120 -0
- package/src/index.ts +9 -0
- package/src/reset.d.ts +2 -0
- package/src/types/_common.ts +5 -0
- package/src/types/element.ts +56 -0
- package/src/types/expression.ts +140 -0
- package/src/types/index.ts +10 -0
- package/src/types/model.ts +15 -0
- package/src/types/opaque.ts +108 -0
- package/src/types/relation.ts +38 -0
- package/src/types/theme.ts +46 -0
- package/src/types/view.ts +263 -0
- package/src/utils/fqn.spec.ts +105 -0
- package/src/utils/fqn.ts +112 -0
- package/src/utils/guards.ts +10 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/promises.ts +9 -0
- package/src/utils/relations.spec.ts +268 -0
- package/src/utils/relations.ts +104 -0
- package/dist/types/computed-view.d.ts +0 -52
- package/dist/types/computed-view.js +0 -1
- package/dist/types/diagram.d.ts +0 -40
- package/dist/types/diagram.js +0 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { compareRelations, Relations } from './relations';
|
|
3
|
+
const { isAnyBetween, isAnyInOut, isBetween, isIncoming, isInside, isOutgoing } = Relations;
|
|
4
|
+
const relations = [
|
|
5
|
+
{
|
|
6
|
+
id: 'customer:cloud.frontend.dashboard',
|
|
7
|
+
source: 'customer',
|
|
8
|
+
target: 'cloud.frontend.dashboard',
|
|
9
|
+
title: ''
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: 'support:cloud.frontend.adminPanel',
|
|
13
|
+
source: 'support',
|
|
14
|
+
target: 'cloud.frontend.adminPanel',
|
|
15
|
+
title: ''
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'cloud.backend.storage:amazon.s3',
|
|
19
|
+
source: 'cloud.backend.storage',
|
|
20
|
+
target: 'amazon.s3',
|
|
21
|
+
title: ''
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'amazon.api:cloud.backend.graphql',
|
|
25
|
+
source: 'amazon.api',
|
|
26
|
+
target: 'cloud.backend.graphql',
|
|
27
|
+
title: ''
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'cloud.backend.graphql:cloud.backend.storage',
|
|
31
|
+
source: 'cloud.backend.graphql',
|
|
32
|
+
target: 'cloud.backend.storage',
|
|
33
|
+
title: ''
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'cloud.frontend.dashboard:cloud.backend.graphql',
|
|
37
|
+
source: 'cloud.frontend.dashboard',
|
|
38
|
+
target: 'cloud.backend.graphql',
|
|
39
|
+
title: ''
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'cloud.frontend.adminPanel:cloud.backend.graphql',
|
|
43
|
+
source: 'cloud.frontend.adminPanel',
|
|
44
|
+
target: 'cloud.backend.graphql',
|
|
45
|
+
title: ''
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
describe('relation predicates', () => {
|
|
49
|
+
const expectRelations = (predicate) => expect(relations.filter(predicate).map(r => r.id));
|
|
50
|
+
const customer = 'customer';
|
|
51
|
+
const cloud = 'cloud';
|
|
52
|
+
const frontend = 'cloud.frontend';
|
|
53
|
+
const backend = 'cloud.backend';
|
|
54
|
+
it('isBetween: cloud.frontend -> cloud.backend', () => {
|
|
55
|
+
expectRelations(isBetween(frontend, backend)).toEqual([
|
|
56
|
+
'cloud.frontend.dashboard:cloud.backend.graphql',
|
|
57
|
+
'cloud.frontend.adminPanel:cloud.backend.graphql'
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
it('isBetween: cloud.frontend.dashboard -> cloud.backend', () => {
|
|
61
|
+
expectRelations(isBetween('cloud.frontend.dashboard', backend)).toEqual([
|
|
62
|
+
'cloud.frontend.dashboard:cloud.backend.graphql'
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
it('isBetween: cloud.backend -> cloud.frontend', () => {
|
|
66
|
+
expectRelations(isBetween(backend, frontend)).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
it('isBetween: cloud.backend -> cloud.backend', () => {
|
|
69
|
+
expectRelations(isBetween(backend, backend)).toEqual([
|
|
70
|
+
'cloud.backend.graphql:cloud.backend.storage'
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
it('isBetween: customer -> cloud', () => {
|
|
74
|
+
expectRelations(isBetween(customer, cloud)).toEqual(['customer:cloud.frontend.dashboard']);
|
|
75
|
+
});
|
|
76
|
+
it('isIncoming: customer', () => {
|
|
77
|
+
expectRelations(isIncoming(customer)).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
it('isIncoming: cloud', () => {
|
|
80
|
+
expectRelations(isIncoming(cloud)).toEqual([
|
|
81
|
+
'customer:cloud.frontend.dashboard',
|
|
82
|
+
'support:cloud.frontend.adminPanel',
|
|
83
|
+
'amazon.api:cloud.backend.graphql'
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
it('isIncoming: cloud.frontend', () => {
|
|
87
|
+
expectRelations(isIncoming(frontend)).toEqual([
|
|
88
|
+
'customer:cloud.frontend.dashboard',
|
|
89
|
+
'support:cloud.frontend.adminPanel'
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
it('isIncoming: cloud.frontend.dashboard', () => {
|
|
93
|
+
expectRelations(isIncoming('cloud.frontend.dashboard')).toEqual([
|
|
94
|
+
'customer:cloud.frontend.dashboard'
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
it('isOutgoing: cloud', () => {
|
|
98
|
+
expectRelations(isOutgoing(cloud)).toEqual(['cloud.backend.storage:amazon.s3']);
|
|
99
|
+
});
|
|
100
|
+
it('isOutgoing: cloud.frontend', () => {
|
|
101
|
+
expectRelations(isOutgoing(frontend)).toEqual([
|
|
102
|
+
'cloud.frontend.dashboard:cloud.backend.graphql',
|
|
103
|
+
'cloud.frontend.adminPanel:cloud.backend.graphql'
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
it('isOutgoing: cloud.backend.storage', () => {
|
|
107
|
+
expectRelations(isOutgoing('cloud.backend.storage')).toEqual([
|
|
108
|
+
'cloud.backend.storage:amazon.s3'
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
it('isInside: cloud', () => {
|
|
112
|
+
expectRelations(isInside(cloud)).toEqual([
|
|
113
|
+
'cloud.backend.graphql:cloud.backend.storage',
|
|
114
|
+
'cloud.frontend.dashboard:cloud.backend.graphql',
|
|
115
|
+
'cloud.frontend.adminPanel:cloud.backend.graphql'
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
it('isInside: cloud.frontend', () => {
|
|
119
|
+
expectRelations(isInside(frontend)).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
it('isInside: cloud.backend', () => {
|
|
122
|
+
expectRelations(isInside(backend)).toEqual(['cloud.backend.graphql:cloud.backend.storage']);
|
|
123
|
+
});
|
|
124
|
+
it('isAnyInOut: customer', () => {
|
|
125
|
+
expectRelations(isAnyInOut(customer)).toEqual(['customer:cloud.frontend.dashboard']);
|
|
126
|
+
});
|
|
127
|
+
it('isAnyInOut: cloud', () => {
|
|
128
|
+
expectRelations(isAnyInOut(cloud)).toEqual([
|
|
129
|
+
'customer:cloud.frontend.dashboard',
|
|
130
|
+
'support:cloud.frontend.adminPanel',
|
|
131
|
+
'cloud.backend.storage:amazon.s3',
|
|
132
|
+
'amazon.api:cloud.backend.graphql'
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
it('isAnyBetween: cloud and amazon', () => {
|
|
136
|
+
expectRelations(isAnyBetween(cloud, 'amazon')).toEqual([
|
|
137
|
+
'cloud.backend.storage:amazon.s3',
|
|
138
|
+
'amazon.api:cloud.backend.graphql'
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('compareRelations', () => {
|
|
143
|
+
function rel(source, target) {
|
|
144
|
+
return {
|
|
145
|
+
source,
|
|
146
|
+
target
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function sorted(...relations) {
|
|
150
|
+
return relations.sort(compareRelations).map(r => r.source + ' -> ' + r.target);
|
|
151
|
+
}
|
|
152
|
+
it('should sort by source and target', () => {
|
|
153
|
+
expect(sorted(rel('customer', 'cloud.ui'), rel('customer', 'cloud'))).toEqual([
|
|
154
|
+
'customer -> cloud',
|
|
155
|
+
'customer -> cloud.ui'
|
|
156
|
+
]);
|
|
157
|
+
expect(sorted(rel('1', '2.1'), rel('1', '2'), rel('1.1.1', '2'), rel('1.1.1', '2.1'), rel('1.1', '2'))).toEqual(['1 -> 2', '1 -> 2.1', '1.1 -> 2', '1.1.1 -> 2', '1.1.1 -> 2.1']);
|
|
158
|
+
});
|
|
159
|
+
it('should sort by parent', () => {
|
|
160
|
+
expect(sorted(
|
|
161
|
+
// same parent
|
|
162
|
+
rel('cloud.1.1', 'cloud.2.1'), rel('cloud.1.1', 'cloud.2'), rel('cloud.1', 'cloud.2.1'), rel('cloud.1', 'cloud.2'),
|
|
163
|
+
// no same parent
|
|
164
|
+
rel('cloud.1.1', 'aws.1'), rel('cloud.1', 'aws.1'), rel('cloud', 'aws'))).toEqual([
|
|
165
|
+
'cloud -> aws',
|
|
166
|
+
'cloud.1 -> aws.1',
|
|
167
|
+
'cloud.1.1 -> aws.1',
|
|
168
|
+
'cloud.1 -> cloud.2',
|
|
169
|
+
'cloud.1 -> cloud.2.1',
|
|
170
|
+
'cloud.1.1 -> cloud.2',
|
|
171
|
+
'cloud.1.1 -> cloud.2.1'
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
174
|
+
it('should sort by ancestors', () => {
|
|
175
|
+
expect(sorted(
|
|
176
|
+
// same parent 2nd level
|
|
177
|
+
rel('cloud.api.1', 'cloud.api.2'), rel('cloud.ui.1', 'cloud.ui.2'), rel('cloud.ui.2', 'cloud.ui.1'),
|
|
178
|
+
// same ancestor cloud
|
|
179
|
+
rel('cloud.ui.1', 'cloud.api.1'), rel('cloud.ui.2', 'cloud.api.2'), rel('cloud.ui', 'cloud.api.1'),
|
|
180
|
+
// same parent 1st level
|
|
181
|
+
rel('cloud.ui', 'cloud.api'),
|
|
182
|
+
// no same parent
|
|
183
|
+
rel('cloud.api.1', 'aws.1'), rel('cloud.api', 'aws.1'), rel('cloud.api', 'aws'), rel('cloud.ui.1', 'aws.1'), rel('cloud.ui', 'aws'), rel('cloud', 'aws'))).toEqual([
|
|
184
|
+
'cloud -> aws',
|
|
185
|
+
'cloud.api -> aws',
|
|
186
|
+
'cloud.ui -> aws',
|
|
187
|
+
'cloud.api -> aws.1',
|
|
188
|
+
'cloud.api.1 -> aws.1',
|
|
189
|
+
'cloud.ui.1 -> aws.1',
|
|
190
|
+
'cloud.ui -> cloud.api',
|
|
191
|
+
'cloud.ui -> cloud.api.1',
|
|
192
|
+
'cloud.ui.1 -> cloud.api.1',
|
|
193
|
+
'cloud.ui.2 -> cloud.api.2',
|
|
194
|
+
'cloud.api.1 -> cloud.api.2',
|
|
195
|
+
'cloud.ui.1 -> cloud.ui.2',
|
|
196
|
+
'cloud.ui.2 -> cloud.ui.1'
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
it('should sort relations from example', () => {
|
|
200
|
+
expect(sorted(...relations)).toEqual([
|
|
201
|
+
'customer -> cloud.frontend.dashboard',
|
|
202
|
+
'support -> cloud.frontend.adminPanel',
|
|
203
|
+
'amazon.api -> cloud.backend.graphql',
|
|
204
|
+
'cloud.backend.storage -> amazon.s3',
|
|
205
|
+
'cloud.frontend.dashboard -> cloud.backend.graphql',
|
|
206
|
+
'cloud.frontend.adminPanel -> cloud.backend.graphql',
|
|
207
|
+
'cloud.backend.graphql -> cloud.backend.storage'
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@likec4/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://likec4.dev",
|
|
6
6
|
"author": "Denis Davydkov <denis@davydkov.com>",
|
|
@@ -12,15 +12,14 @@
|
|
|
12
12
|
"bugs": "https://github.com/likec4/likec4/issues",
|
|
13
13
|
"scripts": {
|
|
14
14
|
"typecheck": "tsc --noEmit",
|
|
15
|
-
"
|
|
16
|
-
"prepack": "tsc -p tsconfig.build.json",
|
|
15
|
+
"prepack": "tsc",
|
|
17
16
|
"lint": "run -T eslint src/ --fix",
|
|
18
17
|
"test": "run -T vitest run",
|
|
19
18
|
"clean": "run -T rimraf dist"
|
|
20
19
|
},
|
|
21
20
|
"files": [
|
|
22
21
|
"dist",
|
|
23
|
-
"
|
|
22
|
+
"src",
|
|
24
23
|
"!**/*.map"
|
|
25
24
|
],
|
|
26
25
|
"type": "module",
|
|
@@ -71,10 +70,11 @@
|
|
|
71
70
|
"type-fest": "^4.18.2"
|
|
72
71
|
},
|
|
73
72
|
"devDependencies": {
|
|
73
|
+
"@likec4/tsconfig": "1.2.1",
|
|
74
74
|
"@total-typescript/ts-reset": "^0.5.1",
|
|
75
75
|
"execa": "^9.1.0",
|
|
76
76
|
"typescript": "^5.4.5",
|
|
77
|
-
"vitest": "~1.5.
|
|
77
|
+
"vitest": "~1.5.3"
|
|
78
78
|
},
|
|
79
|
-
"packageManager": "yarn@4.
|
|
79
|
+
"packageManager": "yarn@4.3.0"
|
|
80
80
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ElementThemeColors, ElementThemeColorValues } from '../types/theme'
|
|
2
|
+
|
|
3
|
+
const blue = {
|
|
4
|
+
fill: '#3b82f6',
|
|
5
|
+
stroke: '#2563eb',
|
|
6
|
+
hiContrast: '#eff6ff',
|
|
7
|
+
loContrast: '#bfdbfe'
|
|
8
|
+
} satisfies ElementThemeColorValues
|
|
9
|
+
|
|
10
|
+
const sky = {
|
|
11
|
+
fill: '#0284c7',
|
|
12
|
+
stroke: '#0369a1',
|
|
13
|
+
hiContrast: '#f0f9ff',
|
|
14
|
+
loContrast: '#B6ECF7'
|
|
15
|
+
} satisfies ElementThemeColorValues
|
|
16
|
+
|
|
17
|
+
const slate = {
|
|
18
|
+
fill: '#64748b',
|
|
19
|
+
stroke: '#475569',
|
|
20
|
+
hiContrast: '#f8fafc',
|
|
21
|
+
loContrast: '#cbd5e1'
|
|
22
|
+
} satisfies ElementThemeColorValues
|
|
23
|
+
|
|
24
|
+
export const ElementColors = {
|
|
25
|
+
primary: blue,
|
|
26
|
+
blue,
|
|
27
|
+
secondary: sky,
|
|
28
|
+
sky,
|
|
29
|
+
muted: slate,
|
|
30
|
+
slate,
|
|
31
|
+
gray: {
|
|
32
|
+
// fill: colors.neutral[500],
|
|
33
|
+
// stroke: colors.neutral[600],
|
|
34
|
+
// hiContrast: colors.neutral[50],
|
|
35
|
+
// loContrast: colors.neutral[200],
|
|
36
|
+
fill: '#737373',
|
|
37
|
+
stroke: '#525252',
|
|
38
|
+
hiContrast: '#fafafa',
|
|
39
|
+
loContrast: '#d4d4d4'
|
|
40
|
+
},
|
|
41
|
+
red: {
|
|
42
|
+
// fill: colors.red[500],
|
|
43
|
+
// stroke: colors.red[600],
|
|
44
|
+
// hiContrast: colors.red[50],
|
|
45
|
+
// loContrast: colors.red[200],
|
|
46
|
+
fill: '#AC4D39',
|
|
47
|
+
// fill: '#b54548',
|
|
48
|
+
stroke: '#853A2D',
|
|
49
|
+
// hiContrast: '#fef2f2',
|
|
50
|
+
// loContrast: '#fecaca',
|
|
51
|
+
// hiContrast: '#191111', // colors.gray[900],
|
|
52
|
+
// loContrast: '#3b1219' // colors.gray[800],
|
|
53
|
+
hiContrast: '#FBD3CB',
|
|
54
|
+
// hiContrast: '#f8fafc',
|
|
55
|
+
// loContrast: '#fdd8d8' // radix black red 12
|
|
56
|
+
loContrast: '#FF977D'
|
|
57
|
+
},
|
|
58
|
+
green: {
|
|
59
|
+
fill: '#428a4f',
|
|
60
|
+
stroke: '#2d5d39',
|
|
61
|
+
hiContrast: '#f8fafc',
|
|
62
|
+
loContrast: '#c2f0c2'
|
|
63
|
+
},
|
|
64
|
+
amber: {
|
|
65
|
+
fill: '#A35829',
|
|
66
|
+
stroke: '#7E451D',
|
|
67
|
+
hiContrast: '#FFE0C2',
|
|
68
|
+
loContrast: '#FFA057'
|
|
69
|
+
},
|
|
70
|
+
indigo: {
|
|
71
|
+
// fill: colors.indigo[500],
|
|
72
|
+
// stroke: colors.indigo[600],
|
|
73
|
+
// hiContrast: colors.indigo[50],
|
|
74
|
+
// loContrast: colors.indigo[200],
|
|
75
|
+
fill: '#6366f1',
|
|
76
|
+
stroke: '#4f46e5',
|
|
77
|
+
hiContrast: '#eef2ff',
|
|
78
|
+
loContrast: '#c7d2fe'
|
|
79
|
+
}
|
|
80
|
+
} as const satisfies ElementThemeColors
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LikeC4Theme } from '../types/theme'
|
|
2
|
+
import { ElementColors } from './element'
|
|
3
|
+
import { RelationshipColors } from './relationships'
|
|
4
|
+
|
|
5
|
+
export const defaultTheme = {
|
|
6
|
+
elements: ElementColors,
|
|
7
|
+
relationships: RelationshipColors,
|
|
8
|
+
font: 'Helvetica',
|
|
9
|
+
shadow: '#0a0a0a'
|
|
10
|
+
} satisfies LikeC4Theme
|
|
11
|
+
|
|
12
|
+
export { ElementColors, RelationshipColors }
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { RelationshipThemeColors, RelationshipThemeColorValues } from '../types/theme'
|
|
2
|
+
|
|
3
|
+
const gray = {
|
|
4
|
+
lineColor: '#6E6E6E',
|
|
5
|
+
labelBgColor: '#18191b',
|
|
6
|
+
labelColor: '#C6C6C6'
|
|
7
|
+
} satisfies RelationshipThemeColorValues
|
|
8
|
+
const slate = {
|
|
9
|
+
lineColor: '#64748b', // 500
|
|
10
|
+
labelBgColor: '#0f172a', // 900
|
|
11
|
+
labelColor: '#cbd5e1' // 300
|
|
12
|
+
} satisfies RelationshipThemeColorValues
|
|
13
|
+
|
|
14
|
+
const blue = {
|
|
15
|
+
lineColor: '#3b82f6', // 500
|
|
16
|
+
labelBgColor: '#172554', // 950
|
|
17
|
+
labelColor: '#60a5fa' // 400
|
|
18
|
+
} satisfies RelationshipThemeColorValues
|
|
19
|
+
|
|
20
|
+
const sky = {
|
|
21
|
+
lineColor: '#0ea5e9', // 500
|
|
22
|
+
labelBgColor: '#082f49', // 950
|
|
23
|
+
labelColor: '#38bdf8' // 400
|
|
24
|
+
} satisfies RelationshipThemeColorValues
|
|
25
|
+
|
|
26
|
+
export const RelationshipColors = {
|
|
27
|
+
amber: {
|
|
28
|
+
lineColor: '#b45309',
|
|
29
|
+
labelBgColor: '#78350f',
|
|
30
|
+
labelColor: '#FFE0C2'
|
|
31
|
+
},
|
|
32
|
+
blue,
|
|
33
|
+
gray,
|
|
34
|
+
green: {
|
|
35
|
+
lineColor: '#15803d', // 700
|
|
36
|
+
labelBgColor: '#052e16', // 950
|
|
37
|
+
labelColor: '#22c55e' // 500
|
|
38
|
+
},
|
|
39
|
+
indigo: {
|
|
40
|
+
lineColor: '#6366f1', // 500
|
|
41
|
+
labelBgColor: '#1e1b4b', // 950
|
|
42
|
+
labelColor: '#818cf8' // 400
|
|
43
|
+
},
|
|
44
|
+
muted: slate,
|
|
45
|
+
primary: blue,
|
|
46
|
+
red: {
|
|
47
|
+
lineColor: '#AC4D39',
|
|
48
|
+
labelBgColor: '#b91c1c',
|
|
49
|
+
labelColor: '#FF977D'
|
|
50
|
+
},
|
|
51
|
+
secondary: sky,
|
|
52
|
+
sky,
|
|
53
|
+
slate
|
|
54
|
+
} satisfies RelationshipThemeColors
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
BaseError,
|
|
4
|
+
InvalidArgError,
|
|
5
|
+
InvalidModelError,
|
|
6
|
+
invariant,
|
|
7
|
+
InvariantError,
|
|
8
|
+
NonExhaustiveError,
|
|
9
|
+
normalizeError,
|
|
10
|
+
NullableError,
|
|
11
|
+
RelationRefError,
|
|
12
|
+
UnknownError
|
|
13
|
+
} from './index'
|
|
14
|
+
|
|
15
|
+
describe('errors', () => {
|
|
16
|
+
describe('normalizeError', () => {
|
|
17
|
+
it.each([
|
|
18
|
+
{ error: new BaseError('BaseError') },
|
|
19
|
+
{ error: new RelationRefError('RelationRefError') },
|
|
20
|
+
{ error: new UnknownError('UnknownError') },
|
|
21
|
+
{ error: new InvariantError('InvariantError') },
|
|
22
|
+
{ error: new NonExhaustiveError('NonExhaustiveError') },
|
|
23
|
+
{ error: new InvalidModelError('InvalidModelError') },
|
|
24
|
+
{ error: new NullableError('NullableError') },
|
|
25
|
+
{ error: new InvalidArgError('InvalidArgError') }
|
|
26
|
+
])('should return $error.name as-is', ({ error }) => {
|
|
27
|
+
expect(normalizeError(error)).toBe(error)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it.skip('should wrap an error', () => {
|
|
31
|
+
const cause = new Error('original test')
|
|
32
|
+
const error = normalizeError(cause)
|
|
33
|
+
expect(error).toBeInstanceOf(UnknownError)
|
|
34
|
+
expect(error).to.include({
|
|
35
|
+
name: 'UnknownError',
|
|
36
|
+
message: 'original test'
|
|
37
|
+
})
|
|
38
|
+
expect(error).to.have.property('cause').that.eq(cause)
|
|
39
|
+
expect(error)
|
|
40
|
+
.to.have.property('stack')
|
|
41
|
+
.that.is.a('string')
|
|
42
|
+
.and.includes('UnknownError: original test')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should wrap a string', () => {
|
|
46
|
+
const cause = 'error string'
|
|
47
|
+
const error = normalizeError(cause)
|
|
48
|
+
expect(error).toBeInstanceOf(UnknownError)
|
|
49
|
+
expect(error).to.include({
|
|
50
|
+
name: 'UnknownError',
|
|
51
|
+
message: cause
|
|
52
|
+
})
|
|
53
|
+
expect(error).not.to.have.property('cause')
|
|
54
|
+
expect(error).to.have.property('stack').that.is.a('string')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Fails on serializiation of AST
|
|
58
|
+
it.skip('should wrap an object', () => {
|
|
59
|
+
const cause = { foo: 'bar' }
|
|
60
|
+
const error = normalizeError(cause)
|
|
61
|
+
expect(error).toBeInstanceOf(UnknownError)
|
|
62
|
+
expect(error).to.include({
|
|
63
|
+
name: 'UnknownError',
|
|
64
|
+
message: '{"foo":"bar"}'
|
|
65
|
+
})
|
|
66
|
+
expect(error).not.to.have.property('cause')
|
|
67
|
+
expect(error).to.have.property('stack').that.is.a('string')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('invariant', () => {
|
|
73
|
+
it('should throw an error if the condition fails', () => {
|
|
74
|
+
expect(() => invariant(false)).toThrow('Invariant failed')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should throw an error if the condition is null', () => {
|
|
78
|
+
expect(() => invariant(null)).toThrow(InvariantError)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should not throw an error if the condition passes', () => {
|
|
82
|
+
expect(() => invariant(true)).not.toThrow()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should throw an error with the provided message if the condition fails', () => {
|
|
86
|
+
expect(() => invariant(false, 'test')).toThrow('test')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { CustomError } from 'ts-custom-error'
|
|
2
|
+
import { isString } from '../utils/guards'
|
|
3
|
+
|
|
4
|
+
export interface BaseErrorOptions {
|
|
5
|
+
cause?: unknown
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base class for all errors in the LikeC4 library.
|
|
10
|
+
*/
|
|
11
|
+
export class BaseError extends CustomError {
|
|
12
|
+
constructor(message: string, options?: BaseErrorOptions) {
|
|
13
|
+
super(message, options)
|
|
14
|
+
// Set name explicitly as minification can mangle class names
|
|
15
|
+
Object.defineProperty(this, 'name', { value: 'BaseError' })
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class InvalidArgError extends BaseError {
|
|
20
|
+
constructor(message: string, options?: BaseErrorOptions) {
|
|
21
|
+
super(message, options)
|
|
22
|
+
// Set name explicitly as minification can mangle class names
|
|
23
|
+
Object.defineProperty(this, 'name', { value: 'InvalidArgError' })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Unknown error, mosly probably a bug, unhandled case or coming from a third-party library.
|
|
29
|
+
*/
|
|
30
|
+
export class UnknownError extends BaseError {
|
|
31
|
+
constructor(message: string, options?: BaseErrorOptions) {
|
|
32
|
+
super(message, options)
|
|
33
|
+
// Set name explicitly as minification can mangle class names
|
|
34
|
+
Object.defineProperty(this, 'name', { value: 'UnknownError' })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class NullableError extends BaseError {
|
|
39
|
+
public constructor(message: string, options?: BaseErrorOptions) {
|
|
40
|
+
super(message, options)
|
|
41
|
+
// Set name explicitly as minification can mangle class names
|
|
42
|
+
Object.defineProperty(this, 'name', { value: 'NullableError' })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class InvariantError extends BaseError {
|
|
47
|
+
public constructor(message: string, options?: BaseErrorOptions) {
|
|
48
|
+
super(message, options)
|
|
49
|
+
// Set name explicitly as minification can mangle class names
|
|
50
|
+
Object.defineProperty(this, 'name', { value: 'InvariantError' })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Ensure that the value is NonNullable
|
|
55
|
+
// Mostly as safer `value!`
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
57
|
+
export function nonNullable<T>(value: T, message?: string): NonNullable<T> {
|
|
58
|
+
if (typeof value === 'undefined' || value == null) {
|
|
59
|
+
throw new NullableError(message ?? `Expected defined value, but received ${value}`)
|
|
60
|
+
}
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class RelationRefError extends BaseError {
|
|
65
|
+
public constructor(message: string, options?: BaseErrorOptions) {
|
|
66
|
+
super(message, options)
|
|
67
|
+
// Set name explicitly as minification can mangle class names
|
|
68
|
+
Object.defineProperty(this, 'name', { value: 'RelationRefError' })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function normalizeError(e: unknown): Error {
|
|
73
|
+
if (e instanceof BaseError || e instanceof Error) {
|
|
74
|
+
return e
|
|
75
|
+
}
|
|
76
|
+
const message = isString(e) ? e : String(e)
|
|
77
|
+
const error = new UnknownError(message)
|
|
78
|
+
try {
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
Error.captureStackTrace(error, normalizeError)
|
|
81
|
+
} catch {
|
|
82
|
+
// Ignore
|
|
83
|
+
}
|
|
84
|
+
return error
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Throw an error if the condition fails
|
|
88
|
+
// > Not providing an inline default argument for message as the result is smaller
|
|
89
|
+
export function invariant(
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
|
+
condition: any,
|
|
92
|
+
// Can provide a string, or a function that returns a string for cases where
|
|
93
|
+
// the message takes a fair amount of effort to compute
|
|
94
|
+
message?: string
|
|
95
|
+
): asserts condition {
|
|
96
|
+
if (condition) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
throw new InvariantError(message ?? 'Invariant failed')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class NonExhaustiveError extends BaseError {
|
|
103
|
+
public constructor(message: string, options?: BaseErrorOptions) {
|
|
104
|
+
super(message, options)
|
|
105
|
+
// Set name explicitly as minification can mangle class names
|
|
106
|
+
Object.defineProperty(this, 'name', { value: 'NonExhaustiveError' })
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function nonexhaustive(value: never): never {
|
|
111
|
+
throw new NonExhaustiveError(`NonExhaustive value: ${value}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class InvalidModelError extends BaseError {
|
|
115
|
+
public constructor(message: string, options?: BaseErrorOptions) {
|
|
116
|
+
super(message, options)
|
|
117
|
+
// Set name explicitly as minification can mangle class names
|
|
118
|
+
Object.defineProperty(this, 'name', { value: 'InvalidModelError' })
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/index.ts
ADDED
package/src/reset.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { IconUrl, NonEmptyArray } from './_common'
|
|
2
|
+
import type { Opaque } from './opaque'
|
|
3
|
+
import type { ThemeColor } from './theme'
|
|
4
|
+
|
|
5
|
+
// Full-qualified-name
|
|
6
|
+
export type Fqn = Opaque<string, 'Fqn'>
|
|
7
|
+
|
|
8
|
+
export function AsFqn(name: string, parent?: Fqn | null) {
|
|
9
|
+
return (parent ? parent + '.' + name : name) as Fqn
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const BorderStyles = ['solid', 'dashed', 'dotted', 'none'] as const
|
|
13
|
+
|
|
14
|
+
export type BorderStyle = typeof BorderStyles[number]
|
|
15
|
+
|
|
16
|
+
export type ElementKind = Opaque<string, 'ElementKind'>
|
|
17
|
+
export const ElementShapes = [
|
|
18
|
+
'rectangle',
|
|
19
|
+
'person',
|
|
20
|
+
'browser',
|
|
21
|
+
'mobile',
|
|
22
|
+
'cylinder',
|
|
23
|
+
'storage',
|
|
24
|
+
'queue'
|
|
25
|
+
] as const
|
|
26
|
+
|
|
27
|
+
export type ElementShape = typeof ElementShapes[number]
|
|
28
|
+
export const DefaultThemeColor = 'primary' satisfies ThemeColor
|
|
29
|
+
export const DefaultElementShape = 'rectangle' satisfies ElementShape
|
|
30
|
+
|
|
31
|
+
export interface ElementStyle {
|
|
32
|
+
border?: BorderStyle
|
|
33
|
+
// 0-100
|
|
34
|
+
opacity?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type Tag = Opaque<string, 'Tag'>
|
|
38
|
+
|
|
39
|
+
export interface TagSpec {
|
|
40
|
+
readonly id: Tag
|
|
41
|
+
readonly style: ElementStyle
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Element {
|
|
45
|
+
readonly id: Fqn
|
|
46
|
+
readonly kind: ElementKind
|
|
47
|
+
readonly title: string
|
|
48
|
+
readonly description: string | null
|
|
49
|
+
readonly technology: string | null
|
|
50
|
+
readonly tags: NonEmptyArray<Tag> | null
|
|
51
|
+
readonly links: NonEmptyArray<string> | null
|
|
52
|
+
readonly icon?: IconUrl
|
|
53
|
+
readonly shape?: ElementShape
|
|
54
|
+
readonly color?: ThemeColor
|
|
55
|
+
readonly style?: ElementStyle
|
|
56
|
+
}
|