@likec4/config 1.48.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,40 +1,27 @@
1
- import { t } from "../_chunks/libs/remeda.mjs";
1
+ import { t as defu } from "../_chunks/libs/defu.mjs";
2
+ import { i as t, n as t$1, r as e, t as n } from "../_chunks/libs/remeda.mjs";
2
3
  import JSON5 from "json5";
3
4
  import z from "zod/v4";
4
5
  import { BorderStyles, ElementShapes, IconPositions, RelationshipArrowTypes, Sizes, ThemeColors, computeColorValues } from "@likec4/core/styles";
5
6
  import { exact } from "@likec4/core/types";
7
+ import { basename } from "pathe";
6
8
  import { invariant } from "@likec4/core";
7
- import { rootLogger } from "@likec4/log";
9
+ import { logger, wrapError } from "@likec4/log";
8
10
  import { bundleRequire } from "bundle-require";
11
+ import { formatMessagesSync } from "esbuild";
9
12
  import * as fs from "node:fs/promises";
10
- import { dirname } from "node:path";
11
-
12
- //#region src/schema.image-alias.ts
13
+ import { basename as basename$1, dirname, resolve } from "node:path";
13
14
  const IMAGE_ALIAS_KEY_REGEX = /^@[A-Za-z0-9_-]*$/;
14
15
  const IMAGE_ALIAS_VALUE_REGEX = /^(?!\/|[A-Za-z]:[\\\/])(?!.*:\/\/).*$/;
16
+ const ImageAliasKey = z.string().min(1, "Image alias key cannot be empty").regex(IMAGE_ALIAS_KEY_REGEX, "Image alias key must match /^@\\w+$/");
15
17
  const ImageAliasValue = z.string().min(1, "Image alias value cannot be empty").regex(IMAGE_ALIAS_VALUE_REGEX, "Image alias value must be a relative path (no leading slash or protocol)");
16
- const ImageAliasesSchema = z.record(z.string(), ImageAliasValue).meta({ description: "Map of image alias prefixes to relative paths (keys must match /^@\\w+$/; values must be relative paths without protocol or leading slash)." });
17
- function validateImageAliases(imageAliases) {
18
- const invalidKeys = [];
19
- const invalidValues = [];
20
- if (imageAliases) for (const [key, value] of Object.entries(imageAliases)) {
21
- if (!IMAGE_ALIAS_KEY_REGEX.test(key)) invalidKeys.push(key);
22
- if (!IMAGE_ALIAS_VALUE_REGEX.test(value)) invalidValues.push(`${key} -> ${value}`);
23
- }
24
- if (invalidKeys.length || invalidValues.length) {
25
- const parts = [];
26
- if (invalidKeys.length) parts.push(`Invalid image alias key(s): ${invalidKeys.map((k) => JSON.stringify(k)).join(", ")} (must match ${IMAGE_ALIAS_KEY_REGEX})`);
27
- if (invalidValues.length) parts.push(`Invalid image alias value(s): ${invalidValues.map((kv) => JSON.stringify(kv)).join(", ")} (must match ${IMAGE_ALIAS_VALUE_REGEX})`);
28
- throw new Error(parts.join(" | "));
29
- }
30
- }
31
-
32
- //#endregion
33
- //#region src/schema.include.ts
34
- const RELATIVE_PATH_REGEX = /^(?!\/|[A-Za-z]:[\\\/])(?!.*:\/\/).*$/;
35
- const IncludePathValue = z.string().min(1, "Include path cannot be empty").regex(RELATIVE_PATH_REGEX, "Include path must be a relative path (no leading slash, drive letter, or protocol)");
36
- const IncludeConfigSchema = z.strictObject({
37
- paths: z.array(IncludePathValue).min(1, "Include paths cannot be empty").meta({ description: [
18
+ const ImageAliasesSchema = z.record(ImageAliasKey, ImageAliasValue).meta({
19
+ id: "ImageAliases",
20
+ description: "Map of image alias prefixes to relative paths (keys must match /^@\\w+$/; values must be relative paths without protocol or leading slash)."
21
+ });
22
+ const IncludePathValue = z.string().min(1, "Include path cannot be empty").regex(/^(?!\/|[A-Za-z]:[\\\/])(?!.*:\/\/).*$/, "Include path must be a relative path (no leading slash, drive letter, or protocol)");
23
+ const IncludeSchema = z.strictObject({
24
+ paths: z.array(IncludePathValue).meta({ description: [
38
25
  "Additional relative directory paths to include LikeC4 source files from, searched recursively.",
39
26
  "Paths are relative to the project folder (the folder containing this config file).",
40
27
  "Example: [\"../shared\", \"../common/specs\"]"
@@ -44,54 +31,33 @@ const IncludeConfigSchema = z.strictObject({
44
31
  "Prevents excessive scanning of deeply nested directories.",
45
32
  "Default: 3"
46
33
  ].join("\n") }),
47
- fileThreshold: z.number().int().min(1).default(30).meta({ description: [
34
+ fileThreshold: z.number().int().min(1).max(1e4).default(30).meta({ description: [
48
35
  "Maximum number of files to load from include paths before warning.",
49
36
  "Helps identify performance issues from accidentally including large directories.",
50
37
  "Default: 30"
51
38
  ].join("\n") })
52
39
  }).meta({
53
40
  id: "include-config",
54
- description: "Configuration for including additional LikeC4 source files"
41
+ description: ["Configuration for including additional LikeC4 source files from other directories.", "Example: { \"paths\": [\"../shared\", \"../common/specs\"], \"maxDepth\": 5, \"fileThreshold\": 50 }"].join("\n")
55
42
  });
56
- const IncludeSchema = IncludeConfigSchema.optional().meta({ description: ["Configuration for including additional LikeC4 source files from other directories.", "Example: { \"paths\": [\"../shared\", \"../common/specs\"], \"maxDepth\": 5, \"fileThreshold\": 50 }"].join("\n") });
57
- function normalizeIncludeConfig(include) {
58
- if (!include) return {
59
- paths: [],
60
- maxDepth: 3,
61
- fileThreshold: 30
62
- };
63
- return include;
64
- }
65
- function validateIncludePaths(include) {
66
- if (!include?.paths) return;
67
- const invalidPaths = [];
68
- for (const path of include.paths) if (!RELATIVE_PATH_REGEX.test(path)) invalidPaths.push(path);
69
- if (invalidPaths.length > 0) throw new Error(`Invalid include path(s): ${invalidPaths.map((p) => JSON.stringify(p)).join(", ")} (must be relative paths without leading slash, drive letter, or protocol)`);
70
- }
71
-
72
- //#endregion
73
- //#region src/schema.theme.ts
74
43
  const opacity = z.int().min(0, "Opacity must be between 0 and 100").max(100, "Opacity must be between 0 and 100").meta({
75
44
  id: "Opacity",
76
45
  description: "Opacity 0-100%"
77
46
  });
78
- const shape = z.literal(ElementShapes).meta({ id: "ElementShape" });
79
- const border = z.literal(BorderStyles).meta({ id: "BorderStyle" });
80
- const size = z.literal(Sizes).meta({ id: "ElementSize" });
81
- const iconPosition = z.literal(IconPositions).meta({ id: "IconPosition" });
82
- const arrow = z.literal(RelationshipArrowTypes).meta({ id: "ArrowType" });
83
- const line = z.literal([
47
+ const shape = z.enum(ElementShapes).meta({ id: "ElementShape" });
48
+ const border = z.enum(BorderStyles).meta({ id: "BorderStyle" });
49
+ const size = z.enum(Sizes).meta({ id: "ElementSize" });
50
+ const iconPosition = z.enum(IconPositions).meta({ id: "IconPosition" });
51
+ const arrow = z.enum(RelationshipArrowTypes).meta({ id: "ArrowType" });
52
+ const line = z.enum([
84
53
  "dashed",
85
54
  "solid",
86
55
  "dotted"
87
56
  ]).meta({ id: "LineType" });
88
- const themeColor = z.literal(ThemeColors).meta({ id: "ThemeColor" });
89
- const customColor = z.string().min(1, "Custom color name cannot be empty").transform((value) => value).meta({ id: "CustomColorName" });
90
- const color = z.union([themeColor, customColor]).transform((value) => value).meta({ id: "ColorName" });
91
- const colorSchema = z.string().min(1, "Color value cannot be empty").transform((value) => value).meta({
92
- id: "ColorLiteral",
93
- description: "Color value in any valid CSS format: hex, rgb, rgba, hsl, hsla ..."
94
- });
57
+ const themeColor = z.enum(ThemeColors).meta({ id: "ThemeColorName" });
58
+ const customColor = z.custom().refine((v) => typeof v === "string", "Custom color name must be a string").transform((value) => value).meta({ id: "CustomColorName" });
59
+ const color = themeColor.or(customColor).transform((value) => value).meta({ id: "ColorName" });
60
+ const colorSchema = z.string().min(1, "Color value cannot be empty").meta({ id: "ColorLiteral" });
95
61
  const ElementColorValuesSchema = z.strictObject({
96
62
  fill: colorSchema.meta({ description: "Background color" }),
97
63
  stroke: colorSchema.meta({ description: "Stroke color (border, paths above background)" }),
@@ -103,44 +69,31 @@ const RelationshipColorValuesSchema = z.strictObject({
103
69
  label: colorSchema.meta({ description: "Label text color" }),
104
70
  labelBg: colorSchema.optional().default("rgba(0, 0, 0, 0.5)").meta({ description: "Label background color" })
105
71
  }).meta({ id: "RelationshipColorValues" }).transform((value) => value);
106
- const StrictThemeColorValuesSchema = z.strictObject({
107
- elements: z.union([colorSchema, ElementColorValuesSchema]).meta({ description: "Element color value (or a breakdown of specific color values)" }).transform((value) => {
108
- if (typeof value === "string") return computeColorValues(value).elements;
109
- return value;
110
- }),
111
- relationships: z.union([colorSchema, RelationshipColorValuesSchema]).meta({ description: "Relationship color value (or a breakdown of specific color values)" }).transform((value) => {
112
- if (typeof value === "string") return computeColorValues(value).relationships;
113
- return value;
114
- })
115
- }).meta({ id: "StrictThemeColorValues" });
116
- const ThemeColorValuesSchema = z.union([colorSchema, StrictThemeColorValuesSchema]).meta({
72
+ const ThemeColorValuesSchema = z.strictObject({
73
+ elements: ElementColorValuesSchema.or(colorSchema.transform((v) => computeColorValues(v).elements)).meta({ description: "Exact color value (hex, rgb, rgba, hsl, hsla ...) or break down of specific color values" }),
74
+ relationships: RelationshipColorValuesSchema.or(colorSchema.transform((v) => computeColorValues(v).relationships)).meta({ description: "Exact color value (hex, rgb, rgba, hsl, hsla ...) or break down of specific color values" })
75
+ }).transform((value) => value).meta({
76
+ id: "StrictThemeColorValues",
77
+ description: "Exact color value (hex, rgb, rgba, hsl, hsla ...) or break down of specific color value"
78
+ }).or(colorSchema.transform((v) => computeColorValues(v))).transform((value) => value).meta({
117
79
  id: "ThemeColorValues",
118
80
  description: "Exact value (hex, rgb, rgba, hsl, hsla ...) or break down of specific color values"
119
- }).transform((value) => {
120
- if (typeof value === "string") return computeColorValues(value);
121
- return value;
122
81
  });
123
- const ThemeColorsSchema = z.record(color, ThemeColorValuesSchema).meta({
124
- id: "ThemeColors",
125
- description: "Override theme colors"
126
- }).transform((value) => value);
82
+ const ThemeColorsSchema = z.partialRecord(color, ThemeColorValuesSchema).transform((value) => value);
127
83
  const DimensionsSchema = z.strictObject({
128
84
  width: z.number().min(50),
129
85
  height: z.number().min(50)
130
86
  }).meta({
131
87
  id: "Dimensions",
132
- description: "Dimensions"
133
- });
134
- const LikeC4Config_Styles_Theme_Sizes = z.strictObject(t(Sizes, () => DimensionsSchema.optional())).meta({
135
- id: "ThemeSizes",
136
- description: "Override theme sizes"
88
+ description: "Defines dimensions for theme size"
137
89
  });
90
+ const LikeC4Config_Styles_Theme_Sizes = z.partialRecord(size, DimensionsSchema);
138
91
  const LikeC4Config_Styles_Theme = z.strictObject({
139
- colors: ThemeColorsSchema.optional(),
140
- sizes: LikeC4Config_Styles_Theme_Sizes.optional()
92
+ colors: ThemeColorsSchema.optional().meta({ description: "Override theme colors" }),
93
+ sizes: LikeC4Config_Styles_Theme_Sizes.optional().meta({ description: "Override theme sizes" })
141
94
  }).meta({
142
95
  id: "ThemeCustomization",
143
- description: "Theme customization"
96
+ description: "Customize theme colors and sizes"
144
97
  }).transform(({ colors, sizes }) => {
145
98
  return exact({
146
99
  colors: colors ? exact(colors) : void 0,
@@ -151,10 +104,7 @@ const LikeC4Config_Styles_Defaults_Group = z.strictObject({
151
104
  color: color.optional().meta({ description: "Default color for groups\n(must be a valid color name from the theme)" }),
152
105
  opacity: opacity.optional().meta({ description: "Default opacity for groups" }),
153
106
  border: border.optional().meta({ description: "Default border for groups" })
154
- }).meta({
155
- id: "GroupDefaultStyleValues",
156
- description: "Override default values for group style properties\nThese values will be used if such property is not defined"
157
- });
107
+ }).meta({ id: "GroupDefaultStyleValues" });
158
108
  const LikeC4Config_Styles_Defaults_Relationship = z.strictObject({
159
109
  color: color.optional().meta({ description: "Default color for relationships\n(must be a valid color name from the theme)" }),
160
110
  line: line.optional().meta({ description: "Default line style for relationships" }),
@@ -170,23 +120,14 @@ const LikeC4Config_Styles_Defaults = z.strictObject({
170
120
  size: size.optional().meta({ description: "Default size for elements" }),
171
121
  shape: shape.optional().meta({ description: "Default shape for elements" }),
172
122
  iconPosition: iconPosition.optional().meta({ description: "Default icon position for elements" }),
173
- group: LikeC4Config_Styles_Defaults_Group.optional().meta({ description: "Default style values for groups" }),
174
- relationship: LikeC4Config_Styles_Defaults_Relationship.optional().meta({ description: "Default style values for relationships" })
175
- }).meta({
176
- id: "DefaultStyleValues",
177
- description: "Override default values for style properties\nThese values will be used if such property is not defined"
178
- });
179
- const LikeC4Config_Styles_CustomStylesheets = z.union([z.string().min(1, "Custom CSS file path cannot be empty"), z.array(z.string().min(1, "Custom CSS file path cannot be empty"))]).meta({
180
- id: "CustomStylesheets",
181
- description: "Custom CSS (or list of CSS files) to be included in the generated diagrams"
182
- });
123
+ group: LikeC4Config_Styles_Defaults_Group.optional().meta({ description: "Override default values for group style properties\nThese values will be used if such property is not defined" }),
124
+ relationship: LikeC4Config_Styles_Defaults_Relationship.optional().meta({ description: "Override default values for relationship style properties\nThese values will be used if such property is not defined" })
125
+ }).meta({ id: "DefaultStyleValues" });
126
+ const LikeC4Config_Styles_CustomStylesheets = z.union([z.string().min(1, "Custom CSS file path cannot be empty"), z.array(z.string().min(1, "Custom CSS file path cannot be empty"))]).meta({ id: "CustomStylesheets" });
183
127
  const LikeC4StylesConfigSchema = z.strictObject({
184
- theme: LikeC4Config_Styles_Theme.optional(),
185
- defaults: LikeC4Config_Styles_Defaults.optional(),
186
- customCss: LikeC4Config_Styles_CustomStylesheets.optional()
187
- }).meta({
188
- id: "StylesConfiguration",
189
- description: "Project styles customization"
128
+ theme: LikeC4Config_Styles_Theme.optional().meta({ description: "Project theme customization" }),
129
+ defaults: LikeC4Config_Styles_Defaults.optional().meta({ description: "Override default values for style properties\nThese values will be used if such property is not defined" }),
130
+ customCss: LikeC4Config_Styles_CustomStylesheets.optional().meta({ description: "Custom CSS (or list of CSS files) to be included in the generated diagrams" })
190
131
  }).transform(({ theme, defaults, customCss }) => exact({
191
132
  defaults: normalizeDefaults(defaults),
192
133
  customCss: normalizeStylesheets(customCss),
@@ -210,16 +151,13 @@ function normalizeStylesheets(stylesheets) {
210
151
  content: ""
211
152
  };
212
153
  }
213
-
214
- //#endregion
215
- //#region src/schema.ts
216
154
  const ManualLayoutsConfigSchema = z.strictObject({ outDir: z.string().default(".likec4").meta({ description: [
217
155
  "Path to the directory where manual layouts will be stored,",
218
156
  "relative to the folder containing the project config. ",
219
157
  "",
220
158
  "Defaults to '.likec4'."
221
159
  ].join("\n") }) }).meta({
222
- id: "manual-layouts-config",
160
+ id: "ManualLayoutsConfig",
223
161
  description: "Configuration for manual layouts"
224
162
  });
225
163
  const LikeC4ProjectJsonConfigSchema = z.object({
@@ -230,30 +168,42 @@ const LikeC4ProjectJsonConfigSchema = z.object({
230
168
  abort: true,
231
169
  error: "Project name cannot contain \".\", \"@\" or \"#\", try to use A-z, 0-9, _ and -"
232
170
  }).meta({ description: "Project name, must be unique in the workspace" }),
171
+ extends: z.union([z.string().min(1, "Extend path cannot be empty"), z.array(z.string().min(1, "Extend path cannot be empty")).min(1, "Extend list cannot be empty")]).optional().meta({ description: "Extend styles from other config files" }),
233
172
  title: z.string().nonempty("Project title cannot be empty if specified").optional().meta({ description: "A human readable title for the project" }),
234
173
  contactPerson: z.string().nonempty("Contact person cannot be empty if specified").optional().meta({ description: "A person who has been involved in creating or maintaining this project" }),
174
+ styles: LikeC4StylesConfigSchema.optional().meta({ description: "Project styles customization" }),
235
175
  imageAliases: ImageAliasesSchema.optional(),
236
- include: IncludeSchema,
237
- styles: LikeC4StylesConfigSchema.optional(),
176
+ include: IncludeSchema.optional(),
238
177
  exclude: z.array(z.string()).optional().meta({ description: "List of file patterns to exclude from the project, default is [\"**/node_modules/**\"]" }),
239
178
  manualLayouts: ManualLayoutsConfigSchema.optional()
240
- }).meta({ description: "LikeC4 project configuration" });
179
+ }).meta({
180
+ id: "LikeC4ProjectConfig",
181
+ description: "LikeC4 Project Configuration"
182
+ });
241
183
  const FunctionType = z.instanceof(Function);
242
184
  const GeneratorsSchema = z.record(z.string(), FunctionType);
243
185
  const LikeC4ProjectConfigSchema = LikeC4ProjectJsonConfigSchema.extend({ generators: GeneratorsSchema.optional() });
244
- /**
245
- * Validates JSON string or JSON object into a LikeC4ProjectConfig object.
246
- */
247
186
  function validateProjectConfig(config) {
248
- const parsed = LikeC4ProjectConfigSchema.safeParse(typeof config === "string" ? JSON5.parse(config) : config);
249
- if (!parsed.success) throw new Error("Config validation failed:\n" + z.prettifyError(parsed.error));
250
- if (parsed.data.imageAliases) validateImageAliases(parsed.data.imageAliases);
251
- if (parsed.data.include) validateIncludePaths(parsed.data.include);
252
- return parsed.data;
187
+ const parsed = LikeC4ProjectConfigSchema.safeParse(config);
188
+ if (parsed.success) return parsed.data;
189
+ throw new Error("Config validation failed:\n" + z.prettifyError(parsed.error));
190
+ }
191
+ function parseProjectConfigJSON(config) {
192
+ return validateProjectConfig(JSON5.parse(config.trim() || "{}"));
253
193
  }
254
-
255
- //#endregion
256
- //#region src/filenames.ts
194
+ const LikeC4ProjectConfigOps = {
195
+ parse: parseProjectConfigJSON,
196
+ validate: validateProjectConfig,
197
+ normalizeInclude: (include) => {
198
+ const parsed = IncludeSchema.safeParse(include);
199
+ if (parsed.success) return parsed.data;
200
+ return {
201
+ paths: [],
202
+ maxDepth: 3,
203
+ fileThreshold: 30
204
+ };
205
+ }
206
+ };
257
207
  const configJsonFilenames = [
258
208
  ".likec4rc",
259
209
  ".likec4.config.json",
@@ -268,195 +218,115 @@ const configNonJsonFilenames = [
268
218
  "likec4.config.mts"
269
219
  ];
270
220
  const ConfigFilenames = [...configJsonFilenames, ...configNonJsonFilenames];
271
- /**
272
- * Checks if the given filename is a LikeC4 JSON config file (JSON, RC).
273
- */
274
221
  function isLikeC4JsonConfig(filename) {
275
- for (const ext of configJsonFilenames) if (filename.endsWith(ext)) return true;
276
- return false;
222
+ return configJsonFilenames.includes(basename(filename));
277
223
  }
278
- /**
279
- * Checks if the given filename is a LikeC4 non-JSON config file (JS, MJS, TS, MTS)
280
- */
281
224
  function isLikeC4NonJsonConfig(filename) {
282
- for (const ext of configNonJsonFilenames) if (filename.endsWith(ext)) return true;
283
- return false;
225
+ return configNonJsonFilenames.includes(basename(filename));
284
226
  }
285
- /**
286
- * Checks if the given filename is a LikeC4 config file (JSON or non-JSON)
287
- */
288
227
  function isLikeC4Config(filename) {
289
228
  return isLikeC4JsonConfig(filename) || isLikeC4NonJsonConfig(filename);
290
229
  }
291
-
292
- //#endregion
293
- //#region src/define-config.ts
294
- /**
295
- * Defines LikeC4 Project, allows custom generators that can be executed using CLI:
296
- *
297
- * `$ likec4 gen <generator-name>`
298
- *
299
- * or VSCode command `LikeC4: Run code generator`
300
- *
301
- * @example
302
- * ```ts
303
- * export default defineConfig({
304
- * name: 'my-project',
305
- * title: 'My Project',
306
- *
307
- * exclude: ['picomatch pattern'],
308
- * generators: {
309
- * '<generator-name>': async ({ likec4model, ctx }) => {
310
- * await ctx.write('my-generator.txt', likec4model.project.id)
311
- * }
312
- * }
313
- * })
314
- * ```
315
- */
316
230
  function defineConfig(config) {
317
231
  return LikeC4ProjectConfigSchema.parse(config);
318
232
  }
319
- /**
320
- * Define reusable custom generators
321
- *
322
- * @example
323
- * ```ts
324
- * // generators.ts
325
- * export default defineGenerators({
326
- * 'my-generator': async ({ likec4model, ctx }) => {
327
- * await ctx.write('my-generator.txt', likec4model.project.id)
328
- * }
329
- * })
330
- *
331
- * // likec4.config.ts
332
- * import generators from './generators'
333
- *
334
- * export default defineConfig({
335
- * name: 'my-project',
336
- * generators,
337
- * })
338
- * ```
339
- */
340
233
  function defineGenerators(generators) {
341
234
  return GeneratorsSchema.parse(generators);
342
235
  }
343
- /**
344
- * Define reusable custom theme color
345
- * @example
346
- * ```ts
347
- * export default defineThemeColor({
348
- * element: {
349
- * fill: 'red'
350
- * }
351
- * })
352
- * ```
353
- */
354
236
  function defineThemeColor(colors) {
355
237
  return ThemeColorValuesSchema.parse(colors);
356
238
  }
357
- /**
358
- * Define reusable custom theme
359
- * @example
360
- * ```ts
361
- * import { defineThemeColor, defineTheme } from 'likec4/config'
362
- *
363
- * export default defineTheme({
364
- * colors: {
365
- * primary: '#FF0000',
366
- * // Or use defineThemeColor
367
- * red: defineThemeColor({
368
- * elements: {
369
- * fill: 'red'
370
- * }
371
- * })
372
- * }
373
- * })
374
- * ```
375
- */
376
239
  function defineTheme(theme) {
377
240
  return LikeC4Config_Styles_Theme.parse(theme);
378
241
  }
379
- /**
380
- * Define reusable custom style
381
- * @example
382
- * ```ts
383
- * import { defineStyle, defineThemeColor } from 'likec4/config'
384
- *
385
- * export default defineStyle({
386
- * theme: {
387
- * colors: {
388
- * red: defineThemeColor({
389
- * elements: {
390
- * fill: 'red'
391
- * }
392
- * })
393
- * }
394
- * },
395
- * defaults: {
396
- * color: 'red',
397
- * opacity: 50,
398
- * border: 'solid',
399
- * size: 'sm',
400
- * relationship: {
401
- * color: 'grey',
402
- * line: 'solid',
403
- * }
404
- * }
405
- * })
406
- */
407
242
  function defineStyle(styles) {
408
243
  return LikeC4StylesConfigSchema.parse(styles);
409
244
  }
410
-
411
- //#endregion
412
- //#region src/node/load-config.ts
413
- /**
414
- * Load LikeC4 Project config file.
415
- * If filepath is a non-JSON file, it will be bundled and required
416
- */
245
+ const JsonConfigInputSchema = LikeC4ProjectJsonConfigSchema.pick({
246
+ extends: true,
247
+ styles: true
248
+ }).loose();
249
+ const normalizeExtends = (value) => {
250
+ if (!value) return [];
251
+ return Array.isArray(value) ? value : [value];
252
+ };
253
+ const parseJsonConfig = async (filepath) => {
254
+ const content = await fs.readFile(filepath, "utf-8");
255
+ let parsed;
256
+ try {
257
+ parsed = JSON5.parse(content.trim() || "{}");
258
+ } catch (e) {
259
+ throw wrapError(e, `${filepath}:`);
260
+ }
261
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${filepath}: Config must be a JSON object`);
262
+ const result = JsonConfigInputSchema.safeParse(parsed);
263
+ if (!result.success) throw new Error(`${filepath}: Invalid config\n` + z.prettifyError(result.error));
264
+ return result.data;
265
+ };
266
+ const loadJsonConfigs = async (filepath, stack) => {
267
+ if (stack.includes(filepath)) {
268
+ const cycleStart = stack.indexOf(filepath);
269
+ const cycle = [...stack.slice(cycleStart), filepath].join(" -> ");
270
+ throw new Error(`Config extends cycle detected: ${cycle}`);
271
+ }
272
+ const parsed = await parseJsonConfig(filepath);
273
+ const extendsPaths = normalizeExtends(parsed.extends);
274
+ const nextStack = [...stack, filepath];
275
+ const configs = [];
276
+ for (const extendPath of extendsPaths) {
277
+ const resolvedPath = resolve(dirname(filepath), extendPath);
278
+ configs.push(...await loadJsonConfigs(resolvedPath, nextStack));
279
+ }
280
+ return [...configs, parsed];
281
+ };
417
282
  async function loadConfig(filepath) {
418
- const logger = rootLogger.getChild("config");
419
- logger.debug`Loading config file: ${filepath.fsPath}`;
420
- if (isLikeC4JsonConfig(filepath.fsPath)) try {
421
- return validateProjectConfig(await fs.readFile(filepath.fsPath, "utf-8"));
422
- } catch (err) {
423
- logger.error(`Failed to load json config file: ${filepath.fsPath}`, { err });
424
- throw err;
283
+ filepath = typeof filepath === "string" ? filepath : filepath.fsPath;
284
+ logger.getChild("config").debug`Loading config: ${filepath}`;
285
+ const folder = dirname(filepath);
286
+ const filename = basename$1(filepath);
287
+ const implicitcfg = { name: basename$1(folder) };
288
+ if (isLikeC4JsonConfig(filename)) {
289
+ const configs = await loadJsonConfigs(resolve(filepath), []);
290
+ invariant(t(configs, 1), "Expect at least one config");
291
+ const rootConfig = n(t$1(configs), ["extends", "styles"]);
292
+ const stylesChain = configs.map((config) => config.styles).filter(e);
293
+ const mergedStyles = stylesChain.length > 0 ? defu({}, ...stylesChain.reverse()) : void 0;
294
+ return validateProjectConfig({
295
+ ...implicitcfg,
296
+ ...rootConfig,
297
+ ...mergedStyles ? { styles: mergedStyles } : {}
298
+ });
425
299
  }
426
- invariant(isLikeC4NonJsonConfig(filepath.fsPath), `Invalid config file: ${filepath.fsPath}`);
427
- try {
428
- const cwd = dirname(filepath.fsPath);
429
- const { mod } = await bundleRequire({
430
- filepath: filepath.fsPath,
431
- cwd,
432
- esbuildOptions: {
433
- resolveExtensions: [
434
- ".ts",
435
- ".mts",
436
- ".cts",
437
- ".mjs",
438
- ".js",
439
- ".cjs"
440
- ],
441
- plugins: [{
442
- name: "likec4-config",
443
- setup(build) {
444
- /**
445
- * Intercept @likec4/config and likec4/config imports
446
- */
447
- build.onResolve({ filter: /^@?likec4\/config$/ }, (args) => ({
448
- path: args.path,
449
- namespace: "likec4-config"
450
- }));
451
- /**
452
- * Mock implementation, this allows to skip redundant bundling @likec4/config
453
- */
454
- build.onLoad({
455
- filter: /.*/,
456
- namespace: "likec4-config"
457
- }, (_args) => {
458
- return {
459
- contents: `
300
+ invariant(isLikeC4NonJsonConfig(filename), `Invalid name for config file: ${filepath}`);
301
+ const { mod } = await bundleRequire({
302
+ filepath,
303
+ cwd: folder,
304
+ esbuildOptions: {
305
+ resolveExtensions: [
306
+ ".ts",
307
+ ".mts",
308
+ ".cts",
309
+ ".mjs",
310
+ ".js",
311
+ ".cjs"
312
+ ],
313
+ plugins: [{
314
+ name: "likec4-config",
315
+ setup(build) {
316
+ build.onResolve({ filter: /^@?likec4\/config$/ }, (args) => ({
317
+ path: args.path,
318
+ namespace: "likec4-config"
319
+ }));
320
+ build.onEnd((result) => {
321
+ const messages = formatMessagesSync(result.errors, { kind: "error" });
322
+ for (const message of messages) logger.error(message);
323
+ });
324
+ build.onLoad({
325
+ filter: /.*/,
326
+ namespace: "likec4-config"
327
+ }, (_args) => {
328
+ return {
329
+ contents: `
460
330
  // Mock implementation to allow loading config files without bundling @likec4/config
461
331
  function mock(x) { return x }
462
332
  export {
@@ -466,19 +336,13 @@ export {
466
336
  mock as defineTheme,
467
337
  mock as defineThemeColor,
468
338
  }`,
469
- loader: "js"
470
- };
471
- });
472
- }
473
- }]
474
- }
475
- });
476
- return defineConfig(mod?.default ?? mod);
477
- } catch (err) {
478
- logger.error(`Failed to load config file: ${filepath.fsPath}`, { err });
479
- throw err;
480
- }
339
+ loader: "js"
340
+ };
341
+ });
342
+ }
343
+ }]
344
+ }
345
+ });
346
+ return validateProjectConfig(Object.assign(implicitcfg, mod?.default ?? mod));
481
347
  }
482
-
483
- //#endregion
484
- export { ConfigFilenames, defineConfig, defineGenerators, defineStyle, defineTheme, defineThemeColor, isLikeC4Config, isLikeC4JsonConfig, isLikeC4NonJsonConfig, loadConfig, normalizeIncludeConfig, validateIncludePaths, validateProjectConfig };
348
+ export { ConfigFilenames, LikeC4ProjectConfigOps, defineConfig, defineGenerators, defineStyle, defineTheme, defineThemeColor, isLikeC4Config, isLikeC4JsonConfig, isLikeC4NonJsonConfig, loadConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likec4/config",
3
- "version": "1.48.0",
3
+ "version": "1.49.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://likec4.dev",
6
6
  "author": "Denis Davydkov <denis@davydkov.com>",
@@ -54,14 +54,15 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "json5": "^2.2.3",
57
+ "pathe": "^2.0.3",
57
58
  "zod": "^3.25.76",
58
59
  "type-fest": "^4.41.0",
59
- "@likec4/core": "1.48.0",
60
- "@likec4/log": "1.48.0"
60
+ "@likec4/log": "1.49.0",
61
+ "@likec4/core": "1.49.0"
61
62
  },
62
63
  "peerDependencies": {
63
64
  "bundle-require": "^5.1.0",
64
- "esbuild": "0.27.2"
65
+ "esbuild": "0.27.3"
65
66
  },
66
67
  "peerDependenciesMeta": {
67
68
  "esbuild": {
@@ -72,19 +73,21 @@
72
73
  }
73
74
  },
74
75
  "devDependencies": {
75
- "@types/node": "~22.19.7",
76
- "remeda": "^2.32.0",
76
+ "@types/node": "~22.19.10",
77
+ "remeda": "^2.33.5",
77
78
  "defu": "^6.1.4",
79
+ "ufo": "1.6.3",
78
80
  "tsx": "4.21.0",
79
- "turbo": "2.7.6",
81
+ "turbo": "2.8.3",
80
82
  "typescript": "5.9.3",
81
- "obuild": "^0.4.20",
83
+ "obuild": "^0.4.27",
82
84
  "nano-spawn": "^2.0.0",
83
85
  "vitest": "4.0.18",
84
- "@likec4/tsconfig": "1.48.0",
85
- "@likec4/devops": "1.42.0"
86
+ "@likec4/devops": "1.42.0",
87
+ "@likec4/tsconfig": "1.49.0"
86
88
  },
87
89
  "scripts": {
90
+ "generate": "tsx --conditions=sources scripts/generate.mts",
88
91
  "typecheck": "tsc -b --verbose",
89
92
  "build": "obuild",
90
93
  "lint:package": "pnpx publint ./package.tgz",