@likec4/config 1.48.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 +5 -0
- package/dist/THIRD-PARTY-LICENSES.md +41 -0
- package/dist/_chunks/libs/defu.mjs +39 -0
- package/dist/_chunks/libs/remeda.mjs +32 -19
- package/dist/index.d.mts +233 -148
- package/dist/index.mjs +106 -136
- package/dist/node/index.d.mts +234 -149
- package/dist/node/index.mjs +208 -194
- package/package.json +11 -9
- package/schema.json +169 -141
- package/src/filenames.ts +33 -22
- package/src/index.ts +6 -5
- package/src/node/index.ts +1 -38
- package/src/node/load-config.ts +121 -51
- package/src/schema.image-alias.ts +10 -46
- package/src/schema.include.ts +36 -74
- package/src/schema.theme.ts +87 -83
- package/src/schema.ts +76 -35
package/dist/node/index.mjs
CHANGED
|
@@ -1,40 +1,26 @@
|
|
|
1
|
-
import { t } from "../_chunks/libs/
|
|
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";
|
|
6
7
|
import { invariant } from "@likec4/core";
|
|
7
|
-
import {
|
|
8
|
+
import { logger, wrapError } from "@likec4/log";
|
|
8
9
|
import { bundleRequire } from "bundle-require";
|
|
10
|
+
import { formatMessagesSync } from "esbuild";
|
|
9
11
|
import * as fs from "node:fs/promises";
|
|
10
|
-
import { dirname } from "node:path";
|
|
11
|
-
|
|
12
|
-
//#region src/schema.image-alias.ts
|
|
12
|
+
import { basename, dirname, resolve } from "node:path";
|
|
13
13
|
const IMAGE_ALIAS_KEY_REGEX = /^@[A-Za-z0-9_-]*$/;
|
|
14
14
|
const IMAGE_ALIAS_VALUE_REGEX = /^(?!\/|[A-Za-z]:[\\\/])(?!.*:\/\/).*$/;
|
|
15
|
+
const ImageAliasKey = z.string().min(1, "Image alias key cannot be empty").regex(IMAGE_ALIAS_KEY_REGEX, "Image alias key must match /^@\\w+$/");
|
|
15
16
|
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(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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: [
|
|
17
|
+
const ImageAliasesSchema = z.record(ImageAliasKey, ImageAliasValue).meta({
|
|
18
|
+
id: "ImageAliases",
|
|
19
|
+
description: "Map of image alias prefixes to relative paths (keys must match /^@\\w+$/; values must be relative paths without protocol or leading slash)."
|
|
20
|
+
});
|
|
21
|
+
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)");
|
|
22
|
+
const IncludeSchema = z.strictObject({
|
|
23
|
+
paths: z.array(IncludePathValue).meta({ description: [
|
|
38
24
|
"Additional relative directory paths to include LikeC4 source files from, searched recursively.",
|
|
39
25
|
"Paths are relative to the project folder (the folder containing this config file).",
|
|
40
26
|
"Example: [\"../shared\", \"../common/specs\"]"
|
|
@@ -44,54 +30,33 @@ const IncludeConfigSchema = z.strictObject({
|
|
|
44
30
|
"Prevents excessive scanning of deeply nested directories.",
|
|
45
31
|
"Default: 3"
|
|
46
32
|
].join("\n") }),
|
|
47
|
-
fileThreshold: z.number().int().min(1).default(30).meta({ description: [
|
|
33
|
+
fileThreshold: z.number().int().min(1).max(1e4).default(30).meta({ description: [
|
|
48
34
|
"Maximum number of files to load from include paths before warning.",
|
|
49
35
|
"Helps identify performance issues from accidentally including large directories.",
|
|
50
36
|
"Default: 30"
|
|
51
37
|
].join("\n") })
|
|
52
38
|
}).meta({
|
|
53
39
|
id: "include-config",
|
|
54
|
-
description: "Configuration for including additional LikeC4 source files"
|
|
40
|
+
description: ["Configuration for including additional LikeC4 source files from other directories.", "Example: { \"paths\": [\"../shared\", \"../common/specs\"], \"maxDepth\": 5, \"fileThreshold\": 50 }"].join("\n")
|
|
55
41
|
});
|
|
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
42
|
const opacity = z.int().min(0, "Opacity must be between 0 and 100").max(100, "Opacity must be between 0 and 100").meta({
|
|
75
43
|
id: "Opacity",
|
|
76
44
|
description: "Opacity 0-100%"
|
|
77
45
|
});
|
|
78
|
-
const shape = z.
|
|
79
|
-
const border = z.
|
|
80
|
-
const size = z.
|
|
81
|
-
const iconPosition = z.
|
|
82
|
-
const arrow = z.
|
|
83
|
-
const line = z.
|
|
46
|
+
const shape = z.enum(ElementShapes).meta({ id: "ElementShape" });
|
|
47
|
+
const border = z.enum(BorderStyles).meta({ id: "BorderStyle" });
|
|
48
|
+
const size = z.enum(Sizes).meta({ id: "ElementSize" });
|
|
49
|
+
const iconPosition = z.enum(IconPositions).meta({ id: "IconPosition" });
|
|
50
|
+
const arrow = z.enum(RelationshipArrowTypes).meta({ id: "ArrowType" });
|
|
51
|
+
const line = z.enum([
|
|
84
52
|
"dashed",
|
|
85
53
|
"solid",
|
|
86
54
|
"dotted"
|
|
87
55
|
]).meta({ id: "LineType" });
|
|
88
|
-
const themeColor = z.
|
|
89
|
-
const customColor = z.
|
|
90
|
-
const color =
|
|
91
|
-
const colorSchema = z.string().min(1, "Color value cannot be empty").
|
|
92
|
-
id: "ColorLiteral",
|
|
93
|
-
description: "Color value in any valid CSS format: hex, rgb, rgba, hsl, hsla ..."
|
|
94
|
-
});
|
|
56
|
+
const themeColor = z.enum(ThemeColors).meta({ id: "ThemeColorName" });
|
|
57
|
+
const customColor = z.custom().refine((v) => typeof v === "string", "Custom color name must be a string").transform((value) => value).meta({ id: "CustomColorName" });
|
|
58
|
+
const color = themeColor.or(customColor).transform((value) => value).meta({ id: "ColorName" });
|
|
59
|
+
const colorSchema = z.string().min(1, "Color value cannot be empty").meta({ id: "ColorLiteral" });
|
|
95
60
|
const ElementColorValuesSchema = z.strictObject({
|
|
96
61
|
fill: colorSchema.meta({ description: "Background color" }),
|
|
97
62
|
stroke: colorSchema.meta({ description: "Stroke color (border, paths above background)" }),
|
|
@@ -103,44 +68,31 @@ const RelationshipColorValuesSchema = z.strictObject({
|
|
|
103
68
|
label: colorSchema.meta({ description: "Label text color" }),
|
|
104
69
|
labelBg: colorSchema.optional().default("rgba(0, 0, 0, 0.5)").meta({ description: "Label background color" })
|
|
105
70
|
}).meta({ id: "RelationshipColorValues" }).transform((value) => value);
|
|
106
|
-
const
|
|
107
|
-
elements:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return value;
|
|
114
|
-
})
|
|
115
|
-
}).meta({ id: "StrictThemeColorValues" });
|
|
116
|
-
const ThemeColorValuesSchema = z.union([colorSchema, StrictThemeColorValuesSchema]).meta({
|
|
71
|
+
const ThemeColorValuesSchema = z.strictObject({
|
|
72
|
+
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" }),
|
|
73
|
+
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" })
|
|
74
|
+
}).transform((value) => value).meta({
|
|
75
|
+
id: "StrictThemeColorValues",
|
|
76
|
+
description: "Exact color value (hex, rgb, rgba, hsl, hsla ...) or break down of specific color value"
|
|
77
|
+
}).or(colorSchema.transform((v) => computeColorValues(v))).transform((value) => value).meta({
|
|
117
78
|
id: "ThemeColorValues",
|
|
118
79
|
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
80
|
});
|
|
123
|
-
const ThemeColorsSchema = z.
|
|
124
|
-
id: "ThemeColors",
|
|
125
|
-
description: "Override theme colors"
|
|
126
|
-
}).transform((value) => value);
|
|
81
|
+
const ThemeColorsSchema = z.partialRecord(color, ThemeColorValuesSchema).transform((value) => value);
|
|
127
82
|
const DimensionsSchema = z.strictObject({
|
|
128
83
|
width: z.number().min(50),
|
|
129
84
|
height: z.number().min(50)
|
|
130
85
|
}).meta({
|
|
131
86
|
id: "Dimensions",
|
|
132
|
-
description: "
|
|
133
|
-
});
|
|
134
|
-
const LikeC4Config_Styles_Theme_Sizes = z.strictObject(t(Sizes, () => DimensionsSchema.optional())).meta({
|
|
135
|
-
id: "ThemeSizes",
|
|
136
|
-
description: "Override theme sizes"
|
|
87
|
+
description: "Defines dimensions for theme size"
|
|
137
88
|
});
|
|
89
|
+
const LikeC4Config_Styles_Theme_Sizes = z.partialRecord(size, DimensionsSchema);
|
|
138
90
|
const LikeC4Config_Styles_Theme = z.strictObject({
|
|
139
|
-
colors: ThemeColorsSchema.optional(),
|
|
140
|
-
sizes: LikeC4Config_Styles_Theme_Sizes.optional()
|
|
91
|
+
colors: ThemeColorsSchema.optional().meta({ description: "Override theme colors" }),
|
|
92
|
+
sizes: LikeC4Config_Styles_Theme_Sizes.optional().meta({ description: "Override theme sizes" })
|
|
141
93
|
}).meta({
|
|
142
94
|
id: "ThemeCustomization",
|
|
143
|
-
description: "
|
|
95
|
+
description: "Customize theme colors and sizes"
|
|
144
96
|
}).transform(({ colors, sizes }) => {
|
|
145
97
|
return exact({
|
|
146
98
|
colors: colors ? exact(colors) : void 0,
|
|
@@ -151,10 +103,7 @@ const LikeC4Config_Styles_Defaults_Group = z.strictObject({
|
|
|
151
103
|
color: color.optional().meta({ description: "Default color for groups\n(must be a valid color name from the theme)" }),
|
|
152
104
|
opacity: opacity.optional().meta({ description: "Default opacity for groups" }),
|
|
153
105
|
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
|
-
});
|
|
106
|
+
}).meta({ id: "GroupDefaultStyleValues" });
|
|
158
107
|
const LikeC4Config_Styles_Defaults_Relationship = z.strictObject({
|
|
159
108
|
color: color.optional().meta({ description: "Default color for relationships\n(must be a valid color name from the theme)" }),
|
|
160
109
|
line: line.optional().meta({ description: "Default line style for relationships" }),
|
|
@@ -170,23 +119,14 @@ const LikeC4Config_Styles_Defaults = z.strictObject({
|
|
|
170
119
|
size: size.optional().meta({ description: "Default size for elements" }),
|
|
171
120
|
shape: shape.optional().meta({ description: "Default shape for elements" }),
|
|
172
121
|
iconPosition: iconPosition.optional().meta({ description: "Default icon position for elements" }),
|
|
173
|
-
group: LikeC4Config_Styles_Defaults_Group.optional().meta({ description: "
|
|
174
|
-
relationship: LikeC4Config_Styles_Defaults_Relationship.optional().meta({ description: "
|
|
175
|
-
}).meta({
|
|
176
|
-
|
|
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
|
-
});
|
|
122
|
+
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" }),
|
|
123
|
+
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" })
|
|
124
|
+
}).meta({ id: "DefaultStyleValues" });
|
|
125
|
+
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
126
|
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"
|
|
127
|
+
theme: LikeC4Config_Styles_Theme.optional().meta({ description: "Project theme customization" }),
|
|
128
|
+
defaults: LikeC4Config_Styles_Defaults.optional().meta({ description: "Override default values for style properties\nThese values will be used if such property is not defined" }),
|
|
129
|
+
customCss: LikeC4Config_Styles_CustomStylesheets.optional().meta({ description: "Custom CSS (or list of CSS files) to be included in the generated diagrams" })
|
|
190
130
|
}).transform(({ theme, defaults, customCss }) => exact({
|
|
191
131
|
defaults: normalizeDefaults(defaults),
|
|
192
132
|
customCss: normalizeStylesheets(customCss),
|
|
@@ -210,16 +150,13 @@ function normalizeStylesheets(stylesheets) {
|
|
|
210
150
|
content: ""
|
|
211
151
|
};
|
|
212
152
|
}
|
|
213
|
-
|
|
214
|
-
//#endregion
|
|
215
|
-
//#region src/schema.ts
|
|
216
153
|
const ManualLayoutsConfigSchema = z.strictObject({ outDir: z.string().default(".likec4").meta({ description: [
|
|
217
154
|
"Path to the directory where manual layouts will be stored,",
|
|
218
155
|
"relative to the folder containing the project config. ",
|
|
219
156
|
"",
|
|
220
157
|
"Defaults to '.likec4'."
|
|
221
158
|
].join("\n") }) }).meta({
|
|
222
|
-
id: "
|
|
159
|
+
id: "ManualLayoutsConfig",
|
|
223
160
|
description: "Configuration for manual layouts"
|
|
224
161
|
});
|
|
225
162
|
const LikeC4ProjectJsonConfigSchema = z.object({
|
|
@@ -230,35 +167,79 @@ const LikeC4ProjectJsonConfigSchema = z.object({
|
|
|
230
167
|
abort: true,
|
|
231
168
|
error: "Project name cannot contain \".\", \"@\" or \"#\", try to use A-z, 0-9, _ and -"
|
|
232
169
|
}).meta({ description: "Project name, must be unique in the workspace" }),
|
|
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" }),
|
|
233
171
|
title: z.string().nonempty("Project title cannot be empty if specified").optional().meta({ description: "A human readable title for the project" }),
|
|
234
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
|
+
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
|
-
manualLayouts: ManualLayoutsConfigSchema.optional()
|
|
240
|
-
|
|
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." })
|
|
185
|
+
}).meta({
|
|
186
|
+
id: "LikeC4ProjectConfig",
|
|
187
|
+
description: "LikeC4 Project Configuration"
|
|
188
|
+
});
|
|
241
189
|
const FunctionType = z.instanceof(Function);
|
|
242
190
|
const GeneratorsSchema = z.record(z.string(), FunctionType);
|
|
243
191
|
const LikeC4ProjectConfigSchema = LikeC4ProjectJsonConfigSchema.extend({ generators: GeneratorsSchema.optional() });
|
|
244
192
|
/**
|
|
245
|
-
* Validates
|
|
193
|
+
* Validates Object into a LikeC4ProjectConfig object.
|
|
246
194
|
*/
|
|
247
195
|
function validateProjectConfig(config) {
|
|
248
|
-
const parsed = LikeC4ProjectConfigSchema.safeParse(
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
if (parsed.data.include) validateIncludePaths(parsed.data.include);
|
|
252
|
-
return parsed.data;
|
|
196
|
+
const parsed = LikeC4ProjectConfigSchema.safeParse(config);
|
|
197
|
+
if (parsed.success) return parsed.data;
|
|
198
|
+
throw new Error("Config validation failed:\n" + z.prettifyError(parsed.error));
|
|
253
199
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Parses JSON string into a LikeC4ProjectConfig object.
|
|
202
|
+
* Does not process "extends" - use `loadConfig` function instead
|
|
203
|
+
*/
|
|
204
|
+
function parseProjectConfigJSON(config) {
|
|
205
|
+
return validateProjectConfig(JSON5.parse(config.trim() || "{}"));
|
|
206
|
+
}
|
|
207
|
+
const LikeC4ProjectConfigOps = {
|
|
208
|
+
parse: parseProjectConfigJSON,
|
|
209
|
+
validate: validateProjectConfig,
|
|
210
|
+
normalizeInclude: (include) => {
|
|
211
|
+
const parsed = IncludeSchema.safeParse(include);
|
|
212
|
+
if (parsed.success) return parsed.data;
|
|
213
|
+
return {
|
|
214
|
+
paths: [],
|
|
215
|
+
maxDepth: 3,
|
|
216
|
+
fileThreshold: 30
|
|
217
|
+
};
|
|
218
|
+
}
|
|
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). */
|
|
257
237
|
const configJsonFilenames = [
|
|
258
238
|
".likec4rc",
|
|
259
239
|
".likec4.config.json",
|
|
260
240
|
"likec4.config.json"
|
|
261
241
|
];
|
|
242
|
+
/** Known LikeC4 non-JSON config filenames (JS, MJS, TS, MTS). */
|
|
262
243
|
const configNonJsonFilenames = [
|
|
263
244
|
"likec4.config.js",
|
|
264
245
|
"likec4.config.cjs",
|
|
@@ -267,30 +248,24 @@ const configNonJsonFilenames = [
|
|
|
267
248
|
"likec4.config.cts",
|
|
268
249
|
"likec4.config.mts"
|
|
269
250
|
];
|
|
251
|
+
/** All known LikeC4 config filenames (JSON and non-JSON). */
|
|
270
252
|
const ConfigFilenames = [...configJsonFilenames, ...configNonJsonFilenames];
|
|
271
|
-
/**
|
|
272
|
-
* Checks if the given filename is a LikeC4 JSON config file (JSON, RC).
|
|
273
|
-
*/
|
|
253
|
+
/** Returns true if the **basename** of the given path matches a known config filename. */
|
|
274
254
|
function isLikeC4JsonConfig(filename) {
|
|
275
|
-
|
|
276
|
-
return false;
|
|
255
|
+
return configJsonFilenames.includes(basename$1(filename));
|
|
277
256
|
}
|
|
278
257
|
/**
|
|
279
|
-
*
|
|
258
|
+
* Returns true if the **basename** of the given path matches a known non-JSON config filename (JS, MJS, TS, MTS).
|
|
280
259
|
*/
|
|
281
260
|
function isLikeC4NonJsonConfig(filename) {
|
|
282
|
-
|
|
283
|
-
return false;
|
|
261
|
+
return configNonJsonFilenames.includes(basename$1(filename));
|
|
284
262
|
}
|
|
285
263
|
/**
|
|
286
|
-
*
|
|
264
|
+
* Returns true if the **basename** of the given path matches a known LikeC4 config file (JSON or non-JSON).
|
|
287
265
|
*/
|
|
288
266
|
function isLikeC4Config(filename) {
|
|
289
267
|
return isLikeC4JsonConfig(filename) || isLikeC4NonJsonConfig(filename);
|
|
290
268
|
}
|
|
291
|
-
|
|
292
|
-
//#endregion
|
|
293
|
-
//#region src/define-config.ts
|
|
294
269
|
/**
|
|
295
270
|
* Defines LikeC4 Project, allows custom generators that can be executed using CLI:
|
|
296
271
|
*
|
|
@@ -407,56 +382,101 @@ function defineTheme(theme) {
|
|
|
407
382
|
function defineStyle(styles) {
|
|
408
383
|
return LikeC4StylesConfigSchema.parse(styles);
|
|
409
384
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
385
|
+
const JsonConfigInputSchema = LikeC4ProjectJsonConfigSchema.pick({
|
|
386
|
+
extends: true,
|
|
387
|
+
styles: true
|
|
388
|
+
}).loose();
|
|
389
|
+
const normalizeExtends = (value) => {
|
|
390
|
+
if (!value) return [];
|
|
391
|
+
return Array.isArray(value) ? value : [value];
|
|
392
|
+
};
|
|
393
|
+
const parseJsonConfig = async (filepath) => {
|
|
394
|
+
const content = await fs.readFile(filepath, "utf-8");
|
|
395
|
+
let parsed;
|
|
396
|
+
try {
|
|
397
|
+
parsed = JSON5.parse(content.trim() || "{}");
|
|
398
|
+
} catch (e) {
|
|
399
|
+
throw wrapError(e, `${filepath}:`);
|
|
400
|
+
}
|
|
401
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${filepath}: Config must be a JSON object`);
|
|
402
|
+
const result = JsonConfigInputSchema.safeParse(parsed);
|
|
403
|
+
if (!result.success) throw new Error(`${filepath}: Invalid config\n` + z.prettifyError(result.error));
|
|
404
|
+
return result.data;
|
|
405
|
+
};
|
|
406
|
+
const loadJsonConfigs = async (filepath, stack) => {
|
|
407
|
+
if (stack.includes(filepath)) {
|
|
408
|
+
const cycleStart = stack.indexOf(filepath);
|
|
409
|
+
const cycle = [...stack.slice(cycleStart), filepath].join(" -> ");
|
|
410
|
+
throw new Error(`Config extends cycle detected: ${cycle}`);
|
|
411
|
+
}
|
|
412
|
+
const parsed = await parseJsonConfig(filepath);
|
|
413
|
+
const extendsPaths = normalizeExtends(parsed.extends);
|
|
414
|
+
const nextStack = [...stack, filepath];
|
|
415
|
+
const configs = [];
|
|
416
|
+
for (const extendPath of extendsPaths) {
|
|
417
|
+
const resolvedPath = resolve(dirname(filepath), extendPath);
|
|
418
|
+
configs.push(...await loadJsonConfigs(resolvedPath, nextStack));
|
|
419
|
+
}
|
|
420
|
+
return [...configs, parsed];
|
|
421
|
+
};
|
|
413
422
|
/**
|
|
414
423
|
* Load LikeC4 Project config file.
|
|
415
424
|
* If filepath is a non-JSON file, it will be bundled and required
|
|
416
425
|
*/
|
|
417
426
|
async function loadConfig(filepath) {
|
|
418
|
-
|
|
419
|
-
logger.debug`Loading config
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
427
|
+
filepath = typeof filepath === "string" ? filepath : filepath.fsPath;
|
|
428
|
+
logger.getChild("config").debug`Loading config: ${filepath}`;
|
|
429
|
+
const folder = dirname(filepath);
|
|
430
|
+
const filename = basename(filepath);
|
|
431
|
+
const implicitcfg = { name: basename(folder) };
|
|
432
|
+
if (isLikeC4JsonConfig(filename)) {
|
|
433
|
+
const configs = await loadJsonConfigs(resolve(filepath), []);
|
|
434
|
+
invariant(t(configs, 1), "Expect at least one config");
|
|
435
|
+
const rootConfig = n(t$1(configs), ["extends", "styles"]);
|
|
436
|
+
const stylesChain = configs.map((config) => config.styles).filter(e);
|
|
437
|
+
const mergedStyles = stylesChain.length > 0 ? defu({}, ...stylesChain.reverse()) : void 0;
|
|
438
|
+
return validateProjectConfig({
|
|
439
|
+
...implicitcfg,
|
|
440
|
+
...rootConfig,
|
|
441
|
+
...mergedStyles ? { styles: mergedStyles } : {}
|
|
442
|
+
});
|
|
425
443
|
}
|
|
426
|
-
invariant(isLikeC4NonJsonConfig(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
444
|
+
invariant(isLikeC4NonJsonConfig(filename), `Invalid name for config file: ${filepath}`);
|
|
445
|
+
const { mod } = await bundleRequire({
|
|
446
|
+
filepath,
|
|
447
|
+
cwd: folder,
|
|
448
|
+
esbuildOptions: {
|
|
449
|
+
resolveExtensions: [
|
|
450
|
+
".ts",
|
|
451
|
+
".mts",
|
|
452
|
+
".cts",
|
|
453
|
+
".mjs",
|
|
454
|
+
".js",
|
|
455
|
+
".cjs"
|
|
456
|
+
],
|
|
457
|
+
plugins: [{
|
|
458
|
+
name: "likec4-config",
|
|
459
|
+
setup(build) {
|
|
460
|
+
/**
|
|
461
|
+
* Intercept @likec4/config and likec4/config imports
|
|
462
|
+
*/
|
|
463
|
+
build.onResolve({ filter: /^@?likec4\/config$/ }, (args) => ({
|
|
464
|
+
path: args.path,
|
|
465
|
+
namespace: "likec4-config"
|
|
466
|
+
}));
|
|
467
|
+
build.onEnd((result) => {
|
|
468
|
+
const messages = formatMessagesSync(result.errors, { kind: "error" });
|
|
469
|
+
for (const message of messages) logger.error(message);
|
|
470
|
+
});
|
|
471
|
+
/**
|
|
472
|
+
* Mock implementation, this allows to skip redundant bundling @likec4/config
|
|
473
|
+
*/
|
|
474
|
+
build.onLoad({
|
|
475
|
+
filter: /.*/,
|
|
476
|
+
namespace: "likec4-config"
|
|
477
|
+
}, (_args) => {
|
|
478
|
+
return {
|
|
479
|
+
contents: `
|
|
460
480
|
// Mock implementation to allow loading config files without bundling @likec4/config
|
|
461
481
|
function mock(x) { return x }
|
|
462
482
|
export {
|
|
@@ -466,19 +486,13 @@ export {
|
|
|
466
486
|
mock as defineTheme,
|
|
467
487
|
mock as defineThemeColor,
|
|
468
488
|
}`,
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
} catch (err) {
|
|
478
|
-
logger.error(`Failed to load config file: ${filepath.fsPath}`, { err });
|
|
479
|
-
throw err;
|
|
480
|
-
}
|
|
489
|
+
loader: "js"
|
|
490
|
+
};
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}]
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
return validateProjectConfig(Object.assign(implicitcfg, mod?.default ?? mod));
|
|
481
497
|
}
|
|
482
|
-
|
|
483
|
-
//#endregion
|
|
484
|
-
export { ConfigFilenames, defineConfig, defineGenerators, defineStyle, defineTheme, defineThemeColor, isLikeC4Config, isLikeC4JsonConfig, isLikeC4NonJsonConfig, loadConfig, normalizeIncludeConfig, validateIncludePaths, validateProjectConfig };
|
|
498
|
+
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.
|
|
3
|
+
"version": "1.50.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://likec4.dev",
|
|
6
6
|
"author": "Denis Davydkov <denis@davydkov.com>",
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
"json5": "^2.2.3",
|
|
57
57
|
"zod": "^3.25.76",
|
|
58
58
|
"type-fest": "^4.41.0",
|
|
59
|
-
"@likec4/core": "1.
|
|
60
|
-
"@likec4/log": "1.
|
|
59
|
+
"@likec4/core": "1.50.0",
|
|
60
|
+
"@likec4/log": "1.50.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"bundle-require": "^5.1.0",
|
|
64
|
-
"esbuild": "0.27.
|
|
64
|
+
"esbuild": "0.27.3"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"esbuild": {
|
|
@@ -72,19 +72,21 @@
|
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@types/node": "~22.19.
|
|
76
|
-
"remeda": "^2.
|
|
75
|
+
"@types/node": "~22.19.11",
|
|
76
|
+
"remeda": "^2.33.5",
|
|
77
77
|
"defu": "^6.1.4",
|
|
78
|
+
"ufo": "1.6.3",
|
|
78
79
|
"tsx": "4.21.0",
|
|
79
|
-
"turbo": "2.
|
|
80
|
+
"turbo": "2.8.10",
|
|
80
81
|
"typescript": "5.9.3",
|
|
81
|
-
"obuild": "^0.4.
|
|
82
|
+
"obuild": "^0.4.31",
|
|
82
83
|
"nano-spawn": "^2.0.0",
|
|
83
84
|
"vitest": "4.0.18",
|
|
84
|
-
"@likec4/tsconfig": "1.
|
|
85
|
+
"@likec4/tsconfig": "1.50.0",
|
|
85
86
|
"@likec4/devops": "1.42.0"
|
|
86
87
|
},
|
|
87
88
|
"scripts": {
|
|
89
|
+
"generate": "tsx --conditions=sources scripts/generate.mts",
|
|
88
90
|
"typecheck": "tsc -b --verbose",
|
|
89
91
|
"build": "obuild",
|
|
90
92
|
"lint:package": "pnpx publint ./package.tgz",
|