@likec4/generators 1.31.0 → 1.32.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/d2/generate-d2.d.ts +3 -2
- package/dist/d2/generate-d2.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mmd/generate-mmd.d.ts +3 -2
- package/dist/mmd/generate-mmd.js +2 -1
- package/dist/model/generate-aux.d.ts +2 -0
- package/dist/model/generate-aux.js +66 -0
- package/dist/model/generate-likec4-model.d.ts +2 -2
- package/dist/model/generate-likec4-model.js +8 -10
- package/dist/puml/generate-puml.d.ts +2 -0
- package/dist/puml/generate-puml.js +176 -0
- package/dist/puml/index.d.ts +1 -0
- package/dist/puml/index.js +1 -0
- package/dist/react/generate-react-types.d.ts +2 -2
- package/dist/react/generate-react-types.js +14 -73
- package/dist/views-data-ts/generate-views-data.d.ts +9 -0
- package/package.json +7 -7
- package/src/__mocks__/data.ts +83 -83
- package/src/d2/__snapshots__/generate-d2.spec.ts.snap +78 -0
- package/src/d2/generate-d2.spec.ts +91 -4
- package/src/d2/generate-d2.ts +20 -15
- package/src/index.ts +1 -0
- package/src/mmd/generate-mmd.spec.ts +12 -4
- package/src/mmd/generate-mmd.ts +25 -20
- package/src/model/__snapshots__/aux.generate-valid-code.snap +56 -0
- package/src/model/__snapshots__/{likec4-model.snap → likec4.computed-model.snap} +342 -165
- package/src/model/__snapshots__/likec4.parsed-model.snap +671 -0
- package/src/model/generate-aux.spec.ts +65 -0
- package/src/model/generate-aux.ts +72 -0
- package/src/model/generate-likec4-model.spec.ts +34 -8
- package/src/model/generate-likec4-model.ts +13 -14
- package/src/puml/__snapshots__/generate-puml.spec.ts.snap +184 -0
- package/src/puml/generate-puml.spec.ts +26 -0
- package/src/puml/generate-puml.ts +264 -0
- package/src/puml/index.ts +1 -0
- package/src/react/__snapshots__/valid-code.snap +111 -0
- package/src/react/generate-react-types.spec.ts +67 -0
- package/src/react/generate-react-types.ts +16 -77
- package/src/views-data-ts/generate-views-data.ts +18 -9
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compareNatural,
|
|
3
|
+
sortNaturalByFqn,
|
|
4
|
+
} from '@likec4/core'
|
|
5
|
+
import { type AnyLikeC4Model } from '@likec4/core/model'
|
|
6
|
+
import { keys, map, pipe, values } from 'remeda'
|
|
7
|
+
|
|
8
|
+
function toUnion(elements: string[]) {
|
|
9
|
+
if (elements.length === 0) {
|
|
10
|
+
return 'never'
|
|
11
|
+
}
|
|
12
|
+
let union = elements
|
|
13
|
+
.sort(compareNatural)
|
|
14
|
+
.map(v => ` | ${JSON.stringify(v)}`)
|
|
15
|
+
return union.join('\n').trimStart()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function elementIdToUnion(_elements: Record<string, { id: string }>) {
|
|
19
|
+
const elements = values(_elements)
|
|
20
|
+
if (elements.length === 0) {
|
|
21
|
+
return 'never'
|
|
22
|
+
}
|
|
23
|
+
let union = pipe(
|
|
24
|
+
elements,
|
|
25
|
+
sortNaturalByFqn,
|
|
26
|
+
map(v => ` | ${JSON.stringify(v.id)}`),
|
|
27
|
+
)
|
|
28
|
+
return union.join('\n').trimStart()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function generateAux(model: AnyLikeC4Model) {
|
|
32
|
+
return `
|
|
33
|
+
import type { Aux, SpecAux } from '@likec4/core/types';
|
|
34
|
+
|
|
35
|
+
export type $Specs = SpecAux<
|
|
36
|
+
// Element kinds
|
|
37
|
+
${toUnion(keys(model.specification.elements))},
|
|
38
|
+
// Deployment kinds
|
|
39
|
+
${toUnion(keys(model.specification.deployments ?? {}))},
|
|
40
|
+
// Relationship kinds
|
|
41
|
+
${toUnion(keys(model.specification.relationships ?? {}))},
|
|
42
|
+
// Tags
|
|
43
|
+
${toUnion(keys(model.specification.tags ?? {}))},
|
|
44
|
+
// Metadata keys
|
|
45
|
+
${toUnion(model.specification.metadataKeys ?? [])}
|
|
46
|
+
>
|
|
47
|
+
|
|
48
|
+
export type $Aux = Aux<
|
|
49
|
+
${JSON.stringify(model.stage)},
|
|
50
|
+
// Elements
|
|
51
|
+
${elementIdToUnion(model.$data.elements)},
|
|
52
|
+
// Deployments
|
|
53
|
+
${elementIdToUnion(model.$data.deployments.elements)},
|
|
54
|
+
// Views
|
|
55
|
+
${toUnion(keys(model.$data.views))},
|
|
56
|
+
// Project ID
|
|
57
|
+
${JSON.stringify(model.projectId)},
|
|
58
|
+
$Specs
|
|
59
|
+
>
|
|
60
|
+
|
|
61
|
+
export type $ElementId = $Aux['ElementId']
|
|
62
|
+
export type $DeploymentId = $Aux['DeploymentId']
|
|
63
|
+
export type $ViewId = $Aux['ViewId']
|
|
64
|
+
|
|
65
|
+
export type $ElementKind = $Aux['ElementKind']
|
|
66
|
+
export type $RelationKind = $Aux['RelationKind']
|
|
67
|
+
export type $DeploymentKind = $Aux['DeploymentKind']
|
|
68
|
+
export type $Tag = $Aux['Tag']
|
|
69
|
+
export type $Tags = readonly $Aux['Tag'][]
|
|
70
|
+
export type $MetadataKey = $Aux['MetadataKey']
|
|
71
|
+
`.trimStart()
|
|
72
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { LikeC4Model } from '@likec4/core'
|
|
2
1
|
import { Builder } from '@likec4/core/builder'
|
|
3
|
-
import {
|
|
2
|
+
import { computeParsedModelData, viewsWithReadableEdges } from '@likec4/core/compute-view'
|
|
3
|
+
import { LikeC4Model } from '@likec4/core/model'
|
|
4
4
|
import { describe, it } from 'vitest'
|
|
5
5
|
import { generateLikeC4Model } from './generate-likec4-model'
|
|
6
6
|
|
|
@@ -53,6 +53,11 @@ const {
|
|
|
53
53
|
},
|
|
54
54
|
},
|
|
55
55
|
},
|
|
56
|
+
tags: {
|
|
57
|
+
internal: {},
|
|
58
|
+
external: {},
|
|
59
|
+
},
|
|
60
|
+
metadataKeys: ['key1'],
|
|
56
61
|
deployments: {
|
|
57
62
|
env: {},
|
|
58
63
|
zone: {},
|
|
@@ -70,11 +75,17 @@ const builder = b
|
|
|
70
75
|
mobile('mobile'),
|
|
71
76
|
),
|
|
72
77
|
component('auth'),
|
|
73
|
-
component('backend'
|
|
78
|
+
component('backend', {
|
|
79
|
+
metadata: {
|
|
80
|
+
key1: 'value1',
|
|
81
|
+
},
|
|
82
|
+
tags: ['external'],
|
|
83
|
+
}).with(
|
|
74
84
|
component('api'),
|
|
75
85
|
component('graphql'),
|
|
76
86
|
),
|
|
77
87
|
component('media', {
|
|
88
|
+
tags: ['internal'],
|
|
78
89
|
shape: 'storage',
|
|
79
90
|
}),
|
|
80
91
|
),
|
|
@@ -83,6 +94,9 @@ const builder = b
|
|
|
83
94
|
shape: 'storage',
|
|
84
95
|
}),
|
|
85
96
|
component('s3', {
|
|
97
|
+
metadata: {
|
|
98
|
+
key1: 'value2',
|
|
99
|
+
},
|
|
86
100
|
shape: 'storage',
|
|
87
101
|
}),
|
|
88
102
|
),
|
|
@@ -103,7 +117,13 @@ const builder = b
|
|
|
103
117
|
$m.rel('cloud.backend.api', 'aws.rds', 'reads/writes'),
|
|
104
118
|
$m.rel('cloud.backend.api', 'email', 'sends emails'),
|
|
105
119
|
$m.rel('cloud.media', 'aws.s3', 'uploads'),
|
|
106
|
-
$m.rel('email', 'customer',
|
|
120
|
+
$m.rel('email', 'customer', {
|
|
121
|
+
tags: ['external'],
|
|
122
|
+
title: 'sends emails',
|
|
123
|
+
metadata: {
|
|
124
|
+
key1: 'value3',
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
107
127
|
),
|
|
108
128
|
deployment(
|
|
109
129
|
node('customer').with(
|
|
@@ -148,11 +168,17 @@ const builder = b
|
|
|
148
168
|
),
|
|
149
169
|
),
|
|
150
170
|
)
|
|
151
|
-
const computed = viewsWithReadableEdges(computeViews(builder.build()))
|
|
152
|
-
const m = LikeC4Model.create(computed)
|
|
153
171
|
|
|
154
172
|
describe('generateLikeC4Model', () => {
|
|
155
|
-
it('
|
|
156
|
-
|
|
173
|
+
it('parsed-model', async ({ expect }) => {
|
|
174
|
+
const parsed = builder.build()
|
|
175
|
+
const m = LikeC4Model.create(parsed)
|
|
176
|
+
await expect(generateLikeC4Model(m)).toMatchFileSnapshot('__snapshots__/likec4.parsed-model.snap')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('computed-model', async ({ expect }) => {
|
|
180
|
+
const computed = viewsWithReadableEdges(computeParsedModelData(builder.build()))
|
|
181
|
+
const m = LikeC4Model.create(computed)
|
|
182
|
+
await expect(generateLikeC4Model(m)).toMatchFileSnapshot('__snapshots__/likec4.computed-model.snap')
|
|
157
183
|
})
|
|
158
184
|
})
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { LikeC4Model } from '@likec4/core/model'
|
|
2
2
|
import JSON5 from 'json5'
|
|
3
3
|
import { CompositeGeneratorNode, toString } from 'langium/generate'
|
|
4
|
+
import { capitalize } from 'remeda'
|
|
5
|
+
import { generateAux } from './generate-aux'
|
|
4
6
|
|
|
5
|
-
export function generateLikeC4Model(model: LikeC4Model) {
|
|
7
|
+
export function generateLikeC4Model(model: LikeC4Model<any>) {
|
|
6
8
|
const out = new CompositeGeneratorNode()
|
|
9
|
+
const aux = generateAux(model)
|
|
10
|
+
const ModelData = capitalize(model.stage) + 'LikeC4ModelData'
|
|
11
|
+
|
|
7
12
|
out.appendTemplate`
|
|
8
13
|
/* prettier-ignore-start */
|
|
9
14
|
/* eslint-disable */
|
|
@@ -13,19 +18,13 @@ export function generateLikeC4Model(model: LikeC4Model) {
|
|
|
13
18
|
* DO NOT EDIT MANUALLY!
|
|
14
19
|
******************************************************************************/
|
|
15
20
|
|
|
16
|
-
import { LikeC4Model } from 'likec4/model'
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
JSON5.stringify(model.$model, {
|
|
20
|
-
space: 2,
|
|
21
|
-
quote: '\'',
|
|
22
|
-
})
|
|
23
|
-
})
|
|
21
|
+
import { LikeC4Model } from '@likec4/core/model'
|
|
22
|
+
import type { ${ModelData} } from '@likec4/core/types'
|
|
23
|
+
${aux}
|
|
24
24
|
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export type LikeC4ViewId = LikeC4ModelTypes['ViewId']
|
|
25
|
+
export const likec4model: LikeC4Model<$Aux> = new LikeC4Model(<${ModelData}<$Aux>>(${
|
|
26
|
+
JSON5.stringify(model.$data, { space: 2, quote: '\'' })
|
|
27
|
+
} as unknown))
|
|
29
28
|
|
|
30
29
|
/* prettier-ignore-end */
|
|
31
30
|
`
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`generate puml - fakeComputedView 3 Levels 1`] = `
|
|
4
|
+
"@startuml
|
|
5
|
+
title "Context: Cloud"
|
|
6
|
+
top to bottom direction
|
|
7
|
+
|
|
8
|
+
hide stereotype
|
|
9
|
+
skinparam ranksep 60
|
|
10
|
+
skinparam nodesep 30
|
|
11
|
+
skinparam {
|
|
12
|
+
arrowFontSize 10
|
|
13
|
+
defaultTextAlignment center
|
|
14
|
+
wrapWidth 200
|
|
15
|
+
maxMessageSize 100
|
|
16
|
+
shadowing false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
skinparam rectangle<<Amazon>>{
|
|
20
|
+
BackgroundColor #3b82f6
|
|
21
|
+
FontColor #FFFFFF
|
|
22
|
+
BorderColor #3b82f6
|
|
23
|
+
}
|
|
24
|
+
skinparam rectangle<<Customer>>{
|
|
25
|
+
BackgroundColor #3b82f6
|
|
26
|
+
FontColor #FFFFFF
|
|
27
|
+
BorderColor #3b82f6
|
|
28
|
+
}
|
|
29
|
+
skinparam rectangle<<Support>>{
|
|
30
|
+
BackgroundColor #3b82f6
|
|
31
|
+
FontColor #FFFFFF
|
|
32
|
+
BorderColor #3b82f6
|
|
33
|
+
}
|
|
34
|
+
skinparam rectangle<<CloudBackendGraphql>>{
|
|
35
|
+
BackgroundColor #3b82f6
|
|
36
|
+
FontColor #FFFFFF
|
|
37
|
+
BorderColor #3b82f6
|
|
38
|
+
}
|
|
39
|
+
skinparam rectangle<<CloudBackendStorage>>{
|
|
40
|
+
BackgroundColor #3b82f6
|
|
41
|
+
FontColor #FFFFFF
|
|
42
|
+
BorderColor #3b82f6
|
|
43
|
+
}
|
|
44
|
+
skinparam rectangle<<CloudFrontendAdminPanel>>{
|
|
45
|
+
BackgroundColor #3b82f6
|
|
46
|
+
FontColor #FFFFFF
|
|
47
|
+
BorderColor #3b82f6
|
|
48
|
+
}
|
|
49
|
+
skinparam rectangle<<CloudFrontendDashboard>>{
|
|
50
|
+
BackgroundColor #3b82f6
|
|
51
|
+
FontColor #FFFFFF
|
|
52
|
+
BorderColor #3b82f6
|
|
53
|
+
}
|
|
54
|
+
rectangle "==amazon" <<Amazon>> as Amazon
|
|
55
|
+
rectangle "cloud" <<Cloud>> as Cloud {
|
|
56
|
+
skinparam RectangleBorderColor<<Cloud>> #3b82f6
|
|
57
|
+
skinparam RectangleFontColor<<Cloud>> #3b82f6
|
|
58
|
+
skinparam RectangleBorderStyle<<Cloud>> dashed
|
|
59
|
+
|
|
60
|
+
rectangle "backend" <<CloudBackend>> as CloudBackend {
|
|
61
|
+
skinparam RectangleBorderColor<<CloudBackend>> #3b82f6
|
|
62
|
+
skinparam RectangleFontColor<<CloudBackend>> #3b82f6
|
|
63
|
+
skinparam RectangleBorderStyle<<CloudBackend>> dashed
|
|
64
|
+
|
|
65
|
+
rectangle "==graphql" <<CloudBackendGraphql>> as CloudBackendGraphql
|
|
66
|
+
rectangle "==storage" <<CloudBackendStorage>> as CloudBackendStorage
|
|
67
|
+
}
|
|
68
|
+
rectangle "==adminPanel" <<CloudFrontendAdminPanel>> as CloudFrontendAdminPanel
|
|
69
|
+
rectangle "==dashboard" <<CloudFrontendDashboard>> as CloudFrontendDashboard
|
|
70
|
+
}
|
|
71
|
+
rectangle "==customer" <<Customer>> as Customer
|
|
72
|
+
rectangle "==support" <<Support>> as Support
|
|
73
|
+
|
|
74
|
+
CloudFrontendDashboard .[#777777,thickness=2].> CloudBackendGraphql
|
|
75
|
+
CloudFrontendAdminPanel .[#777777,thickness=2].> CloudBackendGraphql
|
|
76
|
+
CloudBackendStorage .[#777777,thickness=2].> Amazon
|
|
77
|
+
CloudBackendGraphql .[#777777,thickness=2].> CloudBackendStorage
|
|
78
|
+
Support .[#777777,thickness=2].> CloudFrontendAdminPanel
|
|
79
|
+
Customer .[#777777,thickness=2].> CloudFrontendDashboard
|
|
80
|
+
@enduml
|
|
81
|
+
"
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
exports[`generate puml - fakeDiagram 1`] = `
|
|
85
|
+
"@startuml
|
|
86
|
+
title "fakeView"
|
|
87
|
+
top to bottom direction
|
|
88
|
+
|
|
89
|
+
hide stereotype
|
|
90
|
+
skinparam ranksep 60
|
|
91
|
+
skinparam nodesep 30
|
|
92
|
+
skinparam {
|
|
93
|
+
arrowFontSize 10
|
|
94
|
+
defaultTextAlignment center
|
|
95
|
+
wrapWidth 200
|
|
96
|
+
maxMessageSize 100
|
|
97
|
+
shadowing false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
skinparam rectangle<<Amazon>>{
|
|
101
|
+
BackgroundColor #3b82f6
|
|
102
|
+
FontColor #FFFFFF
|
|
103
|
+
BorderColor #3b82f6
|
|
104
|
+
}
|
|
105
|
+
skinparam rectangle<<Customer>>{
|
|
106
|
+
BackgroundColor #3b82f6
|
|
107
|
+
FontColor #FFFFFF
|
|
108
|
+
BorderColor #3b82f6
|
|
109
|
+
}
|
|
110
|
+
skinparam rectangle<<Support>>{
|
|
111
|
+
BackgroundColor #3b82f6
|
|
112
|
+
FontColor #FFFFFF
|
|
113
|
+
BorderColor #3b82f6
|
|
114
|
+
}
|
|
115
|
+
skinparam rectangle<<CloudBackend>>{
|
|
116
|
+
BackgroundColor #3b82f6
|
|
117
|
+
FontColor #FFFFFF
|
|
118
|
+
BorderColor #3b82f6
|
|
119
|
+
}
|
|
120
|
+
skinparam rectangle<<CloudFrontend>>{
|
|
121
|
+
BackgroundColor #3b82f6
|
|
122
|
+
FontColor #FFFFFF
|
|
123
|
+
BorderColor #3b82f6
|
|
124
|
+
}
|
|
125
|
+
rectangle "==amazon" <<Amazon>> as Amazon
|
|
126
|
+
rectangle "cloud" <<Cloud>> as Cloud {
|
|
127
|
+
skinparam RectangleBorderColor<<Cloud>> #3b82f6
|
|
128
|
+
skinparam RectangleFontColor<<Cloud>> #3b82f6
|
|
129
|
+
skinparam RectangleBorderStyle<<Cloud>> dashed
|
|
130
|
+
|
|
131
|
+
rectangle "==backend" <<CloudBackend>> as CloudBackend
|
|
132
|
+
rectangle "==frontend" <<CloudFrontend>> as CloudFrontend
|
|
133
|
+
}
|
|
134
|
+
rectangle "==customer" <<Customer>> as Customer
|
|
135
|
+
rectangle "==support" <<Support>> as Support
|
|
136
|
+
|
|
137
|
+
CloudFrontend .[#777777,thickness=2].> CloudBackend : "<color:#777777>requests<color:#777777>"
|
|
138
|
+
CloudBackend .[#777777,thickness=2].> Amazon
|
|
139
|
+
Support .[#777777,thickness=2].> CloudFrontend
|
|
140
|
+
Customer .[#777777,thickness=2].> CloudFrontend : "<color:#777777>opens<color:#777777>"
|
|
141
|
+
@enduml
|
|
142
|
+
"
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
exports[`generate puml - fakeDiagram2 1`] = `
|
|
146
|
+
"@startuml
|
|
147
|
+
title "frontend"
|
|
148
|
+
top to bottom direction
|
|
149
|
+
|
|
150
|
+
hide stereotype
|
|
151
|
+
skinparam ranksep 60
|
|
152
|
+
skinparam nodesep 30
|
|
153
|
+
skinparam {
|
|
154
|
+
arrowFontSize 10
|
|
155
|
+
defaultTextAlignment center
|
|
156
|
+
wrapWidth 200
|
|
157
|
+
maxMessageSize 100
|
|
158
|
+
shadowing false
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
skinparam rectangle<<Client>>{
|
|
162
|
+
BackgroundColor #3b82f6
|
|
163
|
+
FontColor #FFFFFF
|
|
164
|
+
BorderColor #3b82f6
|
|
165
|
+
}
|
|
166
|
+
skinparam rectangle<<SystemBackend>>{
|
|
167
|
+
BackgroundColor #3b82f6
|
|
168
|
+
FontColor #FFFFFF
|
|
169
|
+
BorderColor #3b82f6
|
|
170
|
+
}
|
|
171
|
+
skinparam rectangle<<SystemFrontend>>{
|
|
172
|
+
BackgroundColor #3b82f6
|
|
173
|
+
FontColor #FFFFFF
|
|
174
|
+
BorderColor #3b82f6
|
|
175
|
+
}
|
|
176
|
+
rectangle "==client" <<Client>> as Client
|
|
177
|
+
rectangle "==backend" <<SystemBackend>> as SystemBackend
|
|
178
|
+
rectangle "==frontend" <<SystemFrontend>> as SystemFrontend
|
|
179
|
+
|
|
180
|
+
SystemFrontend .[#777777,thickness=2].> SystemBackend : "<color:#777777>requests<color:#777777>"
|
|
181
|
+
Client .[#777777,thickness=2].> SystemFrontend : "<color:#777777>opens<color:#777777>"
|
|
182
|
+
@enduml
|
|
183
|
+
"
|
|
184
|
+
`;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { LikeC4ViewModel } from '@likec4/core/model'
|
|
2
|
+
import type { aux, ProcessedView } from '@likec4/core/types'
|
|
3
|
+
import { expect, test, vi } from 'vitest'
|
|
4
|
+
import { fakeComputedView3Levels, fakeDiagram, fakeDiagram2 } from '../__mocks__/data'
|
|
5
|
+
import { generatePuml } from './generate-puml'
|
|
6
|
+
|
|
7
|
+
const mockViewModel = vi.fn(function($view: ProcessedView) {
|
|
8
|
+
return {
|
|
9
|
+
$view,
|
|
10
|
+
$model: {
|
|
11
|
+
specification: {},
|
|
12
|
+
},
|
|
13
|
+
} as unknown as LikeC4ViewModel<aux.Unknown>
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('generate puml - fakeDiagram', () => {
|
|
17
|
+
expect(generatePuml(mockViewModel(fakeDiagram))).toMatchSnapshot()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('generate puml - fakeDiagram2', () => {
|
|
21
|
+
expect(generatePuml(mockViewModel(fakeDiagram2))).toMatchSnapshot()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('generate puml - fakeComputedView 3 Levels', () => {
|
|
25
|
+
expect(generatePuml(mockViewModel(fakeComputedView3Levels))).toMatchSnapshot()
|
|
26
|
+
})
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import type { aux, LikeC4ViewModel } from '@likec4/core/model'
|
|
2
|
+
import type {
|
|
3
|
+
ComputedEdge,
|
|
4
|
+
ComputedNode,
|
|
5
|
+
ElementThemeColorValues,
|
|
6
|
+
KeysOf,
|
|
7
|
+
NodeId,
|
|
8
|
+
ProcessedView,
|
|
9
|
+
RelationshipThemeColorValues,
|
|
10
|
+
ThemeColorValues,
|
|
11
|
+
} from '@likec4/core/types'
|
|
12
|
+
import { CompositeGeneratorNode, joinToNode, NL, toString } from 'langium/generate'
|
|
13
|
+
import { isNullish as isNil } from 'remeda'
|
|
14
|
+
|
|
15
|
+
const capitalizeFirstLetter = (value: string) => value.charAt(0).toLocaleUpperCase() + value.slice(1)
|
|
16
|
+
|
|
17
|
+
const fqnName = (nodeId: string): string => nodeId.split('.').map(capitalizeFirstLetter).join('')
|
|
18
|
+
|
|
19
|
+
const nodeName = (node: ComputedNode): string => {
|
|
20
|
+
return fqnName(node.parent ? node.id.slice(node.parent.length + 1) : node.id)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const pumlColor = (
|
|
24
|
+
color: string | undefined,
|
|
25
|
+
customColorProvider: (colorKey: string) => string | undefined,
|
|
26
|
+
defaultColor: string = '#3b82f6',
|
|
27
|
+
) => {
|
|
28
|
+
switch (color) {
|
|
29
|
+
case 'blue':
|
|
30
|
+
case 'primary': {
|
|
31
|
+
return '#3b82f6'
|
|
32
|
+
}
|
|
33
|
+
case 'amber': {
|
|
34
|
+
return '#a35829'
|
|
35
|
+
}
|
|
36
|
+
case 'gray': {
|
|
37
|
+
return '#737373'
|
|
38
|
+
}
|
|
39
|
+
case 'green': {
|
|
40
|
+
return '#428a4f'
|
|
41
|
+
}
|
|
42
|
+
case 'indigo': {
|
|
43
|
+
return '#6366f1'
|
|
44
|
+
}
|
|
45
|
+
case 'slate':
|
|
46
|
+
case 'muted': {
|
|
47
|
+
return '#64748b'
|
|
48
|
+
}
|
|
49
|
+
case 'red': {
|
|
50
|
+
return '#ac4d39'
|
|
51
|
+
}
|
|
52
|
+
case 'sky':
|
|
53
|
+
case 'secondary': {
|
|
54
|
+
return '#0284c7'
|
|
55
|
+
}
|
|
56
|
+
case null:
|
|
57
|
+
case undefined: {
|
|
58
|
+
return defaultColor
|
|
59
|
+
}
|
|
60
|
+
default:
|
|
61
|
+
return customColorProvider(color) || color
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const pumlDirection = ({ autoLayout }: ProcessedView) => {
|
|
66
|
+
switch (autoLayout.direction) {
|
|
67
|
+
case 'TB': {
|
|
68
|
+
return 'top to bottom'
|
|
69
|
+
}
|
|
70
|
+
case 'BT': {
|
|
71
|
+
console.warn('Bottom to top direction is not supported. Defaulting to top to bottom.')
|
|
72
|
+
return 'top to bottom'
|
|
73
|
+
}
|
|
74
|
+
case 'LR': {
|
|
75
|
+
return 'left to right'
|
|
76
|
+
}
|
|
77
|
+
case 'RL': {
|
|
78
|
+
console.warn('Right to left direction is not supported. Defaulting to left to right.')
|
|
79
|
+
return 'left to right'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const pumlShape = ({ shape }: ComputedNode) => {
|
|
85
|
+
switch (shape) {
|
|
86
|
+
case 'queue':
|
|
87
|
+
case 'rectangle':
|
|
88
|
+
case 'person': {
|
|
89
|
+
return shape
|
|
90
|
+
}
|
|
91
|
+
case 'storage':
|
|
92
|
+
case 'cylinder': {
|
|
93
|
+
return 'database' as const
|
|
94
|
+
}
|
|
95
|
+
case 'mobile':
|
|
96
|
+
case 'browser': {
|
|
97
|
+
return 'rectangle' as const
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const escapeLabel = (label: string | null | undefined) => isNil(label) ? null : JSON.stringify(label).slice(1, -1)
|
|
103
|
+
|
|
104
|
+
export function generatePuml(viewmodel: LikeC4ViewModel<aux.Unknown>) {
|
|
105
|
+
const view = viewmodel.$view
|
|
106
|
+
const customColorDefinitions = viewmodel.$model.specification.customColors ?? {}
|
|
107
|
+
const { nodes, edges } = view
|
|
108
|
+
const customColors = new Map<string, ThemeColorValues>(Object.entries(customColorDefinitions))
|
|
109
|
+
const elemntColorProvider = (key: KeysOf<ElementThemeColorValues>) => (colorKey: string) =>
|
|
110
|
+
customColors.get(colorKey)?.elements[key]
|
|
111
|
+
const relationshipsColorProvider = (key: KeysOf<RelationshipThemeColorValues>) => (colorKey: string) =>
|
|
112
|
+
customColors.get(colorKey)?.relationships[key]
|
|
113
|
+
const names = new Map<NodeId, string>()
|
|
114
|
+
|
|
115
|
+
const printHeader = () => {
|
|
116
|
+
return new CompositeGeneratorNode()
|
|
117
|
+
.append('title "', view.title || view.id, '"', NL)
|
|
118
|
+
.append(pumlDirection(view), ' direction', NL)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const printTheme = () => {
|
|
122
|
+
return new CompositeGeneratorNode()
|
|
123
|
+
.append('hide stereotype', NL)
|
|
124
|
+
.append('skinparam ranksep ', '60', NL)
|
|
125
|
+
.append('skinparam nodesep ', '30', NL)
|
|
126
|
+
.append('skinparam {', NL)
|
|
127
|
+
.indent({
|
|
128
|
+
indentedChildren: indent =>
|
|
129
|
+
indent
|
|
130
|
+
.append('arrowFontSize ', '10', NL)
|
|
131
|
+
.append('defaultTextAlignment ', 'center', NL)
|
|
132
|
+
.append('wrapWidth ', '200', NL)
|
|
133
|
+
.append('maxMessageSize ', '100', NL)
|
|
134
|
+
.append('shadowing ', 'false', NL),
|
|
135
|
+
indentation: 2,
|
|
136
|
+
})
|
|
137
|
+
.append('}', NL)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const printStereotypes = (node: ComputedNode): CompositeGeneratorNode => {
|
|
141
|
+
const shape = pumlShape(node)
|
|
142
|
+
const fqn = fqnName(node.id)
|
|
143
|
+
|
|
144
|
+
return new CompositeGeneratorNode()
|
|
145
|
+
.append('skinparam ', shape, '<<', fqn, '>>', '{', NL)
|
|
146
|
+
.indent({
|
|
147
|
+
indentedChildren: indent =>
|
|
148
|
+
indent
|
|
149
|
+
.append('BackgroundColor ', pumlColor(node.color, elemntColorProvider('fill')), NL)
|
|
150
|
+
.append(
|
|
151
|
+
'FontColor ',
|
|
152
|
+
customColors.has(node.color)
|
|
153
|
+
? pumlColor(node.color, elemntColorProvider('hiContrast'))
|
|
154
|
+
: '#FFFFFF',
|
|
155
|
+
NL,
|
|
156
|
+
)
|
|
157
|
+
.append('BorderColor ', pumlColor(node.color, elemntColorProvider('stroke')), NL),
|
|
158
|
+
indentation: 2,
|
|
159
|
+
})
|
|
160
|
+
.append('}', NL)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const printNode = (node: ComputedNode): CompositeGeneratorNode => {
|
|
164
|
+
const shape = pumlShape(node)
|
|
165
|
+
const fqn = fqnName(node.id)
|
|
166
|
+
const label = escapeLabel(node.title) || nodeName(node)
|
|
167
|
+
const tech = escapeLabel(node.technology)
|
|
168
|
+
names.set(node.id, fqn)
|
|
169
|
+
|
|
170
|
+
return new CompositeGeneratorNode()
|
|
171
|
+
.append(shape, ' ')
|
|
172
|
+
.append('"')
|
|
173
|
+
.append('==', label)
|
|
174
|
+
.appendIf(!!tech, '\\n', '<size:10>[', tech!, ']</size>')
|
|
175
|
+
.appendIf(!!node.description, '\\n\\n', escapeLabel(node.description)!)
|
|
176
|
+
.append('"', ' <<', fqn, '>> ', 'as ', fqn, NL)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const printBoundary = (node: ComputedNode): CompositeGeneratorNode => {
|
|
180
|
+
const label = escapeLabel(node.title) || nodeName(node)
|
|
181
|
+
const fqn = fqnName(node.id)
|
|
182
|
+
names.set(node.id, fqn)
|
|
183
|
+
|
|
184
|
+
return new CompositeGeneratorNode()
|
|
185
|
+
.append('rectangle "', label, '" <<', fqn, '>> as ', fqn, ' {', NL)
|
|
186
|
+
.indent({
|
|
187
|
+
indentedChildren: indent =>
|
|
188
|
+
indent
|
|
189
|
+
.append(
|
|
190
|
+
'skinparam ',
|
|
191
|
+
'RectangleBorderColor<<',
|
|
192
|
+
fqn,
|
|
193
|
+
'>> ',
|
|
194
|
+
pumlColor(node.color, elemntColorProvider('fill')),
|
|
195
|
+
NL,
|
|
196
|
+
)
|
|
197
|
+
.append(
|
|
198
|
+
'skinparam ',
|
|
199
|
+
'RectangleFontColor<<',
|
|
200
|
+
fqn,
|
|
201
|
+
'>> ',
|
|
202
|
+
pumlColor(node.color, elemntColorProvider('fill')),
|
|
203
|
+
NL,
|
|
204
|
+
)
|
|
205
|
+
.append('skinparam ', 'RectangleBorderStyle<<', fqn, '>> ', 'dashed', NL, NL)
|
|
206
|
+
.append(joinToNode(
|
|
207
|
+
nodes.filter(n => n.parent === node.id),
|
|
208
|
+
c => c.children.length > 0 ? printBoundary(c) : printNode(c),
|
|
209
|
+
)),
|
|
210
|
+
indentation: 2,
|
|
211
|
+
})
|
|
212
|
+
.append('}', NL)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const printEdge = (edge: ComputedEdge): CompositeGeneratorNode => {
|
|
216
|
+
const tech = escapeLabel(edge.technology) || ''
|
|
217
|
+
const label = escapeLabel(edge.label) || ''
|
|
218
|
+
const color = pumlColor(edge.color, relationshipsColorProvider('lineColor'), '#777777')
|
|
219
|
+
|
|
220
|
+
const colorTag = (color: string) => `<color:${color}>`
|
|
221
|
+
|
|
222
|
+
return new CompositeGeneratorNode()
|
|
223
|
+
.append(names.get(edge.source), ' .[', color, ',thickness=2].> ', names.get(edge.target))
|
|
224
|
+
.appendIf(!!(label || tech), ' : "', colorTag(color))
|
|
225
|
+
.appendIf(!!label, label, colorTag(color))
|
|
226
|
+
.appendIf(!!(label && tech), '\\n')
|
|
227
|
+
.appendIf(!!tech, colorTag(color), '<size:8>[', tech, ']</size>')
|
|
228
|
+
.appendIf(!!(label || tech), '"')
|
|
229
|
+
.append(NL)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return toString(
|
|
233
|
+
new CompositeGeneratorNode()
|
|
234
|
+
.append('@startuml', NL)
|
|
235
|
+
.append(printHeader(), NL)
|
|
236
|
+
.append(printTheme(), NL)
|
|
237
|
+
.append(
|
|
238
|
+
joinToNode(
|
|
239
|
+
nodes.filter(n => n.children.length == 0),
|
|
240
|
+
n => printStereotypes(n),
|
|
241
|
+
{
|
|
242
|
+
appendNewLineIfNotEmpty: true,
|
|
243
|
+
},
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
.append(
|
|
247
|
+
joinToNode(
|
|
248
|
+
nodes.filter(n => isNil(n.parent)),
|
|
249
|
+
n => n.children.length > 0 ? printBoundary(n) : printNode(n),
|
|
250
|
+
{
|
|
251
|
+
appendNewLineIfNotEmpty: true,
|
|
252
|
+
},
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
.appendIf(
|
|
256
|
+
edges.length > 0,
|
|
257
|
+
NL,
|
|
258
|
+
joinToNode(edges, e => printEdge(e), {
|
|
259
|
+
appendNewLineIfNotEmpty: true,
|
|
260
|
+
}),
|
|
261
|
+
)
|
|
262
|
+
.append(`@enduml`, NL),
|
|
263
|
+
)
|
|
264
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './generate-puml'
|