@likec4/config 1.47.0 → 1.49.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.
@@ -1,24 +1,29 @@
1
+ // oxlint-disable no-unsafe-type-assertion
2
+ import type {
3
+ ElementColorValues,
4
+ RelationshipColorValues,
5
+ ThemeColorValues,
6
+ } from '@likec4/core/styles'
1
7
  import {
2
- type ColorLiteral,
3
- type ElementColorValues,
4
- type RelationshipColorValues,
5
- type ThemeColor,
6
- type ThemeColorValues,
7
8
  BorderStyles,
8
9
  computeColorValues,
9
10
  ElementShapes,
11
+ IconPositions,
10
12
  RelationshipArrowTypes,
11
13
  Sizes,
12
14
  ThemeColors,
13
15
  } from '@likec4/core/styles'
14
- import {
15
- type LikeC4ProjectStyleDefaults,
16
- type LikeC4ProjectStylesConfig,
17
- type LikeC4ProjectTheme,
18
- exact,
16
+ import type {
17
+ ColorLiteral,
18
+ CustomColor,
19
+ LikeC4ProjectStyleDefaults,
20
+ LikeC4ProjectStylesConfig,
21
+ LikeC4ProjectStylesCustomStylesheets,
22
+ LikeC4ProjectTheme,
23
+ ThemeColor,
19
24
  } from '@likec4/core/types'
20
- import { fromKeys } from 'remeda'
21
- import * as z from 'zod'
25
+ import { exact } from '@likec4/core/types'
26
+ import z from 'zod/v4'
22
27
 
23
28
  const opacity = z
24
29
  .int()
@@ -30,44 +35,74 @@ const opacity = z
30
35
  })
31
36
 
32
37
  const shape = z
33
- .literal(ElementShapes)
38
+ .enum(ElementShapes)
34
39
  .meta({ id: 'ElementShape' })
35
40
 
36
41
  const border = z
37
- .literal(BorderStyles)
42
+ .enum(BorderStyles)
38
43
  .meta({ id: 'BorderStyle' })
39
44
 
40
45
  const size = z
41
- .literal(Sizes)
46
+ .enum(Sizes)
42
47
  .meta({ id: 'ElementSize' })
43
48
 
49
+ const iconPosition = z
50
+ .enum(IconPositions)
51
+ .meta({ id: 'IconPosition' })
52
+
44
53
  const arrow = z
45
- .literal(RelationshipArrowTypes)
54
+ .enum(RelationshipArrowTypes)
46
55
  .meta({ id: 'ArrowType' })
47
56
 
48
57
  const line = z
49
- .literal(['dashed', 'solid', 'dotted'])
58
+ .enum(['dashed', 'solid', 'dotted'])
50
59
  .meta({ id: 'LineType' })
51
60
 
52
61
  const themeColor = z
53
- .literal(ThemeColors)
54
- .meta({ id: 'ThemeColor' })
62
+ .enum(ThemeColors)
63
+ .meta({ id: 'ThemeColorName' })
55
64
 
56
- const color = z
57
- .union([
58
- themeColor,
59
- z.string().min(1, 'Color name cannot be empty'),
60
- ])
65
+ const customColor = z
66
+ .custom<string & Record<never, never>>()
67
+ .refine(v => typeof v === 'string', 'Custom color name must be a string')
68
+ .transform(value => value as unknown as CustomColor)
69
+ .meta({ id: 'CustomColorName' })
70
+
71
+ // const color = z.custom<ColorNameLiteral>(
72
+ // (v) => {
73
+ // if (typeof v !== 'string' || v.length === 0) {
74
+ // throw new Error()
75
+ // }
76
+ // return v
77
+ // },
78
+ // {
79
+ // error: 'Invalid color name',
80
+ // },
81
+ // )
82
+ // .meta({ id: 'ColorName' })
83
+
84
+ const color = themeColor.or(customColor)
61
85
  .transform(value => value as ThemeColor)
62
86
  .meta({ id: 'ColorName' })
87
+ // const color = themeColor.or(
88
+ // // z.never(),
89
+ // z.string().spa(),
90
+ // ).meta({ id: 'ColorName' })
91
+ // // .custom<ColorNameLiteral>(v => typeof v === 'string')
92
+ // .union([
93
+ // themeColor,
94
+ // customColor,
95
+ // ])
96
+ // .transform(value => value as ThemeColor)
97
+ // .meta({ id: 'ColorName' })
63
98
 
64
99
  const colorValue = z
65
100
  .string()
66
101
  .min(1, 'Color value cannot be empty')
67
- .transform(value => value as ColorLiteral)
102
+ // .transform(value => value as ColorLiteral)
68
103
  .meta({
69
104
  id: 'ColorLiteral',
70
- description: 'Color value in any valid CSS format: hex, rgb, rgba, hsl, hsla ...',
105
+ // description: 'Color value in any valid CSS format: hex, rgb, rgba, hsl, hsla ...',
71
106
  })
72
107
 
73
108
  // const ColorSchema = z.union([ColorValue, LightDarkTuple])
@@ -90,86 +125,71 @@ const RelationshipColorValuesSchema = z
90
125
  label: colorSchema.meta({ description: 'Label text color' }),
91
126
  labelBg: colorSchema
92
127
  .optional()
93
- .default('rgba(0, 0, 0, 0)')
128
+ .default('rgba(0, 0, 0, 0.5)')
94
129
  .meta({ description: 'Label background color' }),
95
130
  })
96
131
  .meta({ id: 'RelationshipColorValues' })
97
132
  .transform(value => value as RelationshipColorValues)
98
133
 
99
- export const ThemeColorValuesSchema = z
100
- .union([
101
- colorSchema,
102
- z.strictObject({
103
- elements: z
104
- .union([colorSchema, ElementColorValuesSchema])
105
- .meta({ description: 'Element color value (or a breakdown of specific color values)' })
106
- .transform((value): ElementColorValues => {
107
- if (typeof value === 'string') {
108
- return computeColorValues(value).elements
109
- }
110
- return value
111
- }),
112
- relationships: z
113
- .union([colorSchema, RelationshipColorValuesSchema])
114
- .meta({ description: 'Relationship color value (or a breakdown of specific color values)' })
115
- .transform((value): RelationshipColorValues => {
116
- if (typeof value === 'string') {
117
- return computeColorValues(value).relationships
118
- }
119
- return value
120
- }),
134
+ const StrictThemeColorValuesSchema = z.strictObject({
135
+ elements: ElementColorValuesSchema
136
+ .or(
137
+ colorSchema.transform(v => computeColorValues(v as ColorLiteral).elements),
138
+ )
139
+ .meta({
140
+ description: 'Exact color value (hex, rgb, rgba, hsl, hsla ...) or break down of specific color values',
121
141
  }),
122
- ])
142
+ relationships: RelationshipColorValuesSchema
143
+ .or(
144
+ colorSchema.transform(v => computeColorValues(v as ColorLiteral).relationships),
145
+ )
146
+ .meta({ description: 'Exact color value (hex, rgb, rgba, hsl, hsla ...) or break down of specific color values' }),
147
+ })
148
+ .transform(value => value as ThemeColorValues)
149
+ .meta({
150
+ id: 'StrictThemeColorValues',
151
+ description: 'Exact color value (hex, rgb, rgba, hsl, hsla ...) or break down of specific color value',
152
+ })
153
+
154
+ export const ThemeColorValuesSchema = StrictThemeColorValuesSchema.or(
155
+ colorSchema.transform(v => computeColorValues(v as ColorLiteral)),
156
+ )
157
+ .transform(value => value as ThemeColorValues)
123
158
  .meta({
124
159
  id: 'ThemeColorValues',
125
160
  description: 'Exact value (hex, rgb, rgba, hsl, hsla ...) or break down of specific color values',
126
161
  })
127
- .transform((value): ThemeColorValues => {
128
- if (typeof value === 'string') {
129
- return computeColorValues(value)
130
- }
131
- return value
132
- })
162
+
133
163
  export type ThemeColorValuesInput = z.input<typeof ThemeColorValuesSchema>
134
164
 
135
- const ThemeColorsSchema = z.union([
136
- z.record(
137
- color,
138
- ThemeColorValuesSchema,
139
- ),
140
- z.object(
141
- fromKeys(ThemeColors, () => ThemeColorValuesSchema.optional()),
142
- ),
143
- ]).meta({
144
- id: 'ThemeColors',
145
- description: 'Override theme colors',
146
- })
165
+ const ThemeColorsSchema = z.partialRecord(
166
+ color,
167
+ ThemeColorValuesSchema,
168
+ )
169
+ .transform(value => value as Record<ThemeColor, ThemeColorValues>)
147
170
 
148
171
  const DimensionsSchema = z.strictObject({
149
172
  width: z.number().min(50),
150
173
  height: z.number().min(50),
151
174
  }).meta({
152
175
  id: 'Dimensions',
153
- description: 'Dimensions',
176
+ description: 'Defines dimensions for theme size',
154
177
  })
155
178
 
156
- const LikeC4Config_Styles_Theme_Sizes = z
157
- .strictObject(
158
- fromKeys(Sizes, () => DimensionsSchema.optional()),
159
- )
160
- .meta({
161
- id: 'ThemeSizes',
162
- description: 'Override theme sizes',
163
- })
179
+ const LikeC4Config_Styles_Theme_Sizes = z.partialRecord(size, DimensionsSchema)
164
180
 
165
181
  export const LikeC4Config_Styles_Theme = z
166
182
  .strictObject({
167
- colors: ThemeColorsSchema.optional(),
168
- sizes: LikeC4Config_Styles_Theme_Sizes.optional(),
183
+ colors: ThemeColorsSchema.optional().meta({
184
+ description: 'Override theme colors',
185
+ }),
186
+ sizes: LikeC4Config_Styles_Theme_Sizes.optional().meta({
187
+ description: 'Override theme sizes',
188
+ }),
169
189
  })
170
190
  .meta({
171
191
  id: 'ThemeCustomization',
172
- description: 'Theme customization',
192
+ description: 'Customize theme colors and sizes',
173
193
  })
174
194
  .transform(({ colors, sizes }): LikeC4ProjectTheme => {
175
195
  return exact({
@@ -189,8 +209,6 @@ const LikeC4Config_Styles_Defaults_Group = z
189
209
  })
190
210
  .meta({
191
211
  id: 'GroupDefaultStyleValues',
192
- description:
193
- 'Override default values for group style properties\nThese values will be used if such property is not defined',
194
212
  })
195
213
 
196
214
  const LikeC4Config_Styles_Defaults_Relationship = z
@@ -220,32 +238,48 @@ const LikeC4Config_Styles_Defaults = z
220
238
  }),
221
239
  size: size.optional().meta({ description: 'Default size for elements' }),
222
240
  shape: shape.optional().meta({ description: 'Default shape for elements' }),
223
- group: LikeC4Config_Styles_Defaults_Group.optional().meta({ description: 'Default style values for groups' }),
241
+ iconPosition: iconPosition.optional().meta({ description: 'Default icon position for elements' }),
242
+ group: LikeC4Config_Styles_Defaults_Group.optional().meta({
243
+ description:
244
+ 'Override default values for group style properties\nThese values will be used if such property is not defined',
245
+ }),
224
246
  relationship: LikeC4Config_Styles_Defaults_Relationship.optional().meta({
225
- description: 'Default style values for relationships',
247
+ description:
248
+ 'Override default values for relationship style properties\nThese values will be used if such property is not defined',
226
249
  }),
227
250
  })
228
251
  .meta({
229
252
  id: 'DefaultStyleValues',
230
- description:
231
- 'Override default values for style properties\nThese values will be used if such property is not defined',
253
+ })
254
+
255
+ const LikeC4Config_Styles_CustomStylesheets = z
256
+ .union([
257
+ z.string().min(1, 'Custom CSS file path cannot be empty'),
258
+ z.array(
259
+ z.string().min(1, 'Custom CSS file path cannot be empty'),
260
+ ),
261
+ ])
262
+ .meta({
263
+ id: 'CustomStylesheets',
232
264
  })
233
265
 
234
266
  export const LikeC4StylesConfigSchema = z
235
267
  .strictObject({
236
- theme: LikeC4Config_Styles_Theme.optional(),
237
- defaults: LikeC4Config_Styles_Defaults.optional(),
238
- // customCss: z
239
- // .array(z.string())
240
- // .meta({ description: 'List of custom CSS files' }),
241
- })
242
- .meta({
243
- id: 'StylesConfiguration',
244
- description: 'Project styles customization',
268
+ theme: LikeC4Config_Styles_Theme.optional().meta({
269
+ description: 'Project theme customization',
270
+ }),
271
+ defaults: LikeC4Config_Styles_Defaults.optional().meta({
272
+ description:
273
+ 'Override default values for style properties\nThese values will be used if such property is not defined',
274
+ }),
275
+ customCss: LikeC4Config_Styles_CustomStylesheets.optional().meta({
276
+ description: 'Custom CSS (or list of CSS files) to be included in the generated diagrams',
277
+ }),
245
278
  })
246
- .transform(({ theme, defaults }): LikeC4ProjectStylesConfig =>
279
+ .transform(({ theme, defaults, customCss }): LikeC4ProjectStylesConfig =>
247
280
  exact({
248
281
  defaults: normalizeDefaults(defaults),
282
+ customCss: normalizeStylesheets(customCss),
249
283
  theme,
250
284
  })
251
285
  )
@@ -262,7 +296,24 @@ function normalizeDefaults(
262
296
  const { relationship, group, ...rest } = defaults
263
297
  return exact({
264
298
  ...rest,
265
- relationship: relationship && exact(relationship) as LikeC4ProjectStyleDefaults['relationship'],
266
- group: group && exact(group) as LikeC4ProjectStyleDefaults['group'],
267
- })
299
+ relationship: relationship && exact(relationship) satisfies LikeC4ProjectStyleDefaults['relationship'],
300
+ group: group && exact(group) satisfies LikeC4ProjectStyleDefaults['group'],
301
+ }) satisfies LikeC4ProjectStyleDefaults
302
+ }
303
+
304
+ function normalizeStylesheets(
305
+ stylesheets?: z.infer<typeof LikeC4Config_Styles_CustomStylesheets>,
306
+ ): LikeC4ProjectStylesCustomStylesheets | undefined {
307
+ if (!stylesheets) {
308
+ return undefined
309
+ }
310
+ const paths = (Array.isArray(stylesheets) ? stylesheets : [stylesheets]).filter(Boolean)
311
+ if (paths.length === 0) {
312
+ return undefined
313
+ }
314
+
315
+ return {
316
+ paths,
317
+ content: '',
318
+ }
268
319
  }
package/src/schema.ts CHANGED
@@ -11,15 +11,23 @@ import type {
11
11
  ProjectId,
12
12
  } from '@likec4/core/types'
13
13
  import JSON5 from 'json5'
14
- import type { SimplifyDeep } from 'type-fest'
15
- import type { URI } from 'vscode-uri'
16
- import * as z from 'zod'
17
- import { ImageAliasesSchema, validateImageAliases } from './schema.image-alias'
18
- import { IncludeSchema, validateIncludePaths } from './schema.include'
14
+ import z from 'zod/v4'
15
+ import { ImageAliasesSchema } from './schema.image-alias'
16
+ import { type IncludeConfig, IncludeSchema } from './schema.include'
19
17
  import { LikeC4StylesConfigSchema } from './schema.theme'
20
18
 
19
+ export interface VscodeURI {
20
+ readonly scheme: string
21
+ readonly authority: string
22
+ readonly path: string
23
+ readonly fsPath: string
24
+ readonly query: string
25
+ readonly fragment: string
26
+ toString(): string
27
+ }
28
+
21
29
  export const ManualLayoutsConfigSchema = z
22
- .object({
30
+ .strictObject({
23
31
  outDir: z.string()
24
32
  .default('.likec4')
25
33
  .meta({
@@ -32,7 +40,7 @@ export const ManualLayoutsConfigSchema = z
32
40
  }),
33
41
  })
34
42
  .meta({
35
- id: 'manual-layouts-config',
43
+ id: 'ManualLayoutsConfig',
36
44
  description: 'Configuration for manual layouts',
37
45
  })
38
46
 
@@ -50,6 +58,12 @@ export const LikeC4ProjectJsonConfigSchema = z.object({
50
58
  error: 'Project name cannot contain ".", "@" or "#", try to use A-z, 0-9, _ and -',
51
59
  })
52
60
  .meta({ description: 'Project name, must be unique in the workspace' }),
61
+ extends: z.union([
62
+ z.string().min(1, 'Extend path cannot be empty'),
63
+ z.array(z.string().min(1, 'Extend path cannot be empty')).min(1, 'Extend list cannot be empty'),
64
+ ])
65
+ .optional()
66
+ .meta({ description: 'Extend styles from other config files' }),
53
67
  title: z.string()
54
68
  .nonempty('Project title cannot be empty if specified')
55
69
  .optional()
@@ -58,22 +72,22 @@ export const LikeC4ProjectJsonConfigSchema = z.object({
58
72
  .nonempty('Contact person cannot be empty if specified')
59
73
  .optional()
60
74
  .meta({ description: 'A person who has been involved in creating or maintaining this project' }),
61
- imageAliases: ImageAliasesSchema
62
- .optional(),
63
- include: IncludeSchema,
64
- styles: LikeC4StylesConfigSchema
65
- .optional(),
75
+ styles: LikeC4StylesConfigSchema.optional().meta({
76
+ description: 'Project styles customization',
77
+ }),
78
+ imageAliases: ImageAliasesSchema.optional(),
79
+ include: IncludeSchema.optional(),
66
80
  exclude: z.array(z.string())
67
81
  .optional()
68
82
  .meta({ description: 'List of file patterns to exclude from the project, default is ["**/node_modules/**"]' }),
69
- manualLayouts: ManualLayoutsConfigSchema
70
- .optional(),
83
+ manualLayouts: ManualLayoutsConfigSchema.optional(),
71
84
  })
72
85
  .meta({
73
- description: 'LikeC4 project configuration',
86
+ id: 'LikeC4ProjectConfig',
87
+ description: 'LikeC4 Project Configuration',
74
88
  })
75
89
 
76
- export type LikeC4ProjectJsonConfig = SimplifyDeep<z.input<typeof LikeC4ProjectJsonConfigSchema>>
90
+ export type LikeC4ProjectJsonConfig = z.input<typeof LikeC4ProjectJsonConfigSchema>
77
91
 
78
92
  const FunctionType = z.instanceof(Function)
79
93
  export const GeneratorsSchema = z.record(z.string(), FunctionType)
@@ -101,7 +115,7 @@ export type LocateResult = {
101
115
  /**
102
116
  * Full path to the source file
103
117
  */
104
- document: URI
118
+ document: VscodeURI
105
119
  /**
106
120
  * Document path relative to the project folder
107
121
  */
@@ -120,7 +134,7 @@ export interface GeneratorFnContext {
120
134
  /**
121
135
  * Workspace root directory
122
136
  */
123
- readonly workspace: URI
137
+ readonly workspace: VscodeURI
124
138
 
125
139
  /**
126
140
  * Current project
@@ -136,7 +150,7 @@ export interface GeneratorFnContext {
136
150
  /**
137
151
  * Project folder
138
152
  */
139
- readonly folder: URI
153
+ readonly folder: VscodeURI
140
154
  }
141
155
 
142
156
  /**
@@ -158,7 +172,7 @@ export interface GeneratorFnContext {
158
172
  * @param content - Content of the file
159
173
  */
160
174
  write(file: {
161
- path: string | string[] | URI
175
+ path: string | string[] | VscodeURI
162
176
  content:
163
177
  | string
164
178
  | NodeJS.ArrayBufferView
@@ -227,30 +241,42 @@ export type LikeC4ProjectConfig = z.infer<typeof LikeC4ProjectJsonConfigSchema>
227
241
  generators?: Record<string, GeneratorFn> | undefined
228
242
  }
229
243
 
230
- export type LikeC4ProjectConfigInput = SimplifyDeep<
231
- z.input<typeof LikeC4ProjectJsonConfigSchema> & {
232
- generators?: Record<string, GeneratorFn> | undefined
233
- }
234
- >
244
+ export type LikeC4ProjectConfigInput = LikeC4ProjectJsonConfig & {
245
+ generators?: Record<string, GeneratorFn> | undefined
246
+ }
235
247
 
236
248
  /**
237
- * Validates JSON string or JSON object into a LikeC4ProjectConfig object.
249
+ * Validates Object into a LikeC4ProjectConfig object.
238
250
  */
239
- export function validateProjectConfig<C extends string | Record<string, unknown>>(
240
- config: C,
241
- ): LikeC4ProjectConfig {
242
- const parsed = LikeC4ProjectConfigSchema.safeParse(
243
- typeof config === 'string' ? JSON5.parse(config) : config,
244
- )
245
- if (!parsed.success) {
246
- throw new Error('Config validation failed:\n' + z.prettifyError(parsed.error))
247
- }
248
- // TODO: rewrite with zod refine
249
- if (parsed.data.imageAliases) {
250
- validateImageAliases(parsed.data.imageAliases)
251
- }
252
- if (parsed.data.include) {
253
- validateIncludePaths(parsed.data.include)
251
+ export function validateProjectConfig<C extends Record<string, unknown>>(config: C): LikeC4ProjectConfig {
252
+ const parsed = LikeC4ProjectConfigSchema.safeParse(config)
253
+ if (parsed.success) {
254
+ return parsed.data as unknown as LikeC4ProjectConfig
254
255
  }
255
- return parsed.data as unknown as LikeC4ProjectConfig
256
+ throw new Error('Config validation failed:\n' + z.prettifyError(parsed.error))
257
+ }
258
+
259
+ /**
260
+ * Parses JSON string into a LikeC4ProjectConfig object.
261
+ * Does not process "extends" - use `loadConfig` function instead
262
+ */
263
+ export function parseProjectConfigJSON(config: string): LikeC4ProjectConfig {
264
+ const parsed = JSON5.parse(config.trim() || '{}')
265
+ return validateProjectConfig(parsed)
266
+ }
267
+
268
+ export const LikeC4ProjectConfigOps = {
269
+ parse: parseProjectConfigJSON,
270
+ validate: validateProjectConfig,
271
+ normalizeInclude: (include: z.input<typeof IncludeSchema> | undefined): IncludeConfig => {
272
+ const parsed = IncludeSchema.safeParse(include)
273
+ if (parsed.success) {
274
+ return parsed.data
275
+ }
276
+ return {
277
+ paths: [],
278
+ maxDepth: 3,
279
+ fileThreshold: 30,
280
+ }
281
+ },
256
282
  }