@likec4/config 1.39.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-2025 Denis Davydkov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # `@likec4/config`
2
+
3
+ <a href="https://www.npmjs.com/package/%40likec4%2Fconfig" target="_blank">![NPM Version](https://img.shields.io/npm/v/%40likec4%2Fconfig)</a>
4
+ <a href="https://www.npmjs.com/package/%40likec4%2Fconfig" target="_blank">![NPM Downloads](https://img.shields.io/npm/dm/%40likec4%2Fconfig)</a>
5
+
6
+ Configuration utilities and schema for LikeC4 projects.
7
+
8
+ Provides:
9
+
10
+ - Project config schema (Zod) and JSON Schema for editors
11
+ - Helpers to define TypeScript configs and reusable generators
12
+ - Runtime parsers/validators for JSON/JSON5 configs
13
+ - Node helper to load config files from disk
14
+ - Filename predicates to detect config files
15
+
16
+ Docs – https://likec4.dev/
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pnpm add -D @likec4/config
22
+ ```
23
+
24
+ ## Recognized filenames
25
+
26
+ These are treated as LikeC4 project configuration files:
27
+
28
+ - `.likec4rc`
29
+ - `.likec4.config.json`
30
+ - `likec4.config.json`
31
+ - `likec4.config.js`
32
+ - `likec4.config.mjs`
33
+ - `likec4.config.ts`
34
+ - `likec4.config.mts`
35
+
36
+ See `ConfigFilenames` in `@likec4/config` and helpers `isLikeC4Config(...)`, `isLikeC4JsonConfig(...)`, `isLikeC4NonJsonConfig(...)`.
37
+
38
+ ## Quick start
39
+
40
+ ### JSON/JSON5 config (e.g. `.likec4rc`)
41
+
42
+ ```json5
43
+ {
44
+ // optional: reference JSON Schema for editor validation
45
+ "$schema": "node_modules/@likec4/config/schema.json",
46
+ "name": "my-project",
47
+ "title": "My Project",
48
+ "exclude": ["**/node_modules/**", "**/.cache/**"]
49
+ }
50
+ ```
51
+
52
+ ### TypeScript/JavaScript Config
53
+
54
+ You can define a config using TypeScript or JavaScript. The config file can be any of the following:
55
+
56
+ - `likec4.config.js`
57
+ - `likec4.config.mjs`
58
+ - `likec4.config.ts`
59
+ - `likec4.config.mts`
60
+
61
+ These config files allow you to define custom generators:
62
+
63
+ ```ts
64
+ import { defineConfig } from '@likec4/config'
65
+
66
+ export default defineConfig({
67
+ name: 'my-project',
68
+ title: 'My Project',
69
+ generators: {
70
+ 'hello': async ({ likec4model, ctx }) => {
71
+ for (const view of likec4model.views()) {
72
+ // resolve folder containing the source file of the view
73
+ const { folder } = ctx.locate(view)
74
+ // write view to a JSON file
75
+ await ctx.write({
76
+ path: [folder, 'views', `${view.id}.json`],
77
+ content: JSON.stringify(view.$view),
78
+ })
79
+ }
80
+ },
81
+ },
82
+ })
83
+ ```
84
+
85
+ You can run your generator via CLI:
86
+
87
+ ```bash
88
+ likec4 gen hello
89
+ ```
90
+
91
+ In multi-project workspace use:
92
+
93
+ ```bash
94
+ likec4 gen hello --project my-project
95
+ # Other options
96
+ likec4 gen hello --project my-project --use-dot
97
+ ```
98
+
99
+ There is also helper function `defineGenerators` to define reusable generators:
100
+
101
+ ```ts
102
+ // shared_generators.ts
103
+ import { defineGenerators } from '@likec4/config'
104
+
105
+ export default defineGenerators({
106
+ 'hello': async ({ likec4model, ctx }) => {
107
+ await ctx.write({
108
+ path: 'hello.txt', // relative to the project root
109
+ content: `Project: ${likec4model.project.id}`,
110
+ })
111
+ },
112
+ })
113
+
114
+ // likec4.config.ts
115
+ import { defineConfig } from '@likec4/config'
116
+ import generators from './shared_generators'
117
+
118
+ export default defineConfig({
119
+ name: 'my-project',
120
+ title: 'My Project',
121
+ generators,
122
+ })
123
+ ```
124
+
125
+ ## Programmatic usage
126
+
127
+ ### Validate/parse JSON config
128
+
129
+ ```ts
130
+ import { validateProjectConfig } from '@likec4/config'
131
+
132
+ const json = `
133
+ {
134
+ name: "my-project" // JSON5 is supported
135
+ }
136
+ `
137
+ const cfg = validateProjectConfig(json)
138
+ // or
139
+ const cfg2 = validateProjectConfig({ name: 'my-project' })
140
+ ```
141
+
142
+ ### Load config from TypeScript/JavaScript
143
+
144
+ Available only in Node.js via `@likec4/config/node`:
145
+
146
+ ```ts
147
+ import { loadConfig } from '@likec4/config/node'
148
+ import { URI } from 'vscode-uri'
149
+
150
+ const uri = URI.file('/path/to/likec4.config.ts')
151
+ const project = await loadConfig(uri)
152
+ ```
153
+
154
+ ### Detect config filenames
155
+
156
+ ```ts
157
+ import { ConfigFilenames, isLikeC4Config, isLikeC4JsonConfig, isLikeC4NonJsonConfig } from '@likec4/config'
158
+
159
+ for (const name of ConfigFilenames) {
160
+ if (!isLikeC4Config(name)) {
161
+ // handle other files
162
+ }
163
+ if (isLikeC4JsonConfig(name)) {
164
+ // handle JSON config
165
+ }
166
+ if (isLikeC4NonJsonConfig(name)) {
167
+ // handle TS/JS config
168
+ }
169
+ }
170
+ ```
171
+
172
+ ## JSON Schema
173
+
174
+ The JSON Schema is published at `@likec4/config/schema.json` and mirrors the Zod schema.
175
+
176
+ Fields:
177
+
178
+ - `name` (required): unique project id within the workspace
179
+ - `title` (optional): human-readable project title
180
+ - `contactPerson` (optional): maintainer/author
181
+ - `exclude` (optional): array of glob patterns (picomatch) to exclude (defaults to `['**/node_modules/**']`)
182
+
183
+ ## Getting help
184
+
185
+ We are always happy to help you get started:
186
+
187
+ - [Join Discord community](https://discord.gg/86ZSpjKAdA) – it is the easiest way to get help
188
+ - [GitHub Discussions](https://github.com/likec4/likec4/discussions) – ask anything about the project or give feedback
189
+
190
+ ## Contributors
191
+
192
+ <a href="https://github.com/likec4/likec4/graphs/contributors">
193
+ <img src="https://contrib.rocks/image?repo=likec4/likec4" />
194
+ </a>
195
+
196
+ [Become a contributor](../../CONTRIBUTING.md)
197
+
198
+ ## Support development
199
+
200
+ LikeC4 is a MIT-licensed open source project with its ongoing development made possible entirely by your support.\
201
+ If you like the project, please consider contributing financially to help grow and improve it.\
202
+ You can support us via [OpenCollective](https://opencollective.com/likec4) or [GitHub Sponsors](https://github.com/sponsors/likec4).
203
+
204
+ ## License
205
+
206
+ This project is released under the [MIT License](LICENSE)
@@ -0,0 +1,45 @@
1
+ import { type GeneratorFn, type LikeC4ProjectConfig } from './schema';
2
+ /**
3
+ * Defines LikeC4 Project, allows custom generators that can be executed using CLI:
4
+ *
5
+ * `$ likec4 gen <generator-name>`
6
+ *
7
+ * or VSCode command `LikeC4: Run code generator`
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * export default defineConfig({
12
+ * name: 'my-project',
13
+ * title: 'My Project',
14
+ * exclude: ['picomatch pattern'],
15
+ * generators: {
16
+ * 'my-generator': async ({ likec4model, ctx }) => {
17
+ * await ctx.write('my-generator.txt', likec4model.project.id)
18
+ * }
19
+ * }
20
+ * })
21
+ * ```
22
+ */
23
+ export declare function defineConfig<const C extends LikeC4ProjectConfig>(config: C): C;
24
+ /**
25
+ * Define reusable custom generators
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * // generators.ts
30
+ * export default defineGenerators({
31
+ * 'my-generator': async ({ likec4model, ctx }) => {
32
+ * await ctx.write('my-generator.txt', likec4model.project.id)
33
+ * }
34
+ * })
35
+ *
36
+ * // likec4.config.ts
37
+ * import generators from './generators'
38
+ *
39
+ * export default defineConfig({
40
+ * name: 'my-project',
41
+ * generators,
42
+ * })
43
+ * ```
44
+ */
45
+ export declare function defineGenerators<const G extends Record<string, GeneratorFn>>(generators: G): G;
@@ -0,0 +1,7 @@
1
+ import { GeneratorsSchema, LikeC4ProjectConfigSchema } from "./schema.mjs";
2
+ export function defineConfig(config) {
3
+ return LikeC4ProjectConfigSchema.parse(config);
4
+ }
5
+ export function defineGenerators(generators) {
6
+ return GeneratorsSchema.parse(generators);
7
+ }
@@ -0,0 +1,16 @@
1
+ declare const configJsonFilenames: readonly [".likec4rc", ".likec4.config.json", "likec4.config.json"];
2
+ declare const configNonJsonFilenames: readonly ["likec4.config.js", "likec4.config.mjs", "likec4.config.ts", "likec4.config.mts"];
3
+ export declare const ConfigFilenames: readonly [".likec4rc", ".likec4.config.json", "likec4.config.json", "likec4.config.js", "likec4.config.mjs", "likec4.config.ts", "likec4.config.mts"];
4
+ /**
5
+ * Checks if the given filename is a LikeC4 JSON config file (JSON, RC).
6
+ */
7
+ export declare function isLikeC4JsonConfig(filename: string): filename is typeof configJsonFilenames[number];
8
+ /**
9
+ * Checks if the given filename is a LikeC4 non-JSON config file (JS, MJS, TS, MTS)
10
+ */
11
+ export declare function isLikeC4NonJsonConfig(filename: string): filename is typeof configNonJsonFilenames[number];
12
+ /**
13
+ * Checks if the given filename is a LikeC4 config file (JSON or non-JSON)
14
+ */
15
+ export declare function isLikeC4Config(filename: string): filename is typeof ConfigFilenames[number];
16
+ export {};
@@ -0,0 +1,34 @@
1
+ const configJsonFilenames = [
2
+ ".likec4rc",
3
+ ".likec4.config.json",
4
+ "likec4.config.json"
5
+ ];
6
+ const configNonJsonFilenames = [
7
+ "likec4.config.js",
8
+ "likec4.config.mjs",
9
+ "likec4.config.ts",
10
+ "likec4.config.mts"
11
+ ];
12
+ export const ConfigFilenames = [
13
+ ...configJsonFilenames,
14
+ ...configNonJsonFilenames
15
+ ];
16
+ export function isLikeC4JsonConfig(filename) {
17
+ for (const ext of configJsonFilenames) {
18
+ if (filename.endsWith(ext)) {
19
+ return true;
20
+ }
21
+ }
22
+ return false;
23
+ }
24
+ export function isLikeC4NonJsonConfig(filename) {
25
+ for (const ext of configNonJsonFilenames) {
26
+ if (filename.endsWith(ext)) {
27
+ return true;
28
+ }
29
+ }
30
+ return false;
31
+ }
32
+ export function isLikeC4Config(filename) {
33
+ return isLikeC4JsonConfig(filename) || isLikeC4NonJsonConfig(filename);
34
+ }
@@ -0,0 +1,4 @@
1
+ export type { GeneratorFn, GeneratorFnContext, GeneratorFnParams, LikeC4ProjectConfig, LikeC4ProjectJsonConfig, } from './schema';
2
+ export { serializableLikeC4ProjectConfig, validateProjectConfig } from './schema';
3
+ export { ConfigFilenames, isLikeC4Config, isLikeC4JsonConfig, isLikeC4NonJsonConfig, } from './filenames';
4
+ export { defineConfig, defineGenerators, } from './define-config';
package/dist/index.mjs ADDED
@@ -0,0 +1,11 @@
1
+ export { serializableLikeC4ProjectConfig, validateProjectConfig } from "./schema.mjs";
2
+ export {
3
+ ConfigFilenames,
4
+ isLikeC4Config,
5
+ isLikeC4JsonConfig,
6
+ isLikeC4NonJsonConfig
7
+ } from "./filenames.mjs";
8
+ export {
9
+ defineConfig,
10
+ defineGenerators
11
+ } from "./define-config.mjs";
@@ -0,0 +1 @@
1
+ export declare const logger: any;
@@ -0,0 +1,2 @@
1
+ import { rootLogger } from "@likec4/log";
2
+ export const logger = rootLogger.getChild("config");
@@ -0,0 +1,2 @@
1
+ export * from '../index';
2
+ export { loadConfig } from './load-config';
@@ -0,0 +1,2 @@
1
+ export * from "../index.mjs";
2
+ export { loadConfig } from "./load-config.mjs";
@@ -0,0 +1,7 @@
1
+ import type { URI } from 'vscode-uri';
2
+ import { type LikeC4ProjectConfig } from '../schema';
3
+ /**
4
+ * Load LikeC4 Project config file.
5
+ * If filepath is a non-JSON file, it will be bundled and required
6
+ */
7
+ export declare function loadConfig(filepath: URI): Promise<LikeC4ProjectConfig>;
@@ -0,0 +1,29 @@
1
+ import { invariant } from "@likec4/core";
2
+ import { bundleNRequire } from "bundle-n-require";
3
+ import * as fs from "node:fs/promises";
4
+ import { defineConfig } from "../define-config.mjs";
5
+ import { isLikeC4JsonConfig, isLikeC4NonJsonConfig } from "../filenames.mjs";
6
+ import { logger } from "../logger.mjs";
7
+ import { validateProjectConfig } from "../schema.mjs";
8
+ export async function loadConfig(filepath) {
9
+ logger.debug`Loading config file: ${filepath.fsPath}`;
10
+ if (isLikeC4JsonConfig(filepath.fsPath)) {
11
+ try {
12
+ const content = await fs.readFile(filepath.fsPath, "utf-8");
13
+ return validateProjectConfig(content);
14
+ } catch (err) {
15
+ logger.error(`Failed to load json config file: ${filepath.fsPath}`, { err });
16
+ throw err;
17
+ }
18
+ }
19
+ invariant(isLikeC4NonJsonConfig(filepath.fsPath), `Invalid config file: ${filepath.fsPath}`);
20
+ try {
21
+ const { mod } = await bundleNRequire(filepath.fsPath, {
22
+ interopDefault: true
23
+ });
24
+ return defineConfig(mod?.default ?? mod);
25
+ } catch (err) {
26
+ logger.error(`Failed to load config file: ${filepath.fsPath}`, { err });
27
+ throw err;
28
+ }
29
+ }
@@ -0,0 +1,149 @@
1
+ import type { DeploymentElementModel, DeploymentRelationModel, ElementModel, LikeC4Model, LikeC4ViewModel, RelationshipModel } from '@likec4/core/model';
2
+ import type { aux, ProjectId } from '@likec4/core/types';
3
+ import type Stream from 'node:stream';
4
+ import type { URI } from 'vscode-uri';
5
+ import * as z from 'zod';
6
+ export declare const LikeC4ProjectJsonConfigSchema: z.ZodObject<{
7
+ name: z.ZodPipe<z.ZodString, z.ZodTransform<any, string>>;
8
+ title: z.ZodOptional<z.ZodString>;
9
+ contactPerson: z.ZodOptional<z.ZodString>;
10
+ imageAliases: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
11
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
12
+ }, z.z.core.$strip>;
13
+ export type LikeC4ProjectJsonConfig = z.input<typeof LikeC4ProjectJsonConfigSchema>;
14
+ export declare const GeneratorsSchema: z.ZodRecord<z.ZodString, z.ZodCustom<Function, Function>>;
15
+ export declare const LikeC4ProjectConfigSchema: z.ZodObject<{
16
+ name: z.ZodPipe<z.ZodString, z.ZodTransform<any, string>>;
17
+ title: z.ZodOptional<z.ZodString>;
18
+ contactPerson: z.ZodOptional<z.ZodString>;
19
+ imageAliases: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
20
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
+ generators: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodCustom<Function, Function>>>;
22
+ }, z.z.core.$strip>;
23
+ export interface GeneratorFnContext {
24
+ /**
25
+ * Workspace root directory
26
+ */
27
+ readonly workspace: URI;
28
+ /**
29
+ * Current project
30
+ */
31
+ readonly project: {
32
+ /**
33
+ * Project name
34
+ */
35
+ readonly id: ProjectId;
36
+ readonly title?: string;
37
+ /**
38
+ * Project folder
39
+ */
40
+ readonly folder: URI;
41
+ };
42
+ /**
43
+ * Returns the location of the specified element, relation, view or deployment element
44
+ */
45
+ locate(target: ElementModel | RelationshipModel | DeploymentRelationModel | LikeC4ViewModel | DeploymentElementModel): {
46
+ /**
47
+ * Range inside the source file
48
+ */
49
+ range: {
50
+ start: {
51
+ line: number;
52
+ character: number;
53
+ };
54
+ end: {
55
+ line: number;
56
+ character: number;
57
+ };
58
+ };
59
+ /**
60
+ * Full path to the source file
61
+ */
62
+ document: URI;
63
+ /**
64
+ * Document path relative to the project folder
65
+ */
66
+ relativePath: string;
67
+ /**
68
+ * Folder, containing the source file ("dirname" of document)
69
+ */
70
+ folder: string;
71
+ /**
72
+ * Source file name ("basename" of document)
73
+ */
74
+ filename: string;
75
+ };
76
+ /**
77
+ * Write a file
78
+ * @param path - Path to the file, either absolute or relative to the project folder
79
+ * All folders will be created automatically
80
+ * @param content - Content of the file
81
+ */
82
+ write(file: {
83
+ path: string | string[] | URI;
84
+ content: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView> | Stream;
85
+ }): Promise<void>;
86
+ /**
87
+ * Abort the process
88
+ */
89
+ abort(reason?: string): never;
90
+ }
91
+ export type GeneratorFnParams = {
92
+ /**
93
+ * LikeC4 model
94
+ */
95
+ likec4model: LikeC4Model<aux.UnknownLayouted>;
96
+ /**
97
+ * Generator context
98
+ */
99
+ ctx: GeneratorFnContext;
100
+ };
101
+ export interface GeneratorFn {
102
+ (params: GeneratorFnParams): Promise<void> | void;
103
+ }
104
+ /**
105
+ * LikeC4 project configuration
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * export default defineConfig({
110
+ * name: 'my-project',
111
+ * generators: {
112
+ * 'my-generator': async ({ likec4model, ctx }) => {
113
+ * await ctx.write('my-generator.txt', likec4model.project.id)
114
+ * }
115
+ * }
116
+ * })
117
+ * ```
118
+ */
119
+ export type LikeC4ProjectConfig = z.input<typeof LikeC4ProjectJsonConfigSchema> & {
120
+ /**
121
+ * Add custom generators to the project
122
+ * @example
123
+ * ```ts
124
+ * export default defineConfig({
125
+ * name: 'my-project',
126
+ * generators: {
127
+ * 'my-generator': async ({ likec4model, ctx }) => {
128
+ * await ctx.write('my-generator.txt', likec4model.project.id)
129
+ * }
130
+ * }
131
+ * })
132
+ * ```
133
+ *
134
+ * Execute generator:
135
+ * ```bash
136
+ * likec4 gen my-generator
137
+ * ```
138
+ */
139
+ generators?: Record<string, GeneratorFn> | undefined;
140
+ };
141
+ /**
142
+ * Validates JSON string or JSON object into a LikeC4ProjectConfig object.
143
+ */
144
+ export declare function validateProjectConfig<C extends string | Record<string, unknown>>(config: C): LikeC4ProjectConfig;
145
+ /**
146
+ * Converts a LikeC4ProjectConfig object into a LikeC4ProjectJsonConfig object.
147
+ * Omit generators property (as it is not serializable)
148
+ */
149
+ export declare function serializableLikeC4ProjectConfig({ generators, ...config }: LikeC4ProjectConfig): LikeC4ProjectJsonConfig;
@@ -0,0 +1,5 @@
1
+ import * as z from 'zod';
2
+ export declare const ImageAliasesSchema: z.ZodRecord<z.ZodString, z.ZodString>;
3
+ type LikeC4ImageAliasConfig = z.infer<typeof ImageAliasesSchema>;
4
+ export declare function validateImageAliases(imageAliases?: LikeC4ImageAliasConfig): void;
5
+ export {};
@@ -0,0 +1,42 @@
1
+ import * as z from "zod";
2
+ const IMAGE_ALIAS_KEY_REGEX = /^@[A-Za-z0-9_-]*$/;
3
+ const IMAGE_ALIAS_VALUE_REGEX = /^(?!\/|[A-Za-z]:[\\\/])(?!.*:\/\/).*$/;
4
+ const ImageAliasValue = z.string().nonempty("Image alias value cannot be empty").regex(
5
+ IMAGE_ALIAS_VALUE_REGEX,
6
+ "Image alias value must be a relative path (no leading slash or protocol)"
7
+ );
8
+ export const ImageAliasesSchema = z.record(
9
+ z.string(),
10
+ // PLAIN key schema - valibot JSON schema export-safe.
11
+ ImageAliasValue
12
+ ).meta({
13
+ description: "Map of image alias prefixes to relative paths (keys must match /^@\\w+$/; values must be relative paths without protocol or leading slash)."
14
+ });
15
+ export function validateImageAliases(imageAliases) {
16
+ const invalidKeys = [];
17
+ const invalidValues = [];
18
+ if (imageAliases) {
19
+ for (const [key, value] of Object.entries(imageAliases)) {
20
+ if (!IMAGE_ALIAS_KEY_REGEX.test(key)) {
21
+ invalidKeys.push(key);
22
+ }
23
+ if (!IMAGE_ALIAS_VALUE_REGEX.test(value)) {
24
+ invalidValues.push(`${key} -> ${value}`);
25
+ }
26
+ }
27
+ }
28
+ if (invalidKeys.length || invalidValues.length) {
29
+ const parts = [];
30
+ if (invalidKeys.length) {
31
+ parts.push(
32
+ `Invalid image alias key(s): ${invalidKeys.map((k) => JSON.stringify(k)).join(", ")} (must match ${IMAGE_ALIAS_KEY_REGEX})`
33
+ );
34
+ }
35
+ if (invalidValues.length) {
36
+ parts.push(
37
+ `Invalid image alias value(s): ${invalidValues.map((kv) => JSON.stringify(kv)).join(", ")} (must match ${IMAGE_ALIAS_VALUE_REGEX})`
38
+ );
39
+ }
40
+ throw new Error(parts.join(" | "));
41
+ }
42
+ }
@@ -0,0 +1,38 @@
1
+ import JSON5 from "json5";
2
+ import * as z from "zod";
3
+ import { ImageAliasesSchema, validateImageAliases } from "./schema.image-alias.mjs";
4
+ export const LikeC4ProjectJsonConfigSchema = z.object({
5
+ name: z.string().nonempty("Project name cannot be empty").refine((value) => value !== "default", {
6
+ abort: true,
7
+ error: 'Project name cannot be "default"'
8
+ }).refine((value) => !value.includes(".") && !value.includes("@") && !value.includes("#"), {
9
+ abort: true,
10
+ error: 'Project name cannot contain ".", "@" or "#", try to use A-z, 0-9, _ and -'
11
+ }).transform((value) => value).meta({ description: "Project name, must be unique in the workspace" }),
12
+ title: z.string().nonempty("Project title cannot be empty if specified").optional().meta({ description: "A human readable title for the project" }),
13
+ 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" }),
14
+ imageAliases: ImageAliasesSchema.optional(),
15
+ exclude: z.array(z.string()).optional().meta({ description: 'List of file patterns to exclude from the project, default is ["**/node_modules/**"]' })
16
+ }).meta({
17
+ description: "LikeC4 project configuration"
18
+ });
19
+ const FunctionType = z.instanceof(Function);
20
+ export const GeneratorsSchema = z.record(z.string(), FunctionType);
21
+ export const LikeC4ProjectConfigSchema = LikeC4ProjectJsonConfigSchema.extend({
22
+ generators: GeneratorsSchema.optional()
23
+ });
24
+ export function validateProjectConfig(config) {
25
+ const parsed = LikeC4ProjectConfigSchema.safeParse(
26
+ typeof config === "string" ? JSON5.parse(config) : config
27
+ );
28
+ if (!parsed.success) {
29
+ throw new Error("Config validation failed:\n" + z.prettifyError(parsed.error));
30
+ }
31
+ if (parsed.data.imageAliases) {
32
+ validateImageAliases(parsed.data.imageAliases);
33
+ }
34
+ return parsed.data;
35
+ }
36
+ export function serializableLikeC4ProjectConfig({ generators, ...config }) {
37
+ return LikeC4ProjectJsonConfigSchema.parse(config);
38
+ }
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@likec4/config",
3
+ "version": "1.39.0",
4
+ "license": "MIT",
5
+ "homepage": "https://likec4.dev",
6
+ "author": "Denis Davydkov <denis@davydkov.com>",
7
+ "description": "A configuration package for LikeC4.",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/likec4/likec4.git",
11
+ "directory": "packages/config"
12
+ },
13
+ "bugs": "https://github.com/likec4/likec4/issues",
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "schema.json",
18
+ "!**/*.spec.ts",
19
+ "!**/*.test-d.ts",
20
+ "!**/__*/*",
21
+ "!**/*.map"
22
+ ],
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "exports": {
26
+ ".": {
27
+ "node": {
28
+ "sources": "./src/node/index.ts",
29
+ "default": {
30
+ "types": "./dist/node/index.d.ts",
31
+ "default": "./dist/node/index.mjs"
32
+ }
33
+ },
34
+ "sources": "./src/index.ts",
35
+ "default": {
36
+ "types": "./dist/index.d.ts",
37
+ "default": "./dist/index.mjs"
38
+ }
39
+ },
40
+ "./node": {
41
+ "sources": "./src/node/index.ts",
42
+ "default": {
43
+ "types": "./dist/node/index.d.ts",
44
+ "default": "./dist/node/index.mjs"
45
+ }
46
+ },
47
+ "./src": "./src/index.ts",
48
+ "./src/*": "./src/*",
49
+ "./package.json": "./package.json",
50
+ "./schema.json": "./schema.json"
51
+ },
52
+ "publishConfig": {
53
+ "registry": "https://registry.npmjs.org",
54
+ "access": "public"
55
+ },
56
+ "dependencies": {
57
+ "bundle-n-require": "^1.1.2",
58
+ "defu": "^6.1.4",
59
+ "json5": "^2.2.3",
60
+ "remeda": "^2.23.1",
61
+ "type-fest": "^4.41.0",
62
+ "vscode-uri": "3.1.0",
63
+ "zod": "^4.0.17",
64
+ "@likec4/log": "1.39.0",
65
+ "@likec4/core": "1.39.0"
66
+ },
67
+ "devDependencies": {
68
+ "@types/node": "~20.19.11",
69
+ "tsx": "4.20.3",
70
+ "turbo": "2.5.6",
71
+ "typescript": "5.9.2",
72
+ "unbuild": "3.5.0",
73
+ "vitest": "3.2.4",
74
+ "@likec4/tsconfig": "1.39.0"
75
+ },
76
+ "scripts": {
77
+ "generate": "tsx scripts/generate.mts",
78
+ "typecheck": "tsc -b --verbose",
79
+ "build": "unbuild",
80
+ "lint:package": "pnpx publint ./package.tgz",
81
+ "clean": "pnpm rimraf dist lib",
82
+ "pack": "pnpm pack"
83
+ }
84
+ }
package/schema.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "description": "LikeC4 project configuration",
4
+ "type": "object",
5
+ "properties": {
6
+ "name": {
7
+ "description": "Project name, must be unique in the workspace",
8
+ "type": "string",
9
+ "minLength": 1
10
+ },
11
+ "title": {
12
+ "description": "A human readable title for the project",
13
+ "type": "string",
14
+ "minLength": 1
15
+ },
16
+ "contactPerson": {
17
+ "description": "A person who has been involved in creating or maintaining this project",
18
+ "type": "string",
19
+ "minLength": 1
20
+ },
21
+ "imageAliases": {
22
+ "description": "Map of image alias prefixes to relative paths (keys must match /^@\\w+$/; values must be relative paths without protocol or leading slash).",
23
+ "type": "object",
24
+ "propertyNames": {
25
+ "type": "string"
26
+ },
27
+ "additionalProperties": {
28
+ "type": "string",
29
+ "minLength": 1,
30
+ "pattern": "^(?!\\/|[A-Za-z]:[\\\\\\/])(?!.*:\\/\\/).*$"
31
+ }
32
+ },
33
+ "exclude": {
34
+ "description": "List of file patterns to exclude from the project, default is [\"**/node_modules/**\"]",
35
+ "type": "array",
36
+ "items": {
37
+ "type": "string"
38
+ }
39
+ }
40
+ },
41
+ "required": [
42
+ "name"
43
+ ]
44
+ }
@@ -0,0 +1,51 @@
1
+ import { type GeneratorFn, type LikeC4ProjectConfig, GeneratorsSchema, LikeC4ProjectConfigSchema } from './schema'
2
+
3
+ /**
4
+ * Defines LikeC4 Project, allows custom generators that can be executed using CLI:
5
+ *
6
+ * `$ likec4 gen <generator-name>`
7
+ *
8
+ * or VSCode command `LikeC4: Run code generator`
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * export default defineConfig({
13
+ * name: 'my-project',
14
+ * title: 'My Project',
15
+ * exclude: ['picomatch pattern'],
16
+ * generators: {
17
+ * 'my-generator': async ({ likec4model, ctx }) => {
18
+ * await ctx.write('my-generator.txt', likec4model.project.id)
19
+ * }
20
+ * }
21
+ * })
22
+ * ```
23
+ */
24
+ export function defineConfig<const C extends LikeC4ProjectConfig>(config: C): C {
25
+ return LikeC4ProjectConfigSchema.parse(config) as unknown as C
26
+ }
27
+
28
+ /**
29
+ * Define reusable custom generators
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * // generators.ts
34
+ * export default defineGenerators({
35
+ * 'my-generator': async ({ likec4model, ctx }) => {
36
+ * await ctx.write('my-generator.txt', likec4model.project.id)
37
+ * }
38
+ * })
39
+ *
40
+ * // likec4.config.ts
41
+ * import generators from './generators'
42
+ *
43
+ * export default defineConfig({
44
+ * name: 'my-project',
45
+ * generators,
46
+ * })
47
+ * ```
48
+ */
49
+ export function defineGenerators<const G extends Record<string, GeneratorFn>>(generators: G): G {
50
+ return GeneratorsSchema.parse(generators) as unknown as G
51
+ }
@@ -0,0 +1,48 @@
1
+ const configJsonFilenames = [
2
+ '.likec4rc',
3
+ '.likec4.config.json',
4
+ 'likec4.config.json',
5
+ ] as const
6
+
7
+ const configNonJsonFilenames = [
8
+ 'likec4.config.js',
9
+ 'likec4.config.mjs',
10
+ 'likec4.config.ts',
11
+ 'likec4.config.mts',
12
+ ] as const
13
+
14
+ export const ConfigFilenames = [
15
+ ...configJsonFilenames,
16
+ ...configNonJsonFilenames,
17
+ ] as const
18
+
19
+ /**
20
+ * Checks if the given filename is a LikeC4 JSON config file (JSON, RC).
21
+ */
22
+ export function isLikeC4JsonConfig(filename: string): filename is typeof configJsonFilenames[number] {
23
+ for (const ext of configJsonFilenames) {
24
+ if (filename.endsWith(ext)) {
25
+ return true
26
+ }
27
+ }
28
+ return false
29
+ }
30
+
31
+ /**
32
+ * Checks if the given filename is a LikeC4 non-JSON config file (JS, MJS, TS, MTS)
33
+ */
34
+ export function isLikeC4NonJsonConfig(filename: string): filename is typeof configNonJsonFilenames[number] {
35
+ for (const ext of configNonJsonFilenames) {
36
+ if (filename.endsWith(ext)) {
37
+ return true
38
+ }
39
+ }
40
+ return false
41
+ }
42
+
43
+ /**
44
+ * Checks if the given filename is a LikeC4 config file (JSON or non-JSON)
45
+ */
46
+ export function isLikeC4Config(filename: string): filename is typeof ConfigFilenames[number] {
47
+ return isLikeC4JsonConfig(filename) || isLikeC4NonJsonConfig(filename)
48
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ export type {
2
+ GeneratorFn,
3
+ GeneratorFnContext,
4
+ GeneratorFnParams,
5
+ LikeC4ProjectConfig,
6
+ LikeC4ProjectJsonConfig,
7
+ } from './schema'
8
+
9
+ export { serializableLikeC4ProjectConfig, validateProjectConfig } from './schema'
10
+
11
+ export {
12
+ ConfigFilenames,
13
+ isLikeC4Config,
14
+ isLikeC4JsonConfig,
15
+ isLikeC4NonJsonConfig,
16
+ } from './filenames'
17
+
18
+ export {
19
+ defineConfig,
20
+ defineGenerators,
21
+ } from './define-config'
package/src/logger.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { rootLogger } from '@likec4/log'
2
+
3
+ export const logger = rootLogger.getChild('config')
@@ -0,0 +1,2 @@
1
+ export * from '../index'
2
+ export { loadConfig } from './load-config'
@@ -0,0 +1,36 @@
1
+ import { invariant } from '@likec4/core'
2
+ import { bundleNRequire } from 'bundle-n-require'
3
+ import * as fs from 'node:fs/promises'
4
+ import type { URI } from 'vscode-uri'
5
+ import { defineConfig } from '../define-config'
6
+ import { isLikeC4JsonConfig, isLikeC4NonJsonConfig } from '../filenames'
7
+ import { logger } from '../logger'
8
+ import { type LikeC4ProjectConfig, validateProjectConfig } from '../schema'
9
+
10
+ /**
11
+ * Load LikeC4 Project config file.
12
+ * If filepath is a non-JSON file, it will be bundled and required
13
+ */
14
+ export async function loadConfig(filepath: URI): Promise<LikeC4ProjectConfig> {
15
+ logger.debug`Loading config file: ${filepath.fsPath}`
16
+ if (isLikeC4JsonConfig(filepath.fsPath)) {
17
+ try {
18
+ const content = await fs.readFile(filepath.fsPath, 'utf-8')
19
+ return validateProjectConfig(content)
20
+ } catch (err) {
21
+ logger.error(`Failed to load json config file: ${filepath.fsPath}`, { err })
22
+ throw err
23
+ }
24
+ }
25
+
26
+ invariant(isLikeC4NonJsonConfig(filepath.fsPath), `Invalid config file: ${filepath.fsPath}`)
27
+ try {
28
+ const { mod } = await bundleNRequire(filepath.fsPath, {
29
+ interopDefault: true,
30
+ })
31
+ return defineConfig(mod?.default ?? mod)
32
+ } catch (err) {
33
+ logger.error(`Failed to load config file: ${filepath.fsPath}`, { err })
34
+ throw err
35
+ }
36
+ }
@@ -0,0 +1,68 @@
1
+ import * as z from 'zod'
2
+
3
+ // Key must be prefixed with "@" and contain only allowed characters
4
+ const IMAGE_ALIAS_KEY_REGEX = /^@[A-Za-z0-9_-]*$/
5
+ // Relative path (no leading slash, drive letter, or protocol)
6
+ const IMAGE_ALIAS_VALUE_REGEX = /^(?!\/|[A-Za-z]:[\\\/])(?!.*:\/\/).*$/
7
+
8
+ // Schema for an image alias value: must be a non-empty string representing a relative path (no leading slash, drive letter, or protocol).
9
+ const ImageAliasValue = z
10
+ .string()
11
+ .nonempty('Image alias value cannot be empty')
12
+ .regex(
13
+ IMAGE_ALIAS_VALUE_REGEX,
14
+ 'Image alias value must be a relative path (no leading slash or protocol)',
15
+ )
16
+
17
+ export const ImageAliasesSchema = z.record(
18
+ z.string(), // PLAIN key schema - valibot JSON schema export-safe.
19
+ ImageAliasValue,
20
+ ).meta({
21
+ description:
22
+ 'Map of image alias prefixes to relative paths (keys must match /^@\\w+$/; values must be relative paths without protocol or leading slash).',
23
+ })
24
+
25
+ // This just allows us to have a typed validate function.
26
+ type LikeC4ImageAliasConfig = z.infer<typeof ImageAliasesSchema>
27
+
28
+ export function validateImageAliases(imageAliases?: LikeC4ImageAliasConfig) {
29
+ const invalidKeys: string[] = []
30
+ const invalidValues: string[] = []
31
+
32
+ if (imageAliases) {
33
+ for (const [key, value] of Object.entries(imageAliases)) {
34
+ if (!IMAGE_ALIAS_KEY_REGEX.test(key)) {
35
+ invalidKeys.push(key)
36
+ }
37
+ // Value regex is technically already enforced by Valibot,
38
+ // so this check is purely defensive.
39
+ if (!IMAGE_ALIAS_VALUE_REGEX.test(value)) {
40
+ invalidValues.push(`${key} -> ${value}`)
41
+ }
42
+ }
43
+ }
44
+
45
+ if (invalidKeys.length || invalidValues.length) {
46
+ const parts: string[] = []
47
+ if (invalidKeys.length) {
48
+ parts.push(
49
+ `Invalid image alias key(s): ${
50
+ invalidKeys
51
+ .map((k) => JSON.stringify(k))
52
+ .join(', ')
53
+ } (must match ${IMAGE_ALIAS_KEY_REGEX})`,
54
+ )
55
+ }
56
+ if (invalidValues.length) {
57
+ parts.push(
58
+ `Invalid image alias value(s): ${
59
+ invalidValues
60
+ .map((kv) => JSON.stringify(kv))
61
+ .join(', ')
62
+ } (must match ${IMAGE_ALIAS_VALUE_REGEX})`,
63
+ )
64
+ }
65
+
66
+ throw new Error(parts.join(' | '))
67
+ }
68
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,226 @@
1
+ import type {
2
+ DeploymentElementModel,
3
+ DeploymentRelationModel,
4
+ ElementModel,
5
+ LikeC4Model,
6
+ LikeC4ViewModel,
7
+ RelationshipModel,
8
+ } from '@likec4/core/model'
9
+ import type {
10
+ aux,
11
+ ProjectId,
12
+ } from '@likec4/core/types'
13
+ import JSON5 from 'json5'
14
+ import type Stream from 'node:stream'
15
+ import type { URI } from 'vscode-uri'
16
+ import * as z from 'zod'
17
+ import { ImageAliasesSchema, validateImageAliases } from './schema.image-alias'
18
+
19
+ export const LikeC4ProjectJsonConfigSchema = z.object({
20
+ name: z.string()
21
+ .nonempty('Project name cannot be empty')
22
+ .refine((value) => value !== 'default', {
23
+ abort: true,
24
+ error: 'Project name cannot be "default"',
25
+ })
26
+ .refine((value) => !value.includes('.') && !value.includes('@') && !value.includes('#'), {
27
+ abort: true,
28
+ error: 'Project name cannot contain ".", "@" or "#", try to use A-z, 0-9, _ and -',
29
+ })
30
+ .transform((value) => value as ProjectId)
31
+ .meta({ description: 'Project name, must be unique in the workspace' }),
32
+ title: z.string()
33
+ .nonempty('Project title cannot be empty if specified')
34
+ .optional()
35
+ .meta({ description: 'A human readable title for the project' }),
36
+ contactPerson: z.string()
37
+ .nonempty('Contact person cannot be empty if specified')
38
+ .optional()
39
+ .meta({ description: 'A person who has been involved in creating or maintaining this project' }),
40
+ imageAliases: ImageAliasesSchema
41
+ .optional(),
42
+ exclude: z.array(z.string())
43
+ .optional()
44
+ .meta({ description: 'List of file patterns to exclude from the project, default is ["**/node_modules/**"]' }),
45
+ })
46
+ .meta({
47
+ description: 'LikeC4 project configuration',
48
+ })
49
+
50
+ export type LikeC4ProjectJsonConfig = z.input<typeof LikeC4ProjectJsonConfigSchema>
51
+
52
+ const FunctionType = z.instanceof(Function)
53
+ export const GeneratorsSchema = z.record(z.string(), FunctionType)
54
+ export const LikeC4ProjectConfigSchema = LikeC4ProjectJsonConfigSchema.extend({
55
+ generators: GeneratorsSchema.optional(),
56
+ })
57
+
58
+ export interface GeneratorFnContext {
59
+ /**
60
+ * Workspace root directory
61
+ */
62
+ readonly workspace: URI
63
+
64
+ /**
65
+ * Current project
66
+ */
67
+ readonly project: {
68
+ /**
69
+ * Project name
70
+ */
71
+ readonly id: ProjectId
72
+
73
+ readonly title?: string
74
+
75
+ /**
76
+ * Project folder
77
+ */
78
+ readonly folder: URI
79
+ }
80
+
81
+ /**
82
+ * Returns the location of the specified element, relation, view or deployment element
83
+ */
84
+ locate(
85
+ target:
86
+ | ElementModel
87
+ | RelationshipModel
88
+ | DeploymentRelationModel
89
+ | LikeC4ViewModel
90
+ | DeploymentElementModel,
91
+ ): {
92
+ /**
93
+ * Range inside the source file
94
+ */
95
+ range: {
96
+ start: {
97
+ line: number
98
+ character: number
99
+ }
100
+ end: {
101
+ line: number
102
+ character: number
103
+ }
104
+ }
105
+ /**
106
+ * Full path to the source file
107
+ */
108
+ document: URI
109
+ /**
110
+ * Document path relative to the project folder
111
+ */
112
+ relativePath: string
113
+ /**
114
+ * Folder, containing the source file ("dirname" of document)
115
+ */
116
+ folder: string
117
+ /**
118
+ * Source file name ("basename" of document)
119
+ */
120
+ filename: string
121
+ }
122
+
123
+ /**
124
+ * Write a file
125
+ * @param path - Path to the file, either absolute or relative to the project folder
126
+ * All folders will be created automatically
127
+ * @param content - Content of the file
128
+ */
129
+ write(file: {
130
+ path: string | string[] | URI
131
+ content:
132
+ | string
133
+ | NodeJS.ArrayBufferView
134
+ | Iterable<string | NodeJS.ArrayBufferView>
135
+ | AsyncIterable<string | NodeJS.ArrayBufferView>
136
+ | Stream
137
+ }): Promise<void>
138
+
139
+ /**
140
+ * Abort the process
141
+ */
142
+ abort(reason?: string): never
143
+ }
144
+
145
+ export type GeneratorFnParams = {
146
+ /**
147
+ * LikeC4 model
148
+ */
149
+ likec4model: LikeC4Model<aux.UnknownLayouted>
150
+
151
+ /**
152
+ * Generator context
153
+ */
154
+ ctx: GeneratorFnContext
155
+ }
156
+
157
+ export interface GeneratorFn {
158
+ (params: GeneratorFnParams): Promise<void> | void
159
+ }
160
+
161
+ /**
162
+ * LikeC4 project configuration
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * export default defineConfig({
167
+ * name: 'my-project',
168
+ * generators: {
169
+ * 'my-generator': async ({ likec4model, ctx }) => {
170
+ * await ctx.write('my-generator.txt', likec4model.project.id)
171
+ * }
172
+ * }
173
+ * })
174
+ * ```
175
+ */
176
+ export type LikeC4ProjectConfig = z.input<typeof LikeC4ProjectJsonConfigSchema> & {
177
+ /**
178
+ * Add custom generators to the project
179
+ * @example
180
+ * ```ts
181
+ * export default defineConfig({
182
+ * name: 'my-project',
183
+ * generators: {
184
+ * 'my-generator': async ({ likec4model, ctx }) => {
185
+ * await ctx.write('my-generator.txt', likec4model.project.id)
186
+ * }
187
+ * }
188
+ * })
189
+ * ```
190
+ *
191
+ * Execute generator:
192
+ * ```bash
193
+ * likec4 gen my-generator
194
+ * ```
195
+ */
196
+ generators?: Record<string, GeneratorFn> | undefined
197
+ }
198
+
199
+ /**
200
+ * Validates JSON string or JSON object into a LikeC4ProjectConfig object.
201
+ */
202
+ export function validateProjectConfig<C extends string | Record<string, unknown>>(
203
+ config: C,
204
+ ): LikeC4ProjectConfig {
205
+ const parsed = LikeC4ProjectConfigSchema.safeParse(
206
+ typeof config === 'string' ? JSON5.parse(config) : config,
207
+ )
208
+ if (!parsed.success) {
209
+ throw new Error('Config validation failed:\n' + z.prettifyError(parsed.error))
210
+ }
211
+ // TODO: rewrite with zod refine
212
+ if (parsed.data.imageAliases) {
213
+ validateImageAliases(parsed.data.imageAliases)
214
+ }
215
+ return parsed.data as unknown as LikeC4ProjectConfig
216
+ }
217
+
218
+ /**
219
+ * Converts a LikeC4ProjectConfig object into a LikeC4ProjectJsonConfig object.
220
+ * Omit generators property (as it is not serializable)
221
+ */
222
+ export function serializableLikeC4ProjectConfig(
223
+ { generators, ...config }: LikeC4ProjectConfig,
224
+ ): LikeC4ProjectJsonConfig {
225
+ return LikeC4ProjectJsonConfigSchema.parse(config) as unknown as LikeC4ProjectJsonConfig
226
+ }