@likec4/generators 1.59.2 → 1.59.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likec4/generators",
3
- "version": "1.59.2",
3
+ "version": "1.59.3",
4
4
  "license": "MIT",
5
5
  "bugs": "https://github.com/likec4/likec4/issues",
6
6
  "homepage": "https://likec4.dev",
@@ -45,27 +45,27 @@
45
45
  "access": "public"
46
46
  },
47
47
  "dependencies": {
48
+ "@likec4/core": "1.59.3",
49
+ "@likec4/config": "1.59.3",
50
+ "@likec4/log": "1.59.3",
48
51
  "langium": "3.5.0",
49
52
  "pako": "^2.1.0",
50
- "remeda": "^2.37.0",
51
- "immer": "^11.1.9",
53
+ "remeda": "^2.45.0",
54
+ "immer": "^11.1.18",
52
55
  "json5": "^2.2.3",
53
56
  "type-fest": "^4.41.0",
54
57
  "indent-string": "^5.0.0",
55
58
  "strip-indent": "^4.1.1",
56
- "zod": "^4.4.3",
57
- "@likec4/core": "1.59.2",
58
- "@likec4/log": "1.59.2",
59
- "@likec4/config": "1.59.2"
59
+ "zod": "^4.4.3"
60
60
  },
61
61
  "devDependencies": {
62
+ "@likec4/tsconfig": "1.59.3",
63
+ "@likec4/devops": "1.59.3",
62
64
  "@types/node": "~22.19.19",
63
65
  "@types/pako": "^2.0.4",
64
66
  "typescript": "6.0.3",
65
67
  "tsdown": "^0.22.9",
66
- "vitest": "4.1.9",
67
- "@likec4/tsconfig": "1.59.2",
68
- "@likec4/devops": "1.59.2"
68
+ "vitest": "4.1.11"
69
69
  },
70
70
  "scripts": {
71
71
  "typecheck": "tsc -b --verbose",
@@ -1,7 +1,8 @@
1
1
  import type { LikeC4ViewModel } from '@likec4/core/model'
2
2
  import type { aux, ComputedNode, NodeId, ProcessedView as AnyView } from '@likec4/core/types'
3
- import { CompositeGeneratorNode, joinToNode, NL, toString } from 'langium/generate'
3
+ import { CompositeGeneratorNode, joinToNode } from 'langium/generate'
4
4
  import { isNullish as isNil } from 'remeda'
5
+ import { NL, toStringLF } from '../newline'
5
6
 
6
7
  const capitalizeFirstLetter = (value: string) => value.charAt(0).toLocaleUpperCase() + value.slice(1)
7
8
 
@@ -93,7 +94,7 @@ export function generateD2(viewmodel: LikeC4ViewModel<aux.Unknown>) {
93
94
  .append(out => edge.label && out.append(': ', JSON.stringify(edge.label)))
94
95
  }
95
96
 
96
- return toString(
97
+ return toStringLF(
97
98
  new CompositeGeneratorNode()
98
99
  .append('direction: ', d2direction(view), NL, NL)
99
100
  .append(
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export {
17
17
  parseDrawioToLikeC4,
18
18
  parseDrawioToLikeC4Multi,
19
19
  } from './drawio/parse-drawio'
20
+ export { generateMarkdown } from './markdown/generate-markdown'
20
21
  export { generateMermaid } from './mmd/generate-mmd'
21
22
  export { generateLikeC4Model } from './model/generate-likec4-model'
22
23
  export { generatePuml } from './puml/generate-puml'
@@ -6,8 +6,6 @@ import {
6
6
  CompositeGeneratorNode,
7
7
  joinToNode,
8
8
  NewLineNode,
9
- NL,
10
- toString,
11
9
  } from 'langium/generate'
12
10
  import {
13
11
  filter,
@@ -28,6 +26,7 @@ import { dedent } from 'strip-indent'
28
26
  import type { IfAny, Or } from 'type-fest'
29
27
  import * as z from 'zod/v4'
30
28
  import type * as z4 from 'zod/v4/core'
29
+ import { NL, toStringLF } from '../../newline'
31
30
 
32
31
  function hasContent(out: Generated): boolean {
33
32
  if (typeof out === 'string') {
@@ -109,7 +108,7 @@ export function fresh(ctx?: unknown) {
109
108
  */
110
109
  export function materialize(ctx: AnyCtx | Op<any>, defaultIndentation: string | number = 2): string {
111
110
  const out = isFunction(ctx) ? executeOnFresh(undefined, [ctx]).out : ctx.out
112
- return toString(out, defaultIndentation).replaceAll(/\r\n/g, '\n')
111
+ return toStringLF(out, defaultIndentation)
113
112
  }
114
113
 
115
114
  /**
@@ -3,8 +3,9 @@ import {
3
3
  invariant,
4
4
  nonexhaustive,
5
5
  } from '@likec4/core'
6
- import { CompositeGeneratorNode, NL } from 'langium/generate'
6
+ import { CompositeGeneratorNode } from 'langium/generate'
7
7
  import { hasAtLeast, values } from 'remeda'
8
+ import { NL } from '../../newline'
8
9
  import { schemas } from '../schemas'
9
10
  import {
10
11
  type Op,
@@ -187,6 +188,7 @@ export const elementView = zodOp(schemas.views.elementView.partial({ _type: true
187
188
  lines(
188
189
  tagsProperty(),
189
190
  viewTitleProperty(),
191
+ property('order'),
190
192
  descriptionProperty(),
191
193
  linksProperty(),
192
194
  ),
@@ -237,6 +239,7 @@ export const deploymentView = zodOp(schemas.views.deploymentView.partial({ _type
237
239
  lines(
238
240
  tagsProperty(),
239
241
  viewTitleProperty(),
242
+ property('order'),
240
243
  descriptionProperty(),
241
244
  linksProperty(),
242
245
  ),
@@ -460,6 +463,7 @@ export const dynamicView = zodOp(schemas.views.dynamicView.partial({ _type: true
460
463
  lines(
461
464
  tagsProperty(),
462
465
  viewTitleProperty(),
466
+ property('order'),
463
467
  descriptionProperty(),
464
468
  linksProperty(),
465
469
  property('variant'),
@@ -38,11 +38,7 @@ export const relationship = z
38
38
 
39
39
  export const tagSpec = z
40
40
  .object({
41
- color: z
42
- .string()
43
- .regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/)
44
- .optional()
45
- .catch(undefined),
41
+ color: common.color.nullable(),
46
42
  })
47
43
  .partial()
48
44
  .transform(pickBy(isNonNullish))
@@ -62,7 +62,9 @@ export const viewRuleRank = z
62
62
 
63
63
  // Manually typed due to recursive z.lazy schema
64
64
  export type ViewRuleGroupInput = {
65
- groupRules: Array<z.input<typeof viewRulePredicate> | ViewRuleGroupInput>
65
+ groupRules: Array<
66
+ z.input<typeof viewRulePredicate> | z.input<typeof viewRuleGlobalPredicate> | ViewRuleGroupInput
67
+ >
66
68
  title: string | null
67
69
  color?: z.input<typeof common.color> | undefined
68
70
  border?: z.input<typeof common.border> | undefined
@@ -74,7 +76,9 @@ export type ViewRuleGroupInput = {
74
76
  }
75
77
 
76
78
  export type ViewRuleGroupOutput = {
77
- groupRules: Array<z.infer<typeof viewRulePredicate> | ViewRuleGroupOutput>
79
+ groupRules: Array<
80
+ z.infer<typeof viewRulePredicate> | z.infer<typeof viewRuleGlobalPredicate> | ViewRuleGroupOutput
81
+ >
78
82
  title: string | null
79
83
  color?: z.infer<typeof common.color> | undefined
80
84
  border?: z.infer<typeof common.border> | undefined
@@ -87,7 +91,7 @@ export type ViewRuleGroupOutput = {
87
91
 
88
92
  export const viewRuleGroup: z.ZodType<ViewRuleGroupOutput, ViewRuleGroupInput> = z.lazy(() =>
89
93
  z.object({
90
- groupRules: z.array(z.union([viewRulePredicate, viewRuleGroup])),
94
+ groupRules: z.array(z.union([viewRulePredicate, viewRuleGlobalPredicate, viewRuleGroup])),
91
95
  title: z.string().nullable(),
92
96
  color: common.color.optional(),
93
97
  border: common.border.optional(),
@@ -114,6 +118,7 @@ const viewProps = z.object({
114
118
  id: common.viewId,
115
119
  title: z.string().nullish(),
116
120
  description: common.markdownOrString.nullish(),
121
+ order: z.number().int().nonnegative().optional(),
117
122
  tags: common.tags.nullish(),
118
123
  links: common.links.nullish(),
119
124
  })
@@ -0,0 +1,52 @@
1
+ import type { LikeC4Model, LikeC4ViewModel } from '@likec4/core/model'
2
+ import type { aux } from '@likec4/core/types'
3
+ import { CompositeGeneratorNode } from 'langium/generate'
4
+ import { generateMermaid } from '../mmd/generate-mmd'
5
+ import { NL, toStringLF } from '../newline'
6
+
7
+ type ViewModel = LikeC4ViewModel<aux.Unknown>
8
+
9
+ /**
10
+ * Appends a single view to the document as a level-3 heading, its optional
11
+ * description, and a fenced Mermaid diagram.
12
+ */
13
+ function appendView(doc: CompositeGeneratorNode, view: ViewModel): void {
14
+ doc.append('### ', view.titleOrId, NL, NL)
15
+ if (view.description.nonEmpty) {
16
+ doc.append(view.description.md, NL, NL)
17
+ }
18
+ doc.append('```mermaid', NL, generateMermaid(view).trimEnd(), NL, '```', NL, NL)
19
+ }
20
+
21
+ export type GenerateMarkdownOptions = {
22
+ /**
23
+ * `LikeC4Project` has no description field of its own yet, so callers source this from
24
+ * wherever they keep it (e.g. the project config's `metadata` bag) and pass it through.
25
+ * If a first-class description ever lands on `LikeC4Project`, read it from the model
26
+ * directly instead and drop this option.
27
+ */
28
+ description?: string
29
+ }
30
+
31
+ /**
32
+ * Renders a LikeC4 model as a Markdown document: the project title, an optional
33
+ * description, and every authored view as a Mermaid diagram. Views without a
34
+ * `sourcePath` (e.g. auto-generated index views) are skipped.
35
+ */
36
+ export function generateMarkdown(
37
+ model: LikeC4Model<aux.Unknown>,
38
+ options: GenerateMarkdownOptions = {},
39
+ ): string {
40
+ const doc = new CompositeGeneratorNode()
41
+ doc.append('# ', model.project.title ?? model.projectId, NL, NL)
42
+ if (options.description) {
43
+ doc.append(options.description, NL, NL)
44
+ }
45
+
46
+ for (const view of model.views()) {
47
+ if (view.$view.sourcePath === undefined) continue
48
+ appendView(doc, view)
49
+ }
50
+
51
+ return toStringLF(doc)
52
+ }
@@ -0,0 +1 @@
1
+ export * from './generate-markdown'
@@ -1,8 +1,9 @@
1
1
  import { nonexhaustive } from '@likec4/core'
2
2
  import type { LikeC4ViewModel } from '@likec4/core/model'
3
3
  import type { aux, NodeId, ProcessedView as AnyView } from '@likec4/core/types'
4
- import { CompositeGeneratorNode, joinToNode, NL, toString } from 'langium/generate'
4
+ import { CompositeGeneratorNode, joinToNode } from 'langium/generate'
5
5
  import { isNullish as isNil } from 'remeda'
6
+ import { NL, toStringLF } from '../newline'
6
7
 
7
8
  const capitalizeFirstLetter = (value: string) => value.charAt(0).toLocaleUpperCase() + value.slice(1)
8
9
 
@@ -97,7 +98,7 @@ export function generateMermaid(viewmodel: LikeC4ViewModel<aux.Unknown>) {
97
98
  )
98
99
  }
99
100
 
100
- return toString(
101
+ return toStringLF(
101
102
  new CompositeGeneratorNode()
102
103
  .append(
103
104
  '---',
package/src/newline.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { GeneratorNode } from 'langium/generate'
2
+ import { NewLineNode, toString } from 'langium/generate'
3
+
4
+ /**
5
+ * Line separator for all generated output.
6
+ *
7
+ * Langium's own `NL` resolves to `os.EOL`, which emits CRLF on Windows and makes
8
+ * generated artifacts (Markdown, Mermaid, D2, PlantUML, LikeC4 DSL) differ per platform.
9
+ * Generators must be deterministic, so always emit LF.
10
+ */
11
+ export const NL = new NewLineNode('\n')
12
+
13
+ /**
14
+ * Renders a generator node with LF line endings.
15
+ *
16
+ * Some Langium helpers insert `os.EOL` internally (for example `joinToNode`'s
17
+ * `appendNewLineIfNotEmpty`), so normalize the rendered output as well as using {@link NL}.
18
+ */
19
+ export function toStringLF(node: GeneratorNode | undefined, defaultIndentation?: string | number): string {
20
+ return toString(node, defaultIndentation).replaceAll('\r\n', '\n')
21
+ }
@@ -13,8 +13,9 @@ import type {
13
13
  ThemeColor,
14
14
  } from '@likec4/core/types'
15
15
  import { RichText } from '@likec4/core/types'
16
- import { CompositeGeneratorNode, joinToNode, NL, toString } from 'langium/generate'
16
+ import { CompositeGeneratorNode, joinToNode } from 'langium/generate'
17
17
  import { isEmptyish, isNullish as isNil } from 'remeda'
18
+ import { NL, toStringLF } from '../newline'
18
19
 
19
20
  const capitalizeFirstLetter = (value: string) => value.charAt(0).toLocaleUpperCase() + value.slice(1)
20
21
 
@@ -233,7 +234,7 @@ export function generatePuml(viewmodel: LikeC4ViewModel<aux.Unknown>) {
233
234
  return out.append(NL)
234
235
  }
235
236
 
236
- return toString(
237
+ return toStringLF(
237
238
  new CompositeGeneratorNode()
238
239
  .append('@startuml', NL)
239
240
  .append(printHeader(), NL)
@@ -1,6 +1,7 @@
1
1
  import type { DiagramView } from '@likec4/core'
2
2
  import JSON5 from 'json5'
3
- import { CompositeGeneratorNode, expandToNode, joinToNode, NL, toString } from 'langium/generate'
3
+ import { CompositeGeneratorNode, expandToNode, joinToNode } from 'langium/generate'
4
+ import { NL, toStringLF } from '../newline'
4
5
  import { generateViewId } from './generateViewId'
5
6
 
6
7
  /**
@@ -21,7 +22,7 @@ export function generateViewsDataJs(diagrams: Iterable<DiagramView>) {
21
22
 
22
23
  if (views.length == 0) {
23
24
  out.append('export const LikeC4Views = {}', NL)
24
- return toString(out)
25
+ return toStringLF(out)
25
26
  }
26
27
 
27
28
  out.appendTemplate`
@@ -54,7 +55,7 @@ export function generateViewsDataJs(diagrams: Iterable<DiagramView>) {
54
55
 
55
56
  /* prettier-ignore-end */
56
57
  `.append(NL)
57
- return toString(out)
58
+ return toStringLF(out)
58
59
  }
59
60
 
60
61
  /**
@@ -78,7 +79,7 @@ export function generateViewsDataTs(diagrams: Iterable<DiagramView>) {
78
79
 
79
80
  if (views.length === 0) {
80
81
  out.append('export {}', NL)
81
- return toString(out)
82
+ return toStringLF(out)
82
83
  }
83
84
 
84
85
  out.appendTemplate`
@@ -118,7 +119,7 @@ export function generateViewsDataTs(diagrams: Iterable<DiagramView>) {
118
119
 
119
120
  /* prettier-ignore-end */
120
121
  `.append(NL)
121
- return toString(out)
122
+ return toStringLF(out)
122
123
  }
123
124
 
124
125
  /**
@@ -140,7 +141,7 @@ export function generateViewsDataDTs(diagrams: Iterable<DiagramView>) {
140
141
 
141
142
  if (views.length == 0) {
142
143
  out.append('export {}', NL)
143
- return toString(out)
144
+ return toStringLF(out)
144
145
  }
145
146
 
146
147
  out.appendTemplate`
@@ -152,5 +153,5 @@ export function generateViewsDataDTs(diagrams: Iterable<DiagramView>) {
152
153
 
153
154
  /* prettier-ignore-end */
154
155
  `.append(NL)
155
- return toString(out)
156
+ return toStringLF(out)
156
157
  }