@likec4/config 1.49.0 → 1.50.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.
package/README.md CHANGED
@@ -180,6 +180,7 @@ Fields:
180
180
  - `name` (required): unique project id within the workspace
181
181
  - `title` (optional): human-readable project title
182
182
  - `contactPerson` (optional): maintainer/author
183
+ - `metadata` (optional): arbitrary project-level key-value pairs
183
184
  - `extends` (optional): string or array of strings, paths to JSON configs to merge `styles` from (relative to each config file)
184
185
  - `styles` (optional): theme/defaults/customCss customization
185
186
  - `exclude` (optional): array of glob patterns (picomatch) to exclude (defaults to `['**/node_modules/**']`)
@@ -1,33 +1,33 @@
1
- function e$1(e, t, n) {
1
+ function e$2(e, t, n) {
2
2
  let r = (n) => e(n, ...t);
3
3
  return n === void 0 ? r : Object.assign(r, {
4
4
  lazy: n,
5
5
  lazyArgs: t
6
6
  });
7
7
  }
8
- function t$2(t, n, r) {
8
+ function t$3(t, n, r) {
9
9
  let i = t.length - n.length;
10
10
  if (i === 0) return t(...n);
11
- if (i === 1) return e$1(t, n, r);
11
+ if (i === 1) return e$2(t, n, r);
12
12
  throw Error(`Wrong number of arguments`);
13
13
  }
14
- function t$1(...t) {
15
- return t$2(n$2, t);
14
+ function t$2(...t) {
15
+ return t$3(n$2, t);
16
16
  }
17
17
  const n$2 = (e, t) => e.length >= t;
18
- function e(e) {
18
+ function e$1(e) {
19
19
  return e != null;
20
20
  }
21
- function t(...t) {
22
- return t$2(n$1, t);
21
+ function t$1(...t) {
22
+ return t$3(n$1, t);
23
23
  }
24
24
  const n$1 = (e) => e.at(-1);
25
25
  function n(...t) {
26
- return t$2(r, t);
26
+ return t$3(r, t);
27
27
  }
28
28
  function r(e, n) {
29
- if (!t$1(n, 1)) return { ...e };
30
- if (!t$1(n, 2)) {
29
+ if (!t$2(n, 1)) return { ...e };
30
+ if (!t$2(n, 2)) {
31
31
  let { [n[0]]: t, ...r } = e;
32
32
  return r;
33
33
  }
@@ -35,4 +35,10 @@ function r(e, n) {
35
35
  for (let e of n) delete r[e];
36
36
  return r;
37
37
  }
38
- export { t$1 as i, t as n, e as r, n as t };
38
+ new Set([
39
+ `-`,
40
+ `_`,
41
+ ...` .
42
+ .\v.\f.\r. .….\xA0. . . . . . . . . . . . .\u2028.\u2029. . . .`.split(`.`)
43
+ ]);
44
+ export { t$2 as i, t$1 as n, e$1 as r, n as t };
package/dist/index.d.mts CHANGED
@@ -25,6 +25,7 @@ declare const LikeC4ProjectJsonConfigSchema: z.ZodObject<{
25
25
  extends: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
26
26
  title: z.ZodOptional<z.ZodString>;
27
27
  contactPerson: z.ZodOptional<z.ZodString>;
28
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
28
29
  styles: z.ZodOptional<z.ZodPipe<z.ZodObject<{
29
30
  theme: z.ZodOptional<z.ZodPipe<z.ZodObject<{
30
31
  colors: z.ZodOptional<z.ZodPipe<z.ZodRecord<z.ZodPipe<z.ZodUnion<[z.ZodEnum<{
@@ -140,6 +141,8 @@ declare const LikeC4ProjectJsonConfigSchema: z.ZodObject<{
140
141
  manualLayouts: z.ZodOptional<z.ZodObject<{
141
142
  outDir: z.ZodDefault<z.ZodString>;
142
143
  }, z.core.$strict>>;
144
+ inferTechnologyFromIcon: z.ZodOptional<z.ZodBoolean>;
145
+ implicitViews: z.ZodOptional<z.ZodBoolean>;
143
146
  }, z.core.$strip>;
144
147
  type LikeC4ProjectJsonConfig = z.input<typeof LikeC4ProjectJsonConfigSchema>;
145
148
  /**
@@ -460,21 +463,18 @@ interface LikeC4StylesConfig extends z.infer<typeof LikeC4StylesConfigSchema> {}
460
463
  type LikeC4StylesConfigInput = z.input<typeof LikeC4StylesConfigSchema>;
461
464
  //#endregion
462
465
  //#region src/filenames.d.ts
463
- declare const configJsonFilenames: readonly [".likec4rc", ".likec4.config.json", "likec4.config.json"];
464
- declare const configNonJsonFilenames: readonly ["likec4.config.js", "likec4.config.cjs", "likec4.config.mjs", "likec4.config.ts", "likec4.config.cts", "likec4.config.mts"];
466
+ /** All known LikeC4 config filenames (JSON and non-JSON). */
465
467
  declare const ConfigFilenames: readonly [".likec4rc", ".likec4.config.json", "likec4.config.json", "likec4.config.js", "likec4.config.cjs", "likec4.config.mjs", "likec4.config.ts", "likec4.config.cts", "likec4.config.mts"];
468
+ /** Returns true if the **basename** of the given path matches a known config filename. */
469
+ declare function isLikeC4JsonConfig(filename: string): boolean;
466
470
  /**
467
- * Checks if the given filename is a LikeC4 JSON config file (JSON, RC).
471
+ * Returns true if the **basename** of the given path matches a known non-JSON config filename (JS, MJS, TS, MTS).
468
472
  */
469
- declare function isLikeC4JsonConfig(filename: string): filename is typeof configJsonFilenames[number];
473
+ declare function isLikeC4NonJsonConfig(filename: string): boolean;
470
474
  /**
471
- * Checks if the given filename is a LikeC4 non-JSON config file (JS, MJS, TS, MTS)
475
+ * Returns true if the **basename** of the given path matches a known LikeC4 config file (JSON or non-JSON).
472
476
  */
473
- declare function isLikeC4NonJsonConfig(filename: string): filename is typeof configNonJsonFilenames[number];
474
- /**
475
- * Checks if the given filename is a LikeC4 config file (JSON or non-JSON)
476
- */
477
- declare function isLikeC4Config(filename: string): filename is typeof ConfigFilenames[number];
477
+ declare function isLikeC4Config(filename: string): boolean;
478
478
  //#endregion
479
479
  //#region src/define-config.d.ts
480
480
  /**
package/dist/index.mjs CHANGED
@@ -2,7 +2,6 @@ import JSON5 from "json5";
2
2
  import z from "zod/v4";
3
3
  import { BorderStyles, ElementShapes, IconPositions, RelationshipArrowTypes, Sizes, ThemeColors, computeColorValues } from "@likec4/core/styles";
4
4
  import { exact } from "@likec4/core/types";
5
- import { basename } from "pathe";
6
5
  const IMAGE_ALIAS_KEY_REGEX = /^@[A-Za-z0-9_-]*$/;
7
6
  const IMAGE_ALIAS_VALUE_REGEX = /^(?!\/|[A-Za-z]:[\\\/])(?!.*:\/\/).*$/;
8
7
  const ImageAliasKey = z.string().min(1, "Image alias key cannot be empty").regex(IMAGE_ALIAS_KEY_REGEX, "Image alias key must match /^@\\w+$/");
@@ -163,11 +162,18 @@ const LikeC4ProjectJsonConfigSchema = z.object({
163
162
  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" }),
164
163
  title: z.string().nonempty("Project title cannot be empty if specified").optional().meta({ description: "A human readable title for the project" }),
165
164
  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" }),
165
+ metadata: z.record(z.string(), z.any()).optional().meta({ description: "Arbitrary metadata as key-value pairs for custom project information" }),
166
166
  styles: LikeC4StylesConfigSchema.optional().meta({ description: "Project styles customization" }),
167
167
  imageAliases: ImageAliasesSchema.optional(),
168
168
  include: IncludeSchema.optional(),
169
169
  exclude: z.array(z.string()).optional().meta({ description: "List of file patterns to exclude from the project, default is [\"**/node_modules/**\"]" }),
170
- manualLayouts: ManualLayoutsConfigSchema.optional()
170
+ manualLayouts: ManualLayoutsConfigSchema.optional(),
171
+ inferTechnologyFromIcon: z.boolean().optional().meta({ description: [
172
+ "Automatically derive element technology from icon name when technology is not set explicitly.",
173
+ "Applies to aws:, azure:, gcp:, and tech: icons. Bootstrap icons are excluded.",
174
+ "Defaults to true."
175
+ ].join("\n") }),
176
+ implicitViews: z.boolean().optional().meta({ description: "Auto-generate scoped views for elements without explicit views. Defaults to true." })
171
177
  }).meta({
172
178
  id: "LikeC4ProjectConfig",
173
179
  description: "LikeC4 Project Configuration"
@@ -175,11 +181,18 @@ const LikeC4ProjectJsonConfigSchema = z.object({
175
181
  const FunctionType = z.instanceof(Function);
176
182
  const GeneratorsSchema = z.record(z.string(), FunctionType);
177
183
  const LikeC4ProjectConfigSchema = LikeC4ProjectJsonConfigSchema.extend({ generators: GeneratorsSchema.optional() });
184
+ /**
185
+ * Validates Object into a LikeC4ProjectConfig object.
186
+ */
178
187
  function validateProjectConfig(config) {
179
188
  const parsed = LikeC4ProjectConfigSchema.safeParse(config);
180
189
  if (parsed.success) return parsed.data;
181
190
  throw new Error("Config validation failed:\n" + z.prettifyError(parsed.error));
182
191
  }
192
+ /**
193
+ * Parses JSON string into a LikeC4ProjectConfig object.
194
+ * Does not process "extends" - use `loadConfig` function instead
195
+ */
183
196
  function parseProjectConfigJSON(config) {
184
197
  return validateProjectConfig(JSON5.parse(config.trim() || "{}"));
185
198
  }
@@ -196,11 +209,29 @@ const LikeC4ProjectConfigOps = {
196
209
  };
197
210
  }
198
211
  };
212
+ /** Trim trailing slashes and backslashes (no regex, avoids S5852 ReDoS). */
213
+ function trimTrailingSlashes(s) {
214
+ let end = s.length;
215
+ while (end > 0 && (s[end - 1] === "/" || s[end - 1] === "\\")) end--;
216
+ return s.slice(0, end);
217
+ }
218
+ /** Split by / or \ without regex (avoids S5852 ReDoS). */
219
+ function splitPath(s) {
220
+ return s.split("/").flatMap((part) => part.split("\\"));
221
+ }
222
+ /** basename compatible with Node and browser (no node:path for Vite/playground bundle). */
223
+ function basename(path) {
224
+ const trimmed = trimTrailingSlashes(path);
225
+ const segments = splitPath(trimmed);
226
+ return segments[segments.length - 1] || trimmed;
227
+ }
228
+ /** Known LikeC4 JSON config filenames (RC and .json). */
199
229
  const configJsonFilenames = [
200
230
  ".likec4rc",
201
231
  ".likec4.config.json",
202
232
  "likec4.config.json"
203
233
  ];
234
+ /** Known LikeC4 non-JSON config filenames (JS, MJS, TS, MTS). */
204
235
  const configNonJsonFilenames = [
205
236
  "likec4.config.js",
206
237
  "likec4.config.cjs",
@@ -209,28 +240,137 @@ const configNonJsonFilenames = [
209
240
  "likec4.config.cts",
210
241
  "likec4.config.mts"
211
242
  ];
243
+ /** All known LikeC4 config filenames (JSON and non-JSON). */
212
244
  const ConfigFilenames = [...configJsonFilenames, ...configNonJsonFilenames];
245
+ /** Returns true if the **basename** of the given path matches a known config filename. */
213
246
  function isLikeC4JsonConfig(filename) {
214
247
  return configJsonFilenames.includes(basename(filename));
215
248
  }
249
+ /**
250
+ * Returns true if the **basename** of the given path matches a known non-JSON config filename (JS, MJS, TS, MTS).
251
+ */
216
252
  function isLikeC4NonJsonConfig(filename) {
217
253
  return configNonJsonFilenames.includes(basename(filename));
218
254
  }
255
+ /**
256
+ * Returns true if the **basename** of the given path matches a known LikeC4 config file (JSON or non-JSON).
257
+ */
219
258
  function isLikeC4Config(filename) {
220
259
  return isLikeC4JsonConfig(filename) || isLikeC4NonJsonConfig(filename);
221
260
  }
261
+ /**
262
+ * Defines LikeC4 Project, allows custom generators that can be executed using CLI:
263
+ *
264
+ * `$ likec4 gen <generator-name>`
265
+ *
266
+ * or VSCode command `LikeC4: Run code generator`
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * export default defineConfig({
271
+ * name: 'my-project',
272
+ * title: 'My Project',
273
+ *
274
+ * exclude: ['picomatch pattern'],
275
+ * generators: {
276
+ * '<generator-name>': async ({ likec4model, ctx }) => {
277
+ * await ctx.write('my-generator.txt', likec4model.project.id)
278
+ * }
279
+ * }
280
+ * })
281
+ * ```
282
+ */
222
283
  function defineConfig(config) {
223
284
  return LikeC4ProjectConfigSchema.parse(config);
224
285
  }
286
+ /**
287
+ * Define reusable custom generators
288
+ *
289
+ * @example
290
+ * ```ts
291
+ * // generators.ts
292
+ * export default defineGenerators({
293
+ * 'my-generator': async ({ likec4model, ctx }) => {
294
+ * await ctx.write('my-generator.txt', likec4model.project.id)
295
+ * }
296
+ * })
297
+ *
298
+ * // likec4.config.ts
299
+ * import generators from './generators'
300
+ *
301
+ * export default defineConfig({
302
+ * name: 'my-project',
303
+ * generators,
304
+ * })
305
+ * ```
306
+ */
225
307
  function defineGenerators(generators) {
226
308
  return GeneratorsSchema.parse(generators);
227
309
  }
310
+ /**
311
+ * Define reusable custom theme color
312
+ * @example
313
+ * ```ts
314
+ * export default defineThemeColor({
315
+ * element: {
316
+ * fill: 'red'
317
+ * }
318
+ * })
319
+ * ```
320
+ */
228
321
  function defineThemeColor(colors) {
229
322
  return ThemeColorValuesSchema.parse(colors);
230
323
  }
324
+ /**
325
+ * Define reusable custom theme
326
+ * @example
327
+ * ```ts
328
+ * import { defineThemeColor, defineTheme } from 'likec4/config'
329
+ *
330
+ * export default defineTheme({
331
+ * colors: {
332
+ * primary: '#FF0000',
333
+ * // Or use defineThemeColor
334
+ * red: defineThemeColor({
335
+ * elements: {
336
+ * fill: 'red'
337
+ * }
338
+ * })
339
+ * }
340
+ * })
341
+ * ```
342
+ */
231
343
  function defineTheme(theme) {
232
344
  return LikeC4Config_Styles_Theme.parse(theme);
233
345
  }
346
+ /**
347
+ * Define reusable custom style
348
+ * @example
349
+ * ```ts
350
+ * import { defineStyle, defineThemeColor } from 'likec4/config'
351
+ *
352
+ * export default defineStyle({
353
+ * theme: {
354
+ * colors: {
355
+ * red: defineThemeColor({
356
+ * elements: {
357
+ * fill: 'red'
358
+ * }
359
+ * })
360
+ * }
361
+ * },
362
+ * defaults: {
363
+ * color: 'red',
364
+ * opacity: 50,
365
+ * border: 'solid',
366
+ * size: 'sm',
367
+ * relationship: {
368
+ * color: 'grey',
369
+ * line: 'solid',
370
+ * }
371
+ * }
372
+ * })
373
+ */
234
374
  function defineStyle(styles) {
235
375
  return LikeC4StylesConfigSchema.parse(styles);
236
376
  }
@@ -25,6 +25,7 @@ declare const LikeC4ProjectJsonConfigSchema: z.ZodObject<{
25
25
  extends: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
26
26
  title: z.ZodOptional<z.ZodString>;
27
27
  contactPerson: z.ZodOptional<z.ZodString>;
28
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
28
29
  styles: z.ZodOptional<z.ZodPipe<z.ZodObject<{
29
30
  theme: z.ZodOptional<z.ZodPipe<z.ZodObject<{
30
31
  colors: z.ZodOptional<z.ZodPipe<z.ZodRecord<z.ZodPipe<z.ZodUnion<[z.ZodEnum<{
@@ -140,6 +141,8 @@ declare const LikeC4ProjectJsonConfigSchema: z.ZodObject<{
140
141
  manualLayouts: z.ZodOptional<z.ZodObject<{
141
142
  outDir: z.ZodDefault<z.ZodString>;
142
143
  }, z.core.$strict>>;
144
+ inferTechnologyFromIcon: z.ZodOptional<z.ZodBoolean>;
145
+ implicitViews: z.ZodOptional<z.ZodBoolean>;
143
146
  }, z.core.$strip>;
144
147
  type LikeC4ProjectJsonConfig = z.input<typeof LikeC4ProjectJsonConfigSchema>;
145
148
  /**
@@ -460,21 +463,18 @@ interface LikeC4StylesConfig extends z.infer<typeof LikeC4StylesConfigSchema> {}
460
463
  type LikeC4StylesConfigInput = z.input<typeof LikeC4StylesConfigSchema>;
461
464
  //#endregion
462
465
  //#region src/filenames.d.ts
463
- declare const configJsonFilenames: readonly [".likec4rc", ".likec4.config.json", "likec4.config.json"];
464
- declare const configNonJsonFilenames: readonly ["likec4.config.js", "likec4.config.cjs", "likec4.config.mjs", "likec4.config.ts", "likec4.config.cts", "likec4.config.mts"];
466
+ /** All known LikeC4 config filenames (JSON and non-JSON). */
465
467
  declare const ConfigFilenames: readonly [".likec4rc", ".likec4.config.json", "likec4.config.json", "likec4.config.js", "likec4.config.cjs", "likec4.config.mjs", "likec4.config.ts", "likec4.config.cts", "likec4.config.mts"];
468
+ /** Returns true if the **basename** of the given path matches a known config filename. */
469
+ declare function isLikeC4JsonConfig(filename: string): boolean;
466
470
  /**
467
- * Checks if the given filename is a LikeC4 JSON config file (JSON, RC).
471
+ * Returns true if the **basename** of the given path matches a known non-JSON config filename (JS, MJS, TS, MTS).
468
472
  */
469
- declare function isLikeC4JsonConfig(filename: string): filename is typeof configJsonFilenames[number];
473
+ declare function isLikeC4NonJsonConfig(filename: string): boolean;
470
474
  /**
471
- * Checks if the given filename is a LikeC4 non-JSON config file (JS, MJS, TS, MTS)
475
+ * Returns true if the **basename** of the given path matches a known LikeC4 config file (JSON or non-JSON).
472
476
  */
473
- declare function isLikeC4NonJsonConfig(filename: string): filename is typeof configNonJsonFilenames[number];
474
- /**
475
- * Checks if the given filename is a LikeC4 config file (JSON or non-JSON)
476
- */
477
- declare function isLikeC4Config(filename: string): filename is typeof ConfigFilenames[number];
477
+ declare function isLikeC4Config(filename: string): boolean;
478
478
  //#endregion
479
479
  //#region src/define-config.d.ts
480
480
  /**
@@ -4,13 +4,12 @@ import JSON5 from "json5";
4
4
  import z from "zod/v4";
5
5
  import { BorderStyles, ElementShapes, IconPositions, RelationshipArrowTypes, Sizes, ThemeColors, computeColorValues } from "@likec4/core/styles";
6
6
  import { exact } from "@likec4/core/types";
7
- import { basename } from "pathe";
8
7
  import { invariant } from "@likec4/core";
9
8
  import { logger, wrapError } from "@likec4/log";
10
9
  import { bundleRequire } from "bundle-require";
11
10
  import { formatMessagesSync } from "esbuild";
12
11
  import * as fs from "node:fs/promises";
13
- import { basename as basename$1, dirname, resolve } from "node:path";
12
+ import { basename, dirname, resolve } from "node:path";
14
13
  const IMAGE_ALIAS_KEY_REGEX = /^@[A-Za-z0-9_-]*$/;
15
14
  const IMAGE_ALIAS_VALUE_REGEX = /^(?!\/|[A-Za-z]:[\\\/])(?!.*:\/\/).*$/;
16
15
  const ImageAliasKey = z.string().min(1, "Image alias key cannot be empty").regex(IMAGE_ALIAS_KEY_REGEX, "Image alias key must match /^@\\w+$/");
@@ -171,11 +170,18 @@ const LikeC4ProjectJsonConfigSchema = z.object({
171
170
  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" }),
172
171
  title: z.string().nonempty("Project title cannot be empty if specified").optional().meta({ description: "A human readable title for the project" }),
173
172
  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" }),
173
+ metadata: z.record(z.string(), z.any()).optional().meta({ description: "Arbitrary metadata as key-value pairs for custom project information" }),
174
174
  styles: LikeC4StylesConfigSchema.optional().meta({ description: "Project styles customization" }),
175
175
  imageAliases: ImageAliasesSchema.optional(),
176
176
  include: IncludeSchema.optional(),
177
177
  exclude: z.array(z.string()).optional().meta({ description: "List of file patterns to exclude from the project, default is [\"**/node_modules/**\"]" }),
178
- manualLayouts: ManualLayoutsConfigSchema.optional()
178
+ manualLayouts: ManualLayoutsConfigSchema.optional(),
179
+ inferTechnologyFromIcon: z.boolean().optional().meta({ description: [
180
+ "Automatically derive element technology from icon name when technology is not set explicitly.",
181
+ "Applies to aws:, azure:, gcp:, and tech: icons. Bootstrap icons are excluded.",
182
+ "Defaults to true."
183
+ ].join("\n") }),
184
+ implicitViews: z.boolean().optional().meta({ description: "Auto-generate scoped views for elements without explicit views. Defaults to true." })
179
185
  }).meta({
180
186
  id: "LikeC4ProjectConfig",
181
187
  description: "LikeC4 Project Configuration"
@@ -183,11 +189,18 @@ const LikeC4ProjectJsonConfigSchema = z.object({
183
189
  const FunctionType = z.instanceof(Function);
184
190
  const GeneratorsSchema = z.record(z.string(), FunctionType);
185
191
  const LikeC4ProjectConfigSchema = LikeC4ProjectJsonConfigSchema.extend({ generators: GeneratorsSchema.optional() });
192
+ /**
193
+ * Validates Object into a LikeC4ProjectConfig object.
194
+ */
186
195
  function validateProjectConfig(config) {
187
196
  const parsed = LikeC4ProjectConfigSchema.safeParse(config);
188
197
  if (parsed.success) return parsed.data;
189
198
  throw new Error("Config validation failed:\n" + z.prettifyError(parsed.error));
190
199
  }
200
+ /**
201
+ * Parses JSON string into a LikeC4ProjectConfig object.
202
+ * Does not process "extends" - use `loadConfig` function instead
203
+ */
191
204
  function parseProjectConfigJSON(config) {
192
205
  return validateProjectConfig(JSON5.parse(config.trim() || "{}"));
193
206
  }
@@ -204,11 +217,29 @@ const LikeC4ProjectConfigOps = {
204
217
  };
205
218
  }
206
219
  };
220
+ /** Trim trailing slashes and backslashes (no regex, avoids S5852 ReDoS). */
221
+ function trimTrailingSlashes(s) {
222
+ let end = s.length;
223
+ while (end > 0 && (s[end - 1] === "/" || s[end - 1] === "\\")) end--;
224
+ return s.slice(0, end);
225
+ }
226
+ /** Split by / or \ without regex (avoids S5852 ReDoS). */
227
+ function splitPath(s) {
228
+ return s.split("/").flatMap((part) => part.split("\\"));
229
+ }
230
+ /** basename compatible with Node and browser (no node:path for Vite/playground bundle). */
231
+ function basename$1(path) {
232
+ const trimmed = trimTrailingSlashes(path);
233
+ const segments = splitPath(trimmed);
234
+ return segments[segments.length - 1] || trimmed;
235
+ }
236
+ /** Known LikeC4 JSON config filenames (RC and .json). */
207
237
  const configJsonFilenames = [
208
238
  ".likec4rc",
209
239
  ".likec4.config.json",
210
240
  "likec4.config.json"
211
241
  ];
242
+ /** Known LikeC4 non-JSON config filenames (JS, MJS, TS, MTS). */
212
243
  const configNonJsonFilenames = [
213
244
  "likec4.config.js",
214
245
  "likec4.config.cjs",
@@ -217,28 +248,137 @@ const configNonJsonFilenames = [
217
248
  "likec4.config.cts",
218
249
  "likec4.config.mts"
219
250
  ];
251
+ /** All known LikeC4 config filenames (JSON and non-JSON). */
220
252
  const ConfigFilenames = [...configJsonFilenames, ...configNonJsonFilenames];
253
+ /** Returns true if the **basename** of the given path matches a known config filename. */
221
254
  function isLikeC4JsonConfig(filename) {
222
- return configJsonFilenames.includes(basename(filename));
255
+ return configJsonFilenames.includes(basename$1(filename));
223
256
  }
257
+ /**
258
+ * Returns true if the **basename** of the given path matches a known non-JSON config filename (JS, MJS, TS, MTS).
259
+ */
224
260
  function isLikeC4NonJsonConfig(filename) {
225
- return configNonJsonFilenames.includes(basename(filename));
261
+ return configNonJsonFilenames.includes(basename$1(filename));
226
262
  }
263
+ /**
264
+ * Returns true if the **basename** of the given path matches a known LikeC4 config file (JSON or non-JSON).
265
+ */
227
266
  function isLikeC4Config(filename) {
228
267
  return isLikeC4JsonConfig(filename) || isLikeC4NonJsonConfig(filename);
229
268
  }
269
+ /**
270
+ * Defines LikeC4 Project, allows custom generators that can be executed using CLI:
271
+ *
272
+ * `$ likec4 gen <generator-name>`
273
+ *
274
+ * or VSCode command `LikeC4: Run code generator`
275
+ *
276
+ * @example
277
+ * ```ts
278
+ * export default defineConfig({
279
+ * name: 'my-project',
280
+ * title: 'My Project',
281
+ *
282
+ * exclude: ['picomatch pattern'],
283
+ * generators: {
284
+ * '<generator-name>': async ({ likec4model, ctx }) => {
285
+ * await ctx.write('my-generator.txt', likec4model.project.id)
286
+ * }
287
+ * }
288
+ * })
289
+ * ```
290
+ */
230
291
  function defineConfig(config) {
231
292
  return LikeC4ProjectConfigSchema.parse(config);
232
293
  }
294
+ /**
295
+ * Define reusable custom generators
296
+ *
297
+ * @example
298
+ * ```ts
299
+ * // generators.ts
300
+ * export default defineGenerators({
301
+ * 'my-generator': async ({ likec4model, ctx }) => {
302
+ * await ctx.write('my-generator.txt', likec4model.project.id)
303
+ * }
304
+ * })
305
+ *
306
+ * // likec4.config.ts
307
+ * import generators from './generators'
308
+ *
309
+ * export default defineConfig({
310
+ * name: 'my-project',
311
+ * generators,
312
+ * })
313
+ * ```
314
+ */
233
315
  function defineGenerators(generators) {
234
316
  return GeneratorsSchema.parse(generators);
235
317
  }
318
+ /**
319
+ * Define reusable custom theme color
320
+ * @example
321
+ * ```ts
322
+ * export default defineThemeColor({
323
+ * element: {
324
+ * fill: 'red'
325
+ * }
326
+ * })
327
+ * ```
328
+ */
236
329
  function defineThemeColor(colors) {
237
330
  return ThemeColorValuesSchema.parse(colors);
238
331
  }
332
+ /**
333
+ * Define reusable custom theme
334
+ * @example
335
+ * ```ts
336
+ * import { defineThemeColor, defineTheme } from 'likec4/config'
337
+ *
338
+ * export default defineTheme({
339
+ * colors: {
340
+ * primary: '#FF0000',
341
+ * // Or use defineThemeColor
342
+ * red: defineThemeColor({
343
+ * elements: {
344
+ * fill: 'red'
345
+ * }
346
+ * })
347
+ * }
348
+ * })
349
+ * ```
350
+ */
239
351
  function defineTheme(theme) {
240
352
  return LikeC4Config_Styles_Theme.parse(theme);
241
353
  }
354
+ /**
355
+ * Define reusable custom style
356
+ * @example
357
+ * ```ts
358
+ * import { defineStyle, defineThemeColor } from 'likec4/config'
359
+ *
360
+ * export default defineStyle({
361
+ * theme: {
362
+ * colors: {
363
+ * red: defineThemeColor({
364
+ * elements: {
365
+ * fill: 'red'
366
+ * }
367
+ * })
368
+ * }
369
+ * },
370
+ * defaults: {
371
+ * color: 'red',
372
+ * opacity: 50,
373
+ * border: 'solid',
374
+ * size: 'sm',
375
+ * relationship: {
376
+ * color: 'grey',
377
+ * line: 'solid',
378
+ * }
379
+ * }
380
+ * })
381
+ */
242
382
  function defineStyle(styles) {
243
383
  return LikeC4StylesConfigSchema.parse(styles);
244
384
  }
@@ -279,12 +419,16 @@ const loadJsonConfigs = async (filepath, stack) => {
279
419
  }
280
420
  return [...configs, parsed];
281
421
  };
422
+ /**
423
+ * Load LikeC4 Project config file.
424
+ * If filepath is a non-JSON file, it will be bundled and required
425
+ */
282
426
  async function loadConfig(filepath) {
283
427
  filepath = typeof filepath === "string" ? filepath : filepath.fsPath;
284
428
  logger.getChild("config").debug`Loading config: ${filepath}`;
285
429
  const folder = dirname(filepath);
286
- const filename = basename$1(filepath);
287
- const implicitcfg = { name: basename$1(folder) };
430
+ const filename = basename(filepath);
431
+ const implicitcfg = { name: basename(folder) };
288
432
  if (isLikeC4JsonConfig(filename)) {
289
433
  const configs = await loadJsonConfigs(resolve(filepath), []);
290
434
  invariant(t(configs, 1), "Expect at least one config");
@@ -313,6 +457,9 @@ async function loadConfig(filepath) {
313
457
  plugins: [{
314
458
  name: "likec4-config",
315
459
  setup(build) {
460
+ /**
461
+ * Intercept @likec4/config and likec4/config imports
462
+ */
316
463
  build.onResolve({ filter: /^@?likec4\/config$/ }, (args) => ({
317
464
  path: args.path,
318
465
  namespace: "likec4-config"
@@ -321,6 +468,9 @@ async function loadConfig(filepath) {
321
468
  const messages = formatMessagesSync(result.errors, { kind: "error" });
322
469
  for (const message of messages) logger.error(message);
323
470
  });
471
+ /**
472
+ * Mock implementation, this allows to skip redundant bundling @likec4/config
473
+ */
324
474
  build.onLoad({
325
475
  filter: /.*/,
326
476
  namespace: "likec4-config"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likec4/config",
3
- "version": "1.49.0",
3
+ "version": "1.50.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://likec4.dev",
6
6
  "author": "Denis Davydkov <denis@davydkov.com>",
@@ -54,11 +54,10 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "json5": "^2.2.3",
57
- "pathe": "^2.0.3",
58
57
  "zod": "^3.25.76",
59
58
  "type-fest": "^4.41.0",
60
- "@likec4/log": "1.49.0",
61
- "@likec4/core": "1.49.0"
59
+ "@likec4/core": "1.50.0",
60
+ "@likec4/log": "1.50.0"
62
61
  },
63
62
  "peerDependencies": {
64
63
  "bundle-require": "^5.1.0",
@@ -73,18 +72,18 @@
73
72
  }
74
73
  },
75
74
  "devDependencies": {
76
- "@types/node": "~22.19.10",
75
+ "@types/node": "~22.19.11",
77
76
  "remeda": "^2.33.5",
78
77
  "defu": "^6.1.4",
79
78
  "ufo": "1.6.3",
80
79
  "tsx": "4.21.0",
81
- "turbo": "2.8.3",
80
+ "turbo": "2.8.10",
82
81
  "typescript": "5.9.3",
83
- "obuild": "^0.4.27",
82
+ "obuild": "^0.4.31",
84
83
  "nano-spawn": "^2.0.0",
85
84
  "vitest": "4.0.18",
86
- "@likec4/devops": "1.42.0",
87
- "@likec4/tsconfig": "1.49.0"
85
+ "@likec4/tsconfig": "1.50.0",
86
+ "@likec4/devops": "1.42.0"
88
87
  },
89
88
  "scripts": {
90
89
  "generate": "tsx --conditions=sources scripts/generate.mts",
package/schema.json CHANGED
@@ -36,6 +36,14 @@
36
36
  "type": "string",
37
37
  "minLength": 1
38
38
  },
39
+ "metadata": {
40
+ "description": "Arbitrary metadata as key-value pairs for custom project information",
41
+ "type": "object",
42
+ "propertyNames": {
43
+ "type": "string"
44
+ },
45
+ "additionalProperties": {}
46
+ },
39
47
  "styles": {
40
48
  "description": "Project styles customization",
41
49
  "type": "object",
@@ -70,6 +78,14 @@
70
78
  },
71
79
  "manualLayouts": {
72
80
  "$ref": "#/$defs/ManualLayoutsConfig"
81
+ },
82
+ "inferTechnologyFromIcon": {
83
+ "description": "Automatically derive element technology from icon name when technology is not set explicitly.\nApplies to aws:, azure:, gcp:, and tech: icons. Bootstrap icons are excluded.\nDefaults to true.",
84
+ "type": "boolean"
85
+ },
86
+ "implicitViews": {
87
+ "description": "Auto-generate scoped views for elements without explicit views. Defaults to true.",
88
+ "type": "boolean"
73
89
  }
74
90
  },
75
91
  "required": [
package/src/filenames.ts CHANGED
@@ -1,11 +1,31 @@
1
- import { basename } from 'pathe'
1
+ /** Trim trailing slashes and backslashes (no regex, avoids S5852 ReDoS). */
2
+ function trimTrailingSlashes(s: string): string {
3
+ let end = s.length
4
+ while (end > 0 && (s[end - 1] === '/' || s[end - 1] === '\\')) end--
5
+ return s.slice(0, end)
6
+ }
7
+
8
+ /** Split by / or \ without regex (avoids S5852 ReDoS). */
9
+ function splitPath(s: string): string[] {
10
+ return s.split('/').flatMap(part => part.split('\\'))
11
+ }
2
12
 
13
+ /** basename compatible with Node and browser (no node:path for Vite/playground bundle). */
14
+ function basename(path: string): string {
15
+ const trimmed = trimTrailingSlashes(path)
16
+ const segments = splitPath(trimmed)
17
+ const last = segments[segments.length - 1]
18
+ return last || trimmed
19
+ }
20
+
21
+ /** Known LikeC4 JSON config filenames (RC and .json). */
3
22
  export const configJsonFilenames = [
4
23
  '.likec4rc',
5
24
  '.likec4.config.json',
6
25
  'likec4.config.json',
7
26
  ] as const
8
27
 
28
+ /** Known LikeC4 non-JSON config filenames (JS, MJS, TS, MTS). */
9
29
  export const configNonJsonFilenames = [
10
30
  'likec4.config.js',
11
31
  'likec4.config.cjs',
@@ -15,28 +35,27 @@ export const configNonJsonFilenames = [
15
35
  'likec4.config.mts',
16
36
  ] as const
17
37
 
38
+ /** All known LikeC4 config filenames (JSON and non-JSON). */
18
39
  export const ConfigFilenames = [
19
40
  ...configJsonFilenames,
20
41
  ...configNonJsonFilenames,
21
42
  ] as const
22
43
 
23
- /**
24
- * Checks if the given filename is a LikeC4 JSON config file (JSON, RC).
25
- */
26
- export function isLikeC4JsonConfig(filename: string): filename is typeof configJsonFilenames[number] {
44
+ /** Returns true if the **basename** of the given path matches a known config filename. */
45
+ export function isLikeC4JsonConfig(filename: string): boolean {
27
46
  return (configJsonFilenames as readonly string[]).includes(basename(filename))
28
47
  }
29
48
 
30
49
  /**
31
- * Checks if the given filename is a LikeC4 non-JSON config file (JS, MJS, TS, MTS)
50
+ * Returns true if the **basename** of the given path matches a known non-JSON config filename (JS, MJS, TS, MTS).
32
51
  */
33
- export function isLikeC4NonJsonConfig(filename: string): filename is typeof configNonJsonFilenames[number] {
52
+ export function isLikeC4NonJsonConfig(filename: string): boolean {
34
53
  return (configNonJsonFilenames as readonly string[]).includes(basename(filename))
35
54
  }
36
55
 
37
56
  /**
38
- * Checks if the given filename is a LikeC4 config file (JSON or non-JSON)
57
+ * Returns true if the **basename** of the given path matches a known LikeC4 config file (JSON or non-JSON).
39
58
  */
40
- export function isLikeC4Config(filename: string): filename is typeof ConfigFilenames[number] {
59
+ export function isLikeC4Config(filename: string): boolean {
41
60
  return isLikeC4JsonConfig(filename) || isLikeC4NonJsonConfig(filename)
42
61
  }
package/src/schema.ts CHANGED
@@ -1,3 +1,10 @@
1
+ // SPDX-License-Identifier: MIT
2
+ //
3
+ // Copyright (c) 2023-2026 Denis Davydkov
4
+ // Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
5
+ //
6
+ // Portions of this file have been modified by NVIDIA CORPORATION & AFFILIATES.
7
+
1
8
  import type {
2
9
  DeploymentElementModel,
3
10
  DeploymentRelationModel,
@@ -72,6 +79,9 @@ export const LikeC4ProjectJsonConfigSchema = z.object({
72
79
  .nonempty('Contact person cannot be empty if specified')
73
80
  .optional()
74
81
  .meta({ description: 'A person who has been involved in creating or maintaining this project' }),
82
+ metadata: z.record(z.string(), z.any())
83
+ .optional()
84
+ .meta({ description: 'Arbitrary metadata as key-value pairs for custom project information' }),
75
85
  styles: LikeC4StylesConfigSchema.optional().meta({
76
86
  description: 'Project styles customization',
77
87
  }),
@@ -81,6 +91,20 @@ export const LikeC4ProjectJsonConfigSchema = z.object({
81
91
  .optional()
82
92
  .meta({ description: 'List of file patterns to exclude from the project, default is ["**/node_modules/**"]' }),
83
93
  manualLayouts: ManualLayoutsConfigSchema.optional(),
94
+ inferTechnologyFromIcon: z.boolean()
95
+ .optional()
96
+ .meta({
97
+ description: [
98
+ 'Automatically derive element technology from icon name when technology is not set explicitly.',
99
+ 'Applies to aws:, azure:, gcp:, and tech: icons. Bootstrap icons are excluded.',
100
+ 'Defaults to true.',
101
+ ].join('\n'),
102
+ }),
103
+ implicitViews: z.boolean()
104
+ .optional()
105
+ .meta({
106
+ description: 'Auto-generate scoped views for elements without explicit views. Defaults to true.',
107
+ }),
84
108
  })
85
109
  .meta({
86
110
  id: 'LikeC4ProjectConfig',