@likec4/generators 1.52.0 → 1.53.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.
@@ -0,0 +1,390 @@
1
+ import {
2
+ hasProp,
3
+ invariant,
4
+ nonexhaustive,
5
+ } from '@likec4/core'
6
+ import { CompositeGeneratorNode, NL } from 'langium/generate'
7
+ import { hasAtLeast, values } from 'remeda'
8
+ import { schemas } from '../schemas'
9
+ import {
10
+ type Op,
11
+ body,
12
+ foreach,
13
+ foreachNewLine,
14
+ guard,
15
+ indent,
16
+ inlineText,
17
+ lines,
18
+ merge,
19
+ print,
20
+ printProperty,
21
+ property,
22
+ select,
23
+ separateComma,
24
+ space,
25
+ spaceBetween,
26
+ withctx,
27
+ zodOp,
28
+ } from './base'
29
+ import { expression } from './expressions'
30
+ import {
31
+ colorProperty,
32
+ descriptionProperty,
33
+ linksProperty,
34
+ notationProperty,
35
+ notesProperty,
36
+ styleProperties,
37
+ tagsProperty,
38
+ technologyProperty,
39
+ titleProperty,
40
+ } from './properties'
41
+
42
+ const viewTitleProperty = <A extends { title?: string | null | undefined }>(): Op<A> =>
43
+ property(
44
+ 'title',
45
+ spaceBetween(
46
+ print('title'),
47
+ inlineText(),
48
+ ),
49
+ )
50
+
51
+ export const viewRulePredicate = zodOp(schemas.views.viewRulePredicate)(({ ctx, exec }) => {
52
+ let exprs
53
+ let type
54
+ if ('include' in ctx) {
55
+ exprs = ctx.include
56
+ type = 'include'
57
+ } else if ('exclude' in ctx) {
58
+ exprs = ctx.exclude
59
+ type = 'exclude'
60
+ }
61
+ invariant(exprs && type, 'Invalid view rule predicate')
62
+
63
+ if (!hasAtLeast(exprs, 1)) {
64
+ return
65
+ }
66
+
67
+ const isMultiple = hasAtLeast(exprs, 2)
68
+
69
+ const exprOp = withctx(exprs)(
70
+ foreach(
71
+ expression(),
72
+ separateComma(isMultiple),
73
+ ),
74
+ )
75
+
76
+ exec(
77
+ ctx,
78
+ merge(
79
+ print(type),
80
+ ...(isMultiple ? [indent(exprOp)] : [space(), exprOp]),
81
+ ),
82
+ )
83
+ })
84
+
85
+ export const viewRuleStyle = zodOp(schemas.views.viewRuleStyle)(
86
+ spaceBetween(
87
+ print('style'),
88
+ property(
89
+ 'targets',
90
+ foreach(
91
+ expression(),
92
+ separateComma(),
93
+ ),
94
+ ),
95
+ body('{', '}')(
96
+ notationProperty(),
97
+ property('style', styleProperties()),
98
+ ),
99
+ ),
100
+ )
101
+ export const viewRuleGroup = zodOp(schemas.views.viewRuleGroup)(({ ctx, exec }) => {
102
+ throw new Error('not implemented')
103
+ })
104
+ export const viewRuleGlobalStyle = zodOp(schemas.views.viewRuleGlobalStyle)(({ ctx, exec }) => {
105
+ throw new Error('not implemented')
106
+ })
107
+ export const viewRuleGlobalPredicate = zodOp(schemas.views.viewRuleGlobalPredicate)(({ ctx, exec }) => {
108
+ throw new Error('not implemented')
109
+ })
110
+
111
+ const mapping = {
112
+ 'TB': 'TopBottom',
113
+ 'BT': 'BottomTop',
114
+ 'LR': 'LeftRight',
115
+ 'RL': 'RightLeft',
116
+ } as const
117
+ export const viewRuleAutoLayout = zodOp(schemas.views.viewRuleAutoLayout)(
118
+ spaceBetween(
119
+ print('autoLayout'),
120
+ property(
121
+ 'direction',
122
+ print(v => mapping[v]),
123
+ ),
124
+ guard(
125
+ hasProp('rankSep'),
126
+ spaceBetween(
127
+ printProperty('rankSep'),
128
+ printProperty('nodeSep'),
129
+ ),
130
+ ),
131
+ ),
132
+ )
133
+
134
+ export const viewRuleRank = zodOp(schemas.views.viewRuleRank)(({ ctx, exec }) => {
135
+ throw new Error('not implemented')
136
+ })
137
+
138
+ export const elementViewRule = zodOp(schemas.views.elementViewRule)(
139
+ ({ ctx, exec }) => {
140
+ if ('include' in ctx || 'exclude' in ctx) {
141
+ return exec(ctx, viewRulePredicate())
142
+ }
143
+ if ('groupRules' in ctx) {
144
+ return exec(ctx, viewRuleGroup())
145
+ }
146
+ if ('rank' in ctx) {
147
+ return exec(ctx, viewRuleRank())
148
+ }
149
+ if ('direction' in ctx) {
150
+ return exec(ctx, viewRuleAutoLayout())
151
+ }
152
+ if ('styleId' in ctx) {
153
+ return exec(ctx, viewRuleGlobalStyle())
154
+ }
155
+ if ('predicateId' in ctx) {
156
+ return exec(ctx, viewRuleGlobalPredicate())
157
+ }
158
+ if ('targets' in ctx && 'style' in ctx) {
159
+ return exec(ctx, viewRuleStyle())
160
+ }
161
+ nonexhaustive(ctx)
162
+ },
163
+ )
164
+
165
+ export const elementView = zodOp(schemas.views.elementView.partial({ _type: true }))(
166
+ spaceBetween(
167
+ print('view'),
168
+ print(v => v.id),
169
+ property(
170
+ 'viewOf',
171
+ spaceBetween(
172
+ print('of'),
173
+ print(),
174
+ ),
175
+ ),
176
+ body(
177
+ lines(2)(
178
+ // Properties on each line
179
+ lines(
180
+ tagsProperty(),
181
+ viewTitleProperty(),
182
+ descriptionProperty(),
183
+ linksProperty(),
184
+ ),
185
+ property(
186
+ 'rules',
187
+ foreachNewLine(
188
+ elementViewRule(),
189
+ ),
190
+ ),
191
+ ),
192
+ ),
193
+ ),
194
+ )
195
+
196
+ // --- Deployment View ---
197
+
198
+ export const deploymentViewRule = zodOp(schemas.views.deploymentViewRule)(
199
+ ({ ctx, exec }) => {
200
+ if ('include' in ctx || 'exclude' in ctx) {
201
+ return exec(ctx, viewRulePredicate())
202
+ }
203
+ if ('direction' in ctx) {
204
+ return exec(ctx, viewRuleAutoLayout())
205
+ }
206
+ if ('styleId' in ctx) {
207
+ return exec(ctx, viewRuleGlobalStyle())
208
+ }
209
+ if ('predicateId' in ctx) {
210
+ return exec(ctx, viewRuleGlobalPredicate())
211
+ }
212
+ if ('targets' in ctx && 'style' in ctx) {
213
+ return exec(ctx, viewRuleStyle())
214
+ }
215
+ nonexhaustive(ctx)
216
+ },
217
+ )
218
+
219
+ export const deploymentView = zodOp(schemas.views.deploymentView.partial({ _type: true }))(
220
+ spaceBetween(
221
+ print('deployment view'),
222
+ print(v => v.id),
223
+ body(
224
+ lines(2)(
225
+ // Properties on each line
226
+ lines(
227
+ tagsProperty(),
228
+ viewTitleProperty(),
229
+ descriptionProperty(),
230
+ linksProperty(),
231
+ ),
232
+ property(
233
+ 'rules',
234
+ foreachNewLine(
235
+ deploymentViewRule(),
236
+ ),
237
+ ),
238
+ ),
239
+ ),
240
+ ),
241
+ )
242
+
243
+ // --- Dynamic View ---
244
+
245
+ export const dynamicStep = zodOp(schemas.views.dynamicStep)(
246
+ spaceBetween(
247
+ print(v => v.source),
248
+ print(v => v.isBackward ? '<-' : '->'),
249
+ print(v => v.target),
250
+ body(
251
+ lines(
252
+ titleProperty(),
253
+ technologyProperty(),
254
+ descriptionProperty(),
255
+ notesProperty(),
256
+ property('navigateTo'),
257
+ notationProperty(),
258
+ colorProperty(),
259
+ property('line'),
260
+ property('head'),
261
+ property('tail'),
262
+ ),
263
+ ),
264
+ ),
265
+ )
266
+
267
+ export const dynamicStepsSeries = zodOp(schemas.views.dynamicStepsSeries)(({ ctx, exec }) => {
268
+ throw new Error('Not implemented')
269
+ })
270
+
271
+ export const dynamicStepsParallel = zodOp(schemas.views.dynamicStepsParallel)(({ ctx, exec }) => {
272
+ throw new Error('Not implemented')
273
+ })
274
+
275
+ export const dynamicViewStep = zodOp(schemas.views.dynamicViewStep)(({ ctx, exec }) => {
276
+ if ('__series' in ctx) {
277
+ return exec(ctx, dynamicStepsSeries())
278
+ }
279
+ if ('__parallel' in ctx) {
280
+ return exec(ctx, dynamicStepsParallel())
281
+ }
282
+ return exec(ctx, dynamicStep())
283
+ })
284
+
285
+ export const dynamicViewIncludeRule = zodOp(schemas.views.dynamicViewIncludeRule)(({ ctx, exec }) => {
286
+ if (!hasAtLeast(ctx.include, 1)) {
287
+ return
288
+ }
289
+
290
+ const isMultiple = hasAtLeast(ctx.include, 2)
291
+
292
+ const exprOp = withctx(ctx.include)(
293
+ foreach(
294
+ expression(),
295
+ separateComma(isMultiple),
296
+ ),
297
+ )
298
+
299
+ exec(
300
+ ctx,
301
+ merge(
302
+ print('include'),
303
+ ...(isMultiple ? [indent(exprOp)] : [space(), exprOp]),
304
+ ),
305
+ )
306
+ })
307
+
308
+ export const dynamicViewRule = zodOp(schemas.views.dynamicViewRule)(
309
+ ({ ctx, exec }) => {
310
+ if ('include' in ctx) {
311
+ return exec(ctx, dynamicViewIncludeRule())
312
+ }
313
+ if ('predicateId' in ctx) {
314
+ return exec(ctx, viewRuleGlobalPredicate())
315
+ }
316
+ if ('targets' in ctx && 'style' in ctx) {
317
+ return exec(ctx, viewRuleStyle())
318
+ }
319
+ if ('styleId' in ctx) {
320
+ return exec(ctx, viewRuleGlobalStyle())
321
+ }
322
+ if ('direction' in ctx) {
323
+ return exec(ctx, viewRuleAutoLayout())
324
+ }
325
+ nonexhaustive(ctx)
326
+ },
327
+ )
328
+
329
+ export const dynamicView = zodOp(schemas.views.dynamicView.partial({ _type: true }))(
330
+ spaceBetween(
331
+ print('dynamic view'),
332
+ print(v => v.id),
333
+ body(
334
+ lines(2)(
335
+ lines(
336
+ tagsProperty(),
337
+ viewTitleProperty(),
338
+ descriptionProperty(),
339
+ linksProperty(),
340
+ property('variant'),
341
+ ),
342
+ property(
343
+ 'steps',
344
+ foreachNewLine(
345
+ dynamicViewStep(),
346
+ ),
347
+ ),
348
+ property(
349
+ 'rules',
350
+ foreachNewLine(
351
+ dynamicViewRule(),
352
+ ),
353
+ ),
354
+ ),
355
+ ),
356
+ ),
357
+ )
358
+
359
+ // --- Parsed View ---
360
+ export const anyView = zodOp(schemas.views.anyView)(({ ctx, exec }) => {
361
+ if ('_type' in ctx) {
362
+ if (ctx._type == 'element') {
363
+ return exec(ctx, elementView())
364
+ }
365
+ if (ctx._type === 'deployment') {
366
+ return exec(ctx, deploymentView())
367
+ }
368
+ if (ctx._type === 'dynamic') {
369
+ return exec(ctx, dynamicView())
370
+ }
371
+ }
372
+ nonexhaustive(ctx)
373
+ })
374
+
375
+ export const views = zodOp(schemas.views.views)(
376
+ body('views')(
377
+ select(
378
+ ctx => values(ctx),
379
+ foreach(
380
+ anyView(),
381
+ {
382
+ // Extra line between
383
+ separator: new CompositeGeneratorNode().appendNewLine().appendNewLine(),
384
+ // Add empty line before first view (if not the last one)
385
+ prefix: (_, index, isLast) => index === 0 && !isLast ? NL : undefined,
386
+ },
387
+ ),
388
+ ),
389
+ ),
390
+ )
@@ -0,0 +1,123 @@
1
+ import {
2
+ BorderStyles,
3
+ ElementShapes,
4
+ IconPositions,
5
+ RelationshipArrowTypes,
6
+ Sizes,
7
+ ThemeColors,
8
+ } from '@likec4/core/styles'
9
+ import {
10
+ type CustomColor,
11
+ type Link,
12
+ type scalar,
13
+ exact,
14
+ } from '@likec4/core/types'
15
+ import * as z from 'zod/v4'
16
+
17
+ export const id = z
18
+ .string()
19
+ .regex(/^[a-zA-Z0-9_.-]+$/, 'id must consist of alphanumeric characters, underscores or hyphens')
20
+
21
+ export const viewId = id.transform(value => value as unknown as scalar.ViewId)
22
+
23
+ export const fqn = z
24
+ .string()
25
+ .regex(/^[a-zA-Z0-9_.-]+$/, 'FQN must consist of alphanumeric characters, dots, underscores or hyphens')
26
+ .transform(value => value as unknown as scalar.Fqn)
27
+
28
+ export const kind = z
29
+ .string()
30
+ .regex(/^[a-zA-Z0-9_-]+$/, 'Kind must consist of alphanumeric characters, underscores or hyphens')
31
+ .transform(value => value as unknown as scalar.ElementKind)
32
+
33
+ export const opacity = z
34
+ .int()
35
+ .min(0, 'Opacity must be between 0 and 100')
36
+ .max(100, 'Opacity must be between 0 and 100')
37
+
38
+ export const shape = z.literal(ElementShapes)
39
+
40
+ export const icon = z.string().nonempty('Icon cannot be empty').transform(value => value as unknown as scalar.Icon)
41
+
42
+ export const border = z
43
+ .literal(BorderStyles)
44
+
45
+ export const size = z.literal(Sizes)
46
+
47
+ export const iconPosition = z
48
+ .literal(IconPositions)
49
+
50
+ export const arrow = z
51
+ .literal(RelationshipArrowTypes)
52
+
53
+ export const line = z
54
+ .literal(['dashed', 'solid', 'dotted'])
55
+
56
+ export const link = z.union([
57
+ z.string(),
58
+ z.object({
59
+ title: z.string().optional(),
60
+ url: z.string(),
61
+ }),
62
+ ]).transform(
63
+ value => exact(typeof value === 'string' ? { url: value } : value) as Link,
64
+ )
65
+
66
+ export const links = z.array(link).readonly()
67
+
68
+ export const themeColor = z.literal(ThemeColors)
69
+
70
+ export const customColor = z
71
+ .custom<string & Record<never, never>>()
72
+ .refine(v => typeof v === 'string', 'Custom color name must be a string')
73
+ .transform(value => value as unknown as CustomColor)
74
+
75
+ export const tag = z
76
+ .string()
77
+ .nonempty('Tag cannot be empty')
78
+ .transform(tag => (tag.startsWith('#') ? tag.slice(1) : tag) as unknown as scalar.Tag)
79
+
80
+ export const tags = z.array(tag).readonly()
81
+
82
+ export const markdownOrString = z.union([
83
+ z.string(),
84
+ z.strictObject({ md: z.string() }),
85
+ z.strictObject({ txt: z.string() }),
86
+ ]).transform(v => (typeof v === 'string' ? { txt: v } : v) as scalar.MarkdownOrString)
87
+
88
+ export const color = themeColor.or(customColor)
89
+
90
+ export const style = z
91
+ .object({
92
+ shape: shape,
93
+ icon: icon,
94
+ iconColor: color,
95
+ iconSize: size,
96
+ iconPosition: iconPosition,
97
+ color: color,
98
+ border: border,
99
+ opacity: opacity,
100
+ size: size,
101
+ padding: size,
102
+ textSize: size,
103
+ multiple: z.boolean(),
104
+ })
105
+ .partial()
106
+
107
+ export const metadataValue = z.union([z.string(), z.boolean(), z.number()])
108
+
109
+ export const metadata = z.record(z.string(), metadataValue.or(z.array(metadataValue)))
110
+
111
+ export const props = z
112
+ .object({
113
+ tags: tags.nullable(),
114
+ title: z.string(),
115
+ summary: markdownOrString.nullable(),
116
+ description: markdownOrString.nullable(),
117
+ notation: z.string().nullable(),
118
+ technology: z.string().nullable(),
119
+ links: links.nullable(),
120
+ metadata: metadata,
121
+ })
122
+ // all properties are optional, as the generator should be able to handle missing fields and provide defaults
123
+ .partial()
@@ -0,0 +1,113 @@
1
+ import {
2
+ RelationId,
3
+ } from '@likec4/core/types'
4
+ import { produce } from 'immer'
5
+ import { indexBy, isArray, isNonNullish, pickBy, prop, randomString } from 'remeda'
6
+ import * as z from 'zod/v4'
7
+ import * as common from './common'
8
+ import * as expression from './expression'
9
+
10
+ export const node = common.props
11
+ .extend({
12
+ id: common.fqn,
13
+ kind: common.kind,
14
+ style: common.style.optional(),
15
+ /**
16
+ * Allowing shape, color and icon to be defined at the element level for convenience,
17
+ * they will be moved to the style property during parsing
18
+ * (and will override properties)
19
+ */
20
+ shape: common.shape.optional(),
21
+ color: common.color.optional(),
22
+ icon: common.icon.optional(),
23
+ })
24
+ .readonly()
25
+ .transform(value => {
26
+ const { shape, color, icon, ...rest } = value
27
+ if (shape || color || icon) {
28
+ return produce(rest, draft => {
29
+ draft.style = rest.style || {}
30
+ draft.style.shape = shape ?? rest.style?.shape
31
+ draft.style.color = color ?? rest.style?.color
32
+ draft.style.icon = icon ?? rest.style?.icon
33
+ })
34
+ }
35
+ return rest
36
+ })
37
+ .transform(pickBy(isNonNullish))
38
+
39
+ export const instance = common.props
40
+ .extend({
41
+ id: common.fqn,
42
+ element: common.fqn,
43
+ style: common.style.optional(),
44
+ /**
45
+ * Allowing shape, color and icon to be defined at the element level for convenience,
46
+ * they will be moved to the style property during parsing
47
+ * (and will override properties)
48
+ */
49
+ shape: common.shape.optional(),
50
+ color: common.color.optional(),
51
+ icon: common.icon.optional(),
52
+ })
53
+ .readonly()
54
+ .transform(value => {
55
+ const { shape, color, icon, ...rest } = value
56
+ if (shape || color || icon) {
57
+ return produce(rest, draft => {
58
+ draft.style = rest.style || {}
59
+ draft.style.shape = shape ?? rest.style?.shape
60
+ draft.style.color = color ?? rest.style?.color
61
+ draft.style.icon = icon ?? rest.style?.icon
62
+ })
63
+ }
64
+ return rest
65
+ })
66
+ .transform(pickBy(isNonNullish))
67
+
68
+ const relationshipEndpoint = expression.refDeployment
69
+
70
+ const relationshipId = common.id.transform(value => value as unknown as RelationId)
71
+
72
+ export const relationship = common.props
73
+ .extend({
74
+ id: relationshipId.optional(),
75
+ title: z.string().nullish(),
76
+ source: relationshipEndpoint,
77
+ target: relationshipEndpoint,
78
+ navigateTo: common.viewId.nullish(),
79
+ color: common.color.nullish(),
80
+ kind: common.kind.nullish(),
81
+ line: common.line.nullish(),
82
+ head: common.arrow.nullish(),
83
+ tail: common.arrow.nullish(),
84
+ })
85
+ .readonly()
86
+ .transform(pickBy(isNonNullish))
87
+
88
+ // ============ Top-Level Schema ============
89
+
90
+ export const element = z.union([node, instance])
91
+
92
+ const elements = z.record(common.fqn, element)
93
+ const relationships = z.record(relationshipId, relationship)
94
+
95
+ const genRelationshipId = (r: z.output<typeof relationship>): RelationId => r.id ?? randomString(8) as RelationId
96
+
97
+ export const schema = z
98
+ .object({
99
+ elements: z
100
+ .union([
101
+ elements,
102
+ z.array(element),
103
+ ])
104
+ .transform(v => isArray(v) ? indexBy(v, prop('id')) as unknown as z.output<typeof elements> : v)
105
+ .optional(),
106
+ relations: z
107
+ .union([
108
+ relationships,
109
+ z.array(relationship),
110
+ ])
111
+ .transform(v => isArray(v) ? indexBy(v, genRelationshipId) as unknown as z.output<typeof relationships> : v)
112
+ .optional(),
113
+ })