@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 +21 -0
- package/README.md +206 -0
- package/dist/define-config.d.ts +45 -0
- package/dist/define-config.mjs +7 -0
- package/dist/filenames.d.ts +16 -0
- package/dist/filenames.mjs +34 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.mjs +11 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.mjs +2 -0
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.mjs +2 -0
- package/dist/node/load-config.d.ts +7 -0
- package/dist/node/load-config.mjs +29 -0
- package/dist/schema.d.ts +149 -0
- package/dist/schema.image-alias.d.ts +5 -0
- package/dist/schema.image-alias.mjs +42 -0
- package/dist/schema.mjs +38 -0
- package/package.json +84 -0
- package/schema.json +44 -0
- package/src/define-config.ts +51 -0
- package/src/filenames.ts +48 -0
- package/src/index.ts +21 -0
- package/src/logger.ts +3 -0
- package/src/node/index.ts +2 -0
- package/src/node/load-config.ts +36 -0
- package/src/schema.image-alias.ts +68 -0
- package/src/schema.ts +226 -0
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"></a>
|
|
4
|
+
<a href="https://www.npmjs.com/package/%40likec4%2Fconfig" target="_blank"></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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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";
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const logger: any;
|
package/dist/logger.mjs
ADDED
|
@@ -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
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/schema.mjs
ADDED
|
@@ -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
|
+
}
|
package/src/filenames.ts
ADDED
|
@@ -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,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
|
+
}
|