@likec4/language-server 1.8.1 → 1.10.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.
Files changed (85) hide show
  1. package/contrib/likec4.tmLanguage.json +1 -1
  2. package/dist/browser.cjs +21 -0
  3. package/dist/browser.d.cts +22 -0
  4. package/dist/browser.d.mts +22 -0
  5. package/dist/browser.d.ts +22 -0
  6. package/dist/browser.mjs +19 -0
  7. package/dist/index.cjs +10 -0
  8. package/dist/index.d.cts +18 -0
  9. package/dist/index.d.mts +18 -0
  10. package/dist/index.d.ts +18 -0
  11. package/dist/index.mjs +1 -0
  12. package/dist/likec4lib.cjs +961 -0
  13. package/dist/likec4lib.d.cts +6 -0
  14. package/dist/likec4lib.d.mts +6 -0
  15. package/dist/likec4lib.d.ts +6 -0
  16. package/dist/likec4lib.mjs +957 -0
  17. package/dist/model-graph/index.cjs +10 -0
  18. package/dist/model-graph/index.d.cts +79 -0
  19. package/dist/model-graph/index.d.mts +79 -0
  20. package/dist/model-graph/index.d.ts +79 -0
  21. package/dist/model-graph/index.mjs +1 -0
  22. package/dist/node.cjs +18 -0
  23. package/dist/node.d.cts +20 -0
  24. package/dist/node.d.mts +20 -0
  25. package/dist/node.d.ts +20 -0
  26. package/dist/node.mjs +16 -0
  27. package/dist/protocol.cjs +25 -0
  28. package/dist/protocol.d.cts +43 -0
  29. package/dist/protocol.d.mts +43 -0
  30. package/dist/protocol.d.ts +43 -0
  31. package/dist/protocol.mjs +17 -0
  32. package/dist/shared/language-server.CjFzaJwI.d.cts +1223 -0
  33. package/dist/shared/language-server.CtKHXJDD.d.ts +1223 -0
  34. package/dist/shared/language-server.D-84I33F.d.mts +1223 -0
  35. package/dist/shared/language-server.DBJJUUgF.mjs +5737 -0
  36. package/dist/shared/language-server.DtBRb9os.mjs +1656 -0
  37. package/dist/shared/language-server.DwyCJvXm.cjs +1669 -0
  38. package/dist/shared/language-server.JWkqVjGv.cjs +5748 -0
  39. package/package.json +36 -20
  40. package/src/ast.ts +48 -36
  41. package/src/browser.ts +0 -3
  42. package/src/elementRef.ts +1 -1
  43. package/src/formatting/LikeC4Formatter.ts +388 -0
  44. package/src/formatting/utils.ts +26 -0
  45. package/src/generated/ast.ts +170 -12
  46. package/src/generated/grammar.ts +1 -1
  47. package/src/generated-lib/icons.ts +1 -1
  48. package/src/like-c4.langium +49 -8
  49. package/src/likec4lib.ts +2 -3
  50. package/src/logger.ts +9 -1
  51. package/src/lsp/DocumentLinkProvider.ts +27 -15
  52. package/src/lsp/RenameProvider.ts +8 -0
  53. package/src/lsp/SemanticTokenProvider.ts +20 -2
  54. package/src/lsp/index.ts +1 -0
  55. package/src/model/fqn-computation.ts +33 -23
  56. package/src/model/fqn-index.ts +5 -21
  57. package/src/model/model-builder.ts +180 -112
  58. package/src/model/model-locator.ts +1 -1
  59. package/src/model/model-parser-where.ts +3 -2
  60. package/src/model/model-parser.ts +99 -39
  61. package/src/model-graph/LikeC4ModelGraph.ts +42 -21
  62. package/src/model-graph/compute-view/__test__/fixture.ts +16 -14
  63. package/src/model-graph/compute-view/compute.ts +110 -81
  64. package/src/model-graph/compute-view/predicates.ts +6 -8
  65. package/src/model-graph/dynamic-view/__test__/fixture.ts +1 -0
  66. package/src/model-graph/dynamic-view/compute.ts +98 -61
  67. package/src/model-graph/utils/buildElementNotations.ts +1 -1
  68. package/src/model-graph/utils/elementExpressionToPredicate.ts +1 -1
  69. package/src/model-graph/utils/sortNodes.ts +2 -6
  70. package/src/module.ts +21 -4
  71. package/src/protocol.ts +4 -5
  72. package/src/references/scope-computation.ts +10 -1
  73. package/src/references/scope-provider.ts +2 -1
  74. package/src/shared/NodeKindProvider.ts +73 -34
  75. package/src/test/setup.ts +3 -8
  76. package/src/test/testServices.ts +27 -7
  77. package/src/utils/graphlib.ts +11 -0
  78. package/src/validation/index.ts +2 -1
  79. package/src/validation/property-checks.ts +13 -1
  80. package/src/validation/specification.ts +3 -3
  81. package/src/view-utils/manual-layout.ts +1 -1
  82. package/src/view-utils/resolve-extended-views.ts +19 -10
  83. package/src/view-utils/resolve-relative-paths.ts +19 -24
  84. package/src/view-utils/view-hash.ts +1 -1
  85. package/src/reset.d.ts +0 -2
package/src/module.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  type PartialLangiumSharedServices
10
10
  } from 'langium/lsp'
11
11
  import { LikeC4GeneratedModule, LikeC4GeneratedSharedModule } from './generated/module'
12
- import { logErrorToTelemetry } from './logger'
12
+ import { logErrorToTelemetry, logToLspConnection } from './logger'
13
13
  import {
14
14
  LikeC4CodeLensProvider,
15
15
  LikeC4CompletionProvider,
@@ -25,6 +25,7 @@ import { LikeC4ScopeComputation, LikeC4ScopeProvider } from './references'
25
25
  import { Rpc } from './Rpc'
26
26
  import { LikeC4WorkspaceManager, NodeKindProvider, WorkspaceSymbolProvider } from './shared'
27
27
  import { registerValidationChecks } from './validation'
28
+ import { LikeC4Formatter } from './formatting/LikeC4Formatter'
28
29
 
29
30
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
31
  type Constructor<T, Arguments extends unknown[] = any[]> = new(...arguments_: Arguments) => T
@@ -69,6 +70,7 @@ export interface LikeC4AddedServices {
69
70
  ModelChanges: LikeC4ModelChanges
70
71
  }
71
72
  lsp: {
73
+ // RenameProvider: LikeC4RenameProvider
72
74
  CompletionProvider: LikeC4CompletionProvider
73
75
  DocumentHighlightProvider: LikeC4DocumentHighlightProvider
74
76
  DocumentSymbolProvider: LikeC4DocumentSymbolProvider
@@ -101,13 +103,15 @@ export const LikeC4Module: Module<LikeC4Services, PartialLangiumServices & LikeC
101
103
  ModelLocator: bind(LikeC4ModelLocator)
102
104
  },
103
105
  lsp: {
106
+ // RenameProvider: bind(LikeC4RenameProvider),
104
107
  CompletionProvider: bind(LikeC4CompletionProvider),
105
108
  DocumentHighlightProvider: bind(LikeC4DocumentHighlightProvider),
106
109
  DocumentSymbolProvider: bind(LikeC4DocumentSymbolProvider),
107
110
  SemanticTokenProvider: bind(LikeC4SemanticTokenProvider),
108
111
  HoverProvider: bind(LikeC4HoverProvider),
109
112
  CodeLensProvider: bind(LikeC4CodeLensProvider),
110
- DocumentLinkProvider: bind(LikeC4DocumentLinkProvider)
113
+ DocumentLinkProvider: bind(LikeC4DocumentLinkProvider),
114
+ Formatter: bind(LikeC4Formatter)
111
115
  },
112
116
  references: {
113
117
  ScopeComputation: bind(LikeC4ScopeComputation),
@@ -136,7 +140,15 @@ export function createCustomLanguageServices<I1, I2, I3, I extends I1 & I2 & I3
136
140
  const likec4 = inject(modules)
137
141
  shared.ServiceRegistry.register(likec4)
138
142
  registerValidationChecks(likec4)
139
- likec4.Rpc.init()
143
+
144
+ if (!context.connection) {
145
+ // We don't run inside a language server
146
+ // Therefore, initialize the configuration provider instantly
147
+ shared.workspace.ConfigurationProvider.initialized({})
148
+ } else {
149
+ likec4.Rpc.init()
150
+ }
151
+
140
152
  return { shared, likec4 }
141
153
  }
142
154
 
@@ -146,6 +158,7 @@ export function createSharedServices(context: LanguageServicesContext = {}): Lik
146
158
  ...context
147
159
  }
148
160
  if (context.connection) {
161
+ logToLspConnection(context.connection)
149
162
  logErrorToTelemetry(context.connection)
150
163
  }
151
164
 
@@ -164,7 +177,11 @@ export function createLanguageServices(context: LanguageServicesContext = {}): {
164
177
  const likec4 = inject(createDefaultModule({ shared }), LikeC4GeneratedModule, LikeC4Module)
165
178
  shared.ServiceRegistry.register(likec4)
166
179
  registerValidationChecks(likec4)
167
- likec4.Rpc.init()
180
+
181
+ if (context.connection) {
182
+ likec4.Rpc.init()
183
+ }
184
+
168
185
  return { shared, likec4 }
169
186
  }
170
187
 
package/src/protocol.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import type {
2
- AutoLayoutDirection,
2
+ ComputedLikeC4Model,
3
3
  ComputedView,
4
4
  Fqn,
5
- LikeC4ComputedModel,
6
- LikeC4Model,
5
+ ParsedLikeC4Model,
7
6
  RelationID,
8
7
  ViewChange,
9
8
  ViewID
@@ -17,12 +16,12 @@ export type OnDidChangeModelNotification = typeof onDidChangeModel
17
16
  // #endregion
18
17
 
19
18
  // #region To server
20
- export const fetchModel = new RequestType0<{ model: LikeC4Model | null }, void>(
19
+ export const fetchModel = new RequestType0<{ model: ParsedLikeC4Model | null }, void>(
21
20
  'likec4/fetchModel'
22
21
  )
23
22
  export type FetchModelRequest = typeof fetchModel
24
23
 
25
- export const fetchComputedModel = new RequestType0<{ model: LikeC4ComputedModel | null }, void>(
24
+ export const fetchComputedModel = new RequestType0<{ model: ComputedLikeC4Model | null }, void>(
26
25
  'likec4/fetchComputedModel'
27
26
  )
28
27
  export type FetchComputedModelRequest = typeof fetchComputedModel
@@ -106,7 +106,8 @@ export class LikeC4ScopeComputation extends DefaultScopeComputation {
106
106
  const spec of specifications.flatMap(s => [
107
107
  ...s.elements,
108
108
  ...s.relationships,
109
- ...s.tags
109
+ ...s.tags,
110
+ ...s.colors
110
111
  ])
111
112
  ) {
112
113
  try {
@@ -136,6 +137,14 @@ export class LikeC4ScopeComputation extends DefaultScopeComputation {
136
137
  }
137
138
  continue
138
139
  }
140
+ case ast.isSpecificationColor(spec): {
141
+ if (isTruthy(spec.name.name)) {
142
+ docExports.push(
143
+ this.descriptions.createDescription(spec.name, spec.name.name, document)
144
+ )
145
+ }
146
+ continue
147
+ }
139
148
  // Thow error if not exhaustive
140
149
  default:
141
150
  nonexhaustive(spec)
@@ -1,4 +1,5 @@
1
- import { invariant, type likec4 as c4 } from '@likec4/core'
1
+ import { invariant } from '@likec4/core'
2
+ import type * as c4 from '@likec4/core'
2
3
  import type { AstNode } from 'langium'
3
4
  import {
4
5
  type AstNodeDescription,
@@ -1,4 +1,4 @@
1
- import { type AstNode, type AstNodeDescription } from 'langium'
1
+ import { type AstNode, type AstNodeDescription, isAstNode } from 'langium'
2
2
  import type { LangiumSharedServices, NodeKindProvider as LspNodeKindProvider } from 'langium/lsp'
3
3
  import { CompletionItemKind, SymbolKind } from 'vscode-languageserver-types'
4
4
  import { ast } from '../ast'
@@ -11,32 +11,44 @@ export class NodeKindProvider implements LspNodeKindProvider {
11
11
  */
12
12
  // prettier-ignore
13
13
  getSymbolKind(node: AstNode | AstNodeDescription): SymbolKind {
14
- const hasType = (type: string) => 'type' in node && this.services.AstReflection.isSubtype(node.type, type)
14
+ const nodeType = isAstNode(node) ? node.$type : node.type
15
+ const hasType = (...types: string[]) => types.some(t => this.services.AstReflection.isSubtype(nodeType, t))
15
16
  switch (true) {
16
- case (ast.isElement(node) || hasType(ast.Element))
17
- || (ast.isExtendElement(node) || hasType(ast.ExtendElement)): {
17
+ case hasType(
18
+ ast.Element,
19
+ ast.ExtendElement
20
+ ):
18
21
  return SymbolKind.Constructor
19
- }
20
- case ast.isModel(node) || ast.isModelViews(node) || ast.isSpecificationRule(node)
21
- || hasType(ast.Model) || hasType(ast.ModelViews) || hasType(ast.SpecificationRule): {
22
+
23
+ case hasType(
24
+ ast.Model,
25
+ ast.ModelViews,
26
+ ast.SpecificationRule
27
+ ):
22
28
  return SymbolKind.Namespace
23
- }
24
- case (ast.isLikeC4View(node) || hasType(ast.LikeC4View)): {
29
+
30
+ case hasType(ast.LikeC4View):
25
31
  return SymbolKind.Class
26
- }
27
- case (ast.isTag(node) || hasType(ast.Tag))
28
- || (ast.isLibIcon(node) || hasType(ast.LibIcon))
29
- || (ast.isSpecificationTag(node) || hasType(ast.SpecificationTag)): {
32
+
33
+ case hasType(
34
+ ast.Tag,
35
+ ast.LibIcon,
36
+ ast.CustomColor,
37
+ ast.SpecificationTag
38
+ ):
30
39
  return SymbolKind.EnumMember
31
- }
32
- case (ast.isRelationshipKind(node) || hasType(ast.RelationshipKind))
33
- || (ast.isSpecificationRelationshipKind(node) || hasType(ast.SpecificationRelationshipKind)): {
40
+
41
+ case hasType(
42
+ ast.RelationshipKind,
43
+ ast.SpecificationRelationshipKind
44
+ ):
34
45
  return SymbolKind.Event
35
- }
36
- case (ast.isElementKind(node) || hasType(ast.ElementKind))
37
- || (ast.isSpecificationElementKind(node) || hasType(ast.SpecificationElementKind)): {
46
+
47
+ case hasType(
48
+ ast.ElementKind,
49
+ ast.SpecificationElementKind
50
+ ):
38
51
  return SymbolKind.TypeParameter
39
- }
40
52
  }
41
53
  return SymbolKind.Field
42
54
  }
@@ -44,25 +56,52 @@ export class NodeKindProvider implements LspNodeKindProvider {
44
56
  * Returns a `CompletionItemKind` as used by the `CompletionProvider`.
45
57
  */
46
58
  getCompletionItemKind(node: AstNode | AstNodeDescription): CompletionItemKind {
47
- switch (this.getSymbolKind(node)) {
48
- case SymbolKind.Constructor:
59
+ const nodeType = isAstNode(node) ? node.$type : node.type
60
+ const hasType = (...types: string[]) => types.some(t => this.services.AstReflection.isSubtype(nodeType, t))
61
+ switch (true) {
62
+ case hasType(
63
+ ast.CustomColor
64
+ ):
65
+ return CompletionItemKind.Color
66
+
67
+ case hasType(
68
+ ast.Element,
69
+ ast.ExtendElement
70
+ ):
49
71
  return CompletionItemKind.Constructor
50
- case SymbolKind.Namespace:
72
+
73
+ case hasType(
74
+ ast.Model,
75
+ ast.ModelViews,
76
+ ast.SpecificationRule
77
+ ):
51
78
  return CompletionItemKind.Module
52
- case SymbolKind.Class:
79
+
80
+ case hasType(
81
+ ast.LikeC4View
82
+ ):
53
83
  return CompletionItemKind.Class
54
- case SymbolKind.Enum:
55
- return CompletionItemKind.Enum
56
- case SymbolKind.EnumMember:
84
+
85
+ case hasType(
86
+ ast.Tag,
87
+ ast.LibIcon,
88
+ ast.CustomColor,
89
+ ast.SpecificationTag
90
+ ):
57
91
  return CompletionItemKind.EnumMember
58
- case SymbolKind.TypeParameter:
59
- return CompletionItemKind.TypeParameter
60
- case SymbolKind.Interface:
61
- return CompletionItemKind.Interface
62
- case SymbolKind.Event:
92
+
93
+ case hasType(
94
+ ast.RelationshipKind,
95
+ ast.SpecificationRelationshipKind
96
+ ):
63
97
  return CompletionItemKind.Event
64
- case SymbolKind.Constant:
65
- return CompletionItemKind.Constant
98
+
99
+ case hasType(
100
+ ast.ElementKind,
101
+ ast.SpecificationElementKind
102
+ ):
103
+ return CompletionItemKind.TypeParameter
104
+
66
105
  default:
67
106
  return CompletionItemKind.Reference
68
107
  }
package/src/test/setup.ts CHANGED
@@ -1,13 +1,8 @@
1
- import { beforeAll, beforeEach, vi } from 'vitest'
1
+ import { consola } from '@likec4/log'
2
+ import { beforeEach, vi } from 'vitest'
2
3
  import { logger } from '../logger'
3
-
4
- beforeAll(() => {
5
- // Redirect std and console to consola too
6
- // Calling this once is sufficient
7
- logger.wrapAll()
8
- })
9
-
10
4
  beforeEach(() => {
11
5
  // Vitest
6
+ consola.mockTypes(() => vi.fn())
12
7
  logger.mockTypes(() => vi.fn())
13
8
  })
@@ -1,4 +1,4 @@
1
- import { DocumentState, EmptyFileSystem } from 'langium'
1
+ import { DocumentState, EmptyFileSystem, TextDocument } from 'langium'
2
2
  import * as assert from 'node:assert'
3
3
  import stripIndent from 'strip-indent'
4
4
  import { type Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types'
@@ -13,6 +13,7 @@ export function createTestServices(workspace = 'file:///test/workspace') {
13
13
  const documentBuilder = services.shared.workspace.DocumentBuilder
14
14
  const modelBuilder = services.likec4.ModelBuilder
15
15
  const workspaceUri = URI.parse(workspace)
16
+ const formatter = services.lsp.Formatter
16
17
  const workspaceFolder = {
17
18
  name: 'test',
18
19
  uri: workspaceUri.toString()
@@ -27,11 +28,13 @@ export function createTestServices(workspace = 'file:///test/workspace') {
27
28
  return
28
29
  }
29
30
  isInitialized = true
30
- await services.shared.workspace.WorkspaceManager.initializeWorkspace([workspaceFolder])
31
- // Workaround to set protected folders property
32
- Object.assign(services.shared.workspace.WorkspaceManager, {
33
- folders: [workspaceFolder]
31
+ services.shared.workspace.WorkspaceManager.initialize({
32
+ capabilities: {},
33
+ processId: null,
34
+ rootUri: null,
35
+ workspaceFolders: [workspaceFolder]
34
36
  })
37
+ await services.shared.workspace.WorkspaceManager.initializeWorkspace([workspaceFolder])
35
38
  })
36
39
  }
37
40
  const docUri = Utils.resolvePath(
@@ -66,12 +69,28 @@ export function createTestServices(workspace = 'file:///test/workspace') {
66
69
  }
67
70
  }
68
71
 
72
+ const format = async (input: string | LikeC4LangiumDocument, uri?: string) => {
73
+ const document = typeof input === 'string' ? await parse(input, uri) : input
74
+ await services.shared.workspace.WorkspaceLock.write(async (_cancelToken) => {
75
+ await documentBuilder.build([document], { validation: false })
76
+ })
77
+
78
+ const edits = await services.lsp.Formatter?.formatDocument(
79
+ document,
80
+ {
81
+ options: {tabSize: 2, insertSpaces: true},
82
+ textDocument: { uri: document.uri.toString() }
83
+ });
84
+
85
+ return TextDocument.applyEdits(document.textDocument, edits ?? []);
86
+ }
87
+
69
88
  type ValidateAllResult = {
70
89
  diagnostics: Diagnostic[]
71
90
  errors: string[]
72
91
  warnings: string[]
73
92
  }
74
- let previousPromise = Promise.resolve() as Promise<any>
93
+
75
94
  const validateAll = async () => {
76
95
  await services.shared.workspace.WorkspaceLock.write(async (_cancelToken) => {
77
96
  const docs = langiumDocuments.all.toArray()
@@ -110,7 +129,8 @@ export function createTestServices(workspace = 'file:///test/workspace') {
110
129
  validate,
111
130
  validateAll,
112
131
  buildModel,
113
- resetState
132
+ resetState,
133
+ format
114
134
  }
115
135
  }
116
136
 
@@ -0,0 +1,11 @@
1
+ // '@dagrejs/graphlib' is a CommonJS module
2
+ // Here is a workaround to import it
3
+
4
+ import { Graph } from '@dagrejs/graphlib'
5
+ import graphlib from '@dagrejs/graphlib'
6
+
7
+ export { Graph }
8
+
9
+ export const postorder = graphlib.alg.postorder
10
+ export const findCycles = graphlib.alg.findCycles
11
+ export const isAcyclic = graphlib.alg.isAcyclic
@@ -4,7 +4,7 @@ import type { LikeC4Services } from '../module'
4
4
  import { dynamicViewRulePredicate } from './dynamic-view-rule'
5
5
  import { dynamicViewStep } from './dynamic-view-step'
6
6
  import { elementChecks } from './element'
7
- import { iconPropertyRuleChecks, opacityPropertyRuleChecks } from './property-checks'
7
+ import { iconPropertyRuleChecks, notesPropertyRuleChecks, opacityPropertyRuleChecks } from './property-checks'
8
8
  import { relationBodyChecks, relationChecks } from './relation'
9
9
  import {
10
10
  elementKindChecks,
@@ -27,6 +27,7 @@ export function registerValidationChecks(services: LikeC4Services) {
27
27
  logger.info('registerValidationChecks')
28
28
  const registry = services.validation.ValidationRegistry
29
29
  registry.register<ast.LikeC4AstType>({
30
+ NotesProperty: notesPropertyRuleChecks(services),
30
31
  OpacityProperty: opacityPropertyRuleChecks(services),
31
32
  IconProperty: iconPropertyRuleChecks(services),
32
33
  SpecificationRule: specificationRuleChecks(services),
@@ -1,4 +1,4 @@
1
- import type { ValidationCheck } from 'langium'
1
+ import { AstUtils, type ValidationCheck } from 'langium'
2
2
  import { ast } from '../ast'
3
3
  import type { LikeC4Services } from '../module'
4
4
 
@@ -37,3 +37,15 @@ export const iconPropertyRuleChecks = (
37
37
  }
38
38
  }
39
39
  }
40
+
41
+ export const notesPropertyRuleChecks = (
42
+ _: LikeC4Services
43
+ ): ValidationCheck<ast.NotesProperty> => {
44
+ return (node, accept) => {
45
+ if (!AstUtils.hasContainerOfType(node, ast.isDynamicViewStep)) {
46
+ accept('error', `Notes can be defined only inside dynamic view`, {
47
+ node
48
+ })
49
+ }
50
+ }
51
+ }
@@ -8,7 +8,7 @@ export const specificationRuleChecks = (
8
8
  ): ValidationCheck<ast.SpecificationRule> => {
9
9
  return (node, accept) => {
10
10
  if (node.$containerIndex && node.$containerIndex > 0) {
11
- accept('error', `Only one specification per document is allowed`, {
11
+ accept('warning', `Prefer one specification per document`, {
12
12
  node: node,
13
13
  property: 'name'
14
14
  })
@@ -19,7 +19,7 @@ export const specificationRuleChecks = (
19
19
  export const modelRuleChecks = (_: LikeC4Services): ValidationCheck<ast.Model> => {
20
20
  return (node, accept) => {
21
21
  if (node.$containerIndex && node.$containerIndex > 0) {
22
- accept('error', `Only one model per document is allowed`, {
22
+ accept('warning', `Prefer one model per document`, {
23
23
  node: node,
24
24
  property: 'name'
25
25
  })
@@ -30,7 +30,7 @@ export const modelRuleChecks = (_: LikeC4Services): ValidationCheck<ast.Model> =
30
30
  export const modelViewsChecks = (_: LikeC4Services): ValidationCheck<ast.ModelViews> => {
31
31
  return (node, accept) => {
32
32
  if (node.$containerIndex && node.$containerIndex > 0) {
33
- accept('error', `Only one views block per document is allowed`, {
33
+ accept('warning', `Prefer one views block per document`, {
34
34
  node: node,
35
35
  property: 'name'
36
36
  })
@@ -1,4 +1,4 @@
1
- import type { ViewManualLayout } from '@likec4/core/types'
1
+ import type { ViewManualLayout } from '@likec4/core'
2
2
  import { decode, encode } from '@msgpack/msgpack'
3
3
  import { fromBase64, toBase64 } from '@smithy/util-base64'
4
4
  import { mapValues } from 'remeda'
@@ -1,9 +1,7 @@
1
- import graphlib from '@dagrejs/graphlib'
2
1
  import { isExtendsElementView, type LikeC4View } from '@likec4/core'
3
-
4
- // '@dagrejs/graphlib' is a CommonJS module
5
- // Here is a workaround to import it
6
- const { Graph, alg } = graphlib
2
+ import { logger } from '@likec4/log'
3
+ import { first, last, values } from 'remeda'
4
+ import { findCycles, Graph, isAcyclic, postorder } from '../utils/graphlib'
7
5
 
8
6
  /**
9
7
  * Resolve rules of extended views
@@ -18,21 +16,31 @@ export function resolveRulesExtendedViews<V extends Record<any, LikeC4View>>(
18
16
  multigraph: false,
19
17
  compound: false
20
18
  })
21
- for (const view of Object.values(unresolvedViews)) {
19
+ for (const view of values(unresolvedViews)) {
22
20
  g.setNode(view.id)
23
21
  if (isExtendsElementView(view)) {
24
22
  // view -> parent
25
23
  g.setEdge(view.id, view.extends)
26
24
  }
27
25
  }
26
+ if (g.edgeCount() === 0) {
27
+ return unresolvedViews
28
+ }
28
29
 
29
30
  // Remove circular dependencies
30
- const cycles = alg.findCycles(g)
31
- if (cycles.length > 0) {
32
- cycles.flat().forEach(id => g.removeNode(id))
31
+ while (!isAcyclic(g)) {
32
+ const firstCycle = first(findCycles(g))
33
+ if (!firstCycle) {
34
+ break
35
+ }
36
+ const cycledNode = last(firstCycle)
37
+ if (!cycledNode) {
38
+ break
39
+ }
40
+ g.removeNode(cycledNode)
33
41
  }
34
42
 
35
- const ordered = alg.postorder(g, g.sources())
43
+ const ordered = postorder(g, g.sources())
36
44
 
37
45
  return ordered.reduce((acc, id) => {
38
46
  const view = unresolvedViews[id]
@@ -42,6 +50,7 @@ export function resolveRulesExtendedViews<V extends Record<any, LikeC4View>>(
42
50
  if (isExtendsElementView(view)) {
43
51
  const extendsFrom = acc[view.extends]
44
52
  if (!extendsFrom) {
53
+ logger.debug(`View "${view.id}" extends from "${view.extends}" which does not exist`)
45
54
  return acc
46
55
  }
47
56
  return Object.assign(acc, {
@@ -1,6 +1,7 @@
1
1
  import type { LikeC4View } from '@likec4/core'
2
- import { invariant } from '@likec4/core'
3
- import { filter, hasAtLeast, isTruthy, map, pipe, unique, zip } from 'remeda'
2
+ import { compareNatural, invariant } from '@likec4/core'
3
+ import { filter, hasAtLeast, isTruthy, map, pipe, unique } from 'remeda'
4
+ import { parsePath } from 'ufo'
4
5
 
5
6
  function commonAncestorPath(views: LikeC4View[], sep = '/') {
6
7
  const uniqURIs = pipe(
@@ -10,21 +11,21 @@ function commonAncestorPath(views: LikeC4View[], sep = '/') {
10
11
  unique()
11
12
  )
12
13
  if (uniqURIs.length === 0) return ''
13
- if (hasAtLeast(uniqURIs, 1) && uniqURIs.length === 1) {
14
- const parts = new URL(uniqURIs[0]).pathname.split(sep)
14
+ if (uniqURIs.length === 1) {
15
+ const parts = parsePath(uniqURIs[0]).pathname.split(sep)
15
16
  if (parts.length <= 1) return sep
16
17
  parts.pop() // remove filename
17
18
  return parts.join(sep) + sep
18
19
  }
19
20
  invariant(hasAtLeast(uniqURIs, 2), 'Expected at least 2 unique URIs')
20
21
  const [baseUri, ...tail] = uniqURIs
21
- const parts = new URL(baseUri).pathname.split(sep)
22
+ const parts = parsePath(baseUri).pathname.split(sep)
22
23
  let endOfPrefix = parts.length
23
24
  for (const uri of tail) {
24
25
  if (uri === baseUri) {
25
26
  continue
26
27
  }
27
- const compare = new URL(uri).pathname.split(sep)
28
+ const compare = parsePath(uri).pathname.split(sep)
28
29
  for (let i = 0; i < endOfPrefix; i++) {
29
30
  if (compare[i] !== parts[i]) {
30
31
  endOfPrefix = i
@@ -46,11 +47,11 @@ export function resolveRelativePaths(views: LikeC4View[]): LikeC4View[] {
46
47
  .map(view => {
47
48
  if (!view.docUri) {
48
49
  return {
49
- ...view,
50
+ view,
50
51
  parts: []
51
52
  }
52
53
  }
53
- let path = new URL(view.docUri).pathname
54
+ let path = parsePath(view.docUri).pathname
54
55
  if (commonPrefix.length > 0) {
55
56
  invariant(
56
57
  path.startsWith(commonPrefix),
@@ -61,31 +62,25 @@ export function resolveRelativePaths(views: LikeC4View[]): LikeC4View[] {
61
62
  path = path.includes(sep) ? path.slice(path.lastIndexOf(sep) + 1) : path
62
63
  }
63
64
  return {
64
- ...view,
65
+ view,
65
66
  parts: path.split(sep)
66
67
  }
67
68
  })
68
69
  // Sort views by path segments
69
70
  .sort((a, b) => {
70
- if (a.parts.length === b.parts.length) {
71
- if (a.parts.length === 0) {
72
- return 0
73
- }
74
- if (a.parts.length === 1 && hasAtLeast(a.parts, 1) && hasAtLeast(b.parts, 1)) {
75
- return a.parts[0].localeCompare(b.parts[0])
76
- }
77
- for (const [_a, _b] of zip(a.parts, b.parts)) {
78
- const compare = _a.localeCompare(_b)
79
- if (compare !== 0) {
80
- return compare
81
- }
71
+ if (a.parts.length !== b.parts.length) {
72
+ return a.parts.length - b.parts.length
73
+ }
74
+ for (let i = 0; i < a.parts.length; i++) {
75
+ const compare = compareNatural(a.parts[i], b.parts[i])
76
+ if (compare !== 0) {
77
+ return compare
82
78
  }
83
- return 0
84
79
  }
85
- return a.parts.length - b.parts.length
80
+ return compareNatural(a.view.title ?? a.view.id, b.view.title ?? b.view.id)
86
81
  })
87
82
  // Build relativePath from path segments
88
- .map(({ parts, ...view }) => {
83
+ .map(({ parts, view }) => {
89
84
  return {
90
85
  ...view,
91
86
  relativePath: parts.join(sep)
@@ -1,4 +1,4 @@
1
- import type { ComputedView } from '@likec4/core/types'
1
+ import type { ComputedView } from '@likec4/core'
2
2
  import objectHash from 'object-hash'
3
3
  import { isTruthy, map, mapToObj, pick, pipe } from 'remeda'
4
4
  import type { SetOptional } from 'type-fest'
package/src/reset.d.ts DELETED
@@ -1,2 +0,0 @@
1
- // Do not add any other lines of code to this file!
2
- import '@total-typescript/ts-reset'