@alfacapital-tech/c4builder 0.3.0 → 0.4.0-rc.1
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 +40 -5
- package/dist/cli/commands/check.js +80 -0
- package/dist/cli/dispatch.js +5 -1
- package/dist/core/render/d2renderer.js +12 -1
- package/package.json +1 -1
package/README.MD
CHANGED
|
@@ -94,6 +94,29 @@ Options:
|
|
|
94
94
|
There is also a `jre` subcommand: `c4builder jre install [--force]` pre-downloads the
|
|
95
95
|
private JRE cache, `c4builder jre info` prints which Java would be used.
|
|
96
96
|
|
|
97
|
+
### Validating single diagrams
|
|
98
|
+
|
|
99
|
+
`c4builder check <file...>` validates individual `.puml` / `.iuml` / `.d2` files with the
|
|
100
|
+
same engines the build uses (bundled PlantUML jar + system/managed Java, bundled D2) —
|
|
101
|
+
no project or `.c4builder` needed, nothing is written. Handy for pre-commit hooks and CI:
|
|
102
|
+
exit code 0 when every file compiles, 1 otherwise; errors are printed as
|
|
103
|
+
`✗ file: line N: <engine message>`.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
c4builder check src/c4/context.puml lib/shared.iuml diagrams/flow.d2
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`!include` paths resolve relative to the checked file's directory; a `.iuml` library is
|
|
110
|
+
validated by including it into an empty diagram.
|
|
111
|
+
|
|
112
|
+
A minimal pre-commit hook (`.git/hooks/pre-commit`) that validates staged diagrams:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
#!/bin/sh
|
|
116
|
+
files=$(git diff --cached --name-only --diff-filter=ACM -- '*.puml' '*.iuml' '*.d2')
|
|
117
|
+
[ -z "$files" ] || exec c4builder check $files
|
|
118
|
+
```
|
|
119
|
+
|
|
97
120
|
By default, running `c4builder` will attempt to build the project.
|
|
98
121
|
|
|
99
122
|
If no configuration was set it will ask for it and then do the actual build. Running just `c4builder` after a configuration will no longer show the wizard.
|
|
@@ -305,19 +328,24 @@ Use the image directly in CI (GitHub Actions, GitLab CI) as the job image — th
|
|
|
305
328
|
Publishing is triggered by pushing a git tag `v<version>` — pushes/merges to `master` alone
|
|
306
329
|
do not publish.
|
|
307
330
|
|
|
308
|
-
|
|
331
|
+
Every user-facing PR adds a line to the `## Unreleased` section of the change log below;
|
|
332
|
+
the release commit renames it to `## vX.Y.Z`.
|
|
333
|
+
|
|
334
|
+
**Release candidate** (from `master`, as many as needed — consumers pinned to `rc`
|
|
335
|
+
pick each one up automatically):
|
|
309
336
|
|
|
310
337
|
```
|
|
311
|
-
npm version
|
|
338
|
+
npm version X.Y.Z-rc.1 -m "rc: %s" # first candidate
|
|
339
|
+
npm version prerelease -m "rc: %s" # next ones: rc.2, rc.3, …
|
|
312
340
|
git push --follow-tags
|
|
313
341
|
```
|
|
314
342
|
|
|
315
|
-
Publishes to dist-tag `rc
|
|
343
|
+
Publishes to npm dist-tag `rc` and docker tag `rc`; `latest` is untouched.
|
|
316
344
|
|
|
317
|
-
**Final release
|
|
345
|
+
**Final release** (rename `## Unreleased` → `## vX.Y.Z` first):
|
|
318
346
|
|
|
319
347
|
```
|
|
320
|
-
npm version
|
|
348
|
+
npm version X.Y.Z -m "release: %s"
|
|
321
349
|
git push --follow-tags
|
|
322
350
|
```
|
|
323
351
|
|
|
@@ -325,6 +353,13 @@ Publishes to dist-tag `latest`.
|
|
|
325
353
|
|
|
326
354
|
# Change log
|
|
327
355
|
|
|
356
|
+
## Unreleased
|
|
357
|
+
|
|
358
|
+
- `c4builder check <file...>` — validate individual `.puml` / `.iuml` / `.d2` files with the
|
|
359
|
+
bundled engines, without a project build (#13)
|
|
360
|
+
- D2 compile errors are printed as readable `file:line:col: message` lines instead of raw JSON
|
|
361
|
+
- Regression test for two adjacent `` embeds (#12 — the bug itself was fixed in 0.3.0)
|
|
362
|
+
|
|
328
363
|
## v0.3.0
|
|
329
364
|
|
|
330
365
|
Full TypeScript rewrite (ESM, Node.js ≥ 20.19) with the same CLI and `.c4builder` format.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { VENDOR_DIR } from "../../util/paths.js";
|
|
5
|
+
import { VENDORED_JAR } from "../../util/utils.js";
|
|
6
|
+
import { resolveJava } from "../../core/render/jre.js";
|
|
7
|
+
import { renderDiagram } from "../../core/render/diagrams.js";
|
|
8
|
+
import { renderD2, teardownD2 } from "../../core/render/d2renderer.js";
|
|
9
|
+
// `c4builder check <file...>` — валидация отдельных диаграмм ТЕМ ЖЕ движком, что и
|
|
10
|
+
// сборка (вендорный PlantUML jar + системная/managed java, бандл D2), без проекта и
|
|
11
|
+
// `.c4builder`. Рендер идёт в память и выбрасывается: проверяем ровно то, что упало бы
|
|
12
|
+
// в сборке (системный `plantuml` может отличаться версией). Код выхода 0 — все файлы ок,
|
|
13
|
+
// 1 — есть ошибка (движка, неизвестного расширения или чтения файла).
|
|
14
|
+
// stderr PlantUML в -pipe: `ERROR` / позиция / описание. Позиция — 0-based строка
|
|
15
|
+
// диаграммы, для файла с одной диаграммой это file:N+1.
|
|
16
|
+
// ponytail: в файле с несколькими @startuml позиция считается от начала блока — смещение
|
|
17
|
+
// не учитываем, добавить, если такие файлы станут проверять.
|
|
18
|
+
const PUML_ERROR = /^ERROR\n(\d+)\n(.*)$/m;
|
|
19
|
+
const JAR_PATH = path.join(VENDOR_DIR, VENDORED_JAR.jar);
|
|
20
|
+
// JRE резолвится лениво и один раз: список только из .d2 java не трогает.
|
|
21
|
+
let javaPromise = null;
|
|
22
|
+
const getJava = () => {
|
|
23
|
+
javaPromise ??= resolveJava({ log: (m) => console.log(chalk.gray(m)) }).then((r) => r.path);
|
|
24
|
+
return javaPromise;
|
|
25
|
+
};
|
|
26
|
+
const checkPlantUml = async (abs) => {
|
|
27
|
+
// .iuml — библиотека без @startuml: сам по себе PlantUML «рендерит» его без ошибок,
|
|
28
|
+
// поэтому проверяем через включение в пустую диаграмму.
|
|
29
|
+
const isLib = path.extname(abs) === '.iuml';
|
|
30
|
+
const content = isLib ? `@startuml\n!include ${abs}\n@enduml\n` : await readFile(abs);
|
|
31
|
+
const isDitaa = /@startditaa/i.test(content.toString());
|
|
32
|
+
try {
|
|
33
|
+
await renderDiagram(content, {
|
|
34
|
+
javaBin: await getJava(),
|
|
35
|
+
jarPath: JAR_PATH,
|
|
36
|
+
includePath: path.dirname(abs),
|
|
37
|
+
format: isDitaa ? 'png' : 'svg',
|
|
38
|
+
charset: 'UTF-8',
|
|
39
|
+
isDitaa,
|
|
40
|
+
useSystemFonts: false
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
const m = e.message.match(PUML_ERROR);
|
|
45
|
+
if (!m)
|
|
46
|
+
throw e;
|
|
47
|
+
// Для .iuml позиция относится к обёртке, а не к файлу — не показываем.
|
|
48
|
+
throw new Error(isLib ? m[2] : `строка ${Number(m[1]) + 1}: ${m[2]}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const CHECKERS = {
|
|
52
|
+
'.puml': checkPlantUml,
|
|
53
|
+
'.iuml': checkPlantUml,
|
|
54
|
+
'.d2': (abs) => renderD2(abs)
|
|
55
|
+
};
|
|
56
|
+
export default async (files) => {
|
|
57
|
+
if (files.length === 0) {
|
|
58
|
+
console.log(chalk.red('использование: c4builder check <file...> (.puml | .iuml | .d2)'));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
let failed = 0;
|
|
62
|
+
// ponytail: последовательно — на сотнях файлов добавить пул как в generateImages.
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const abs = path.resolve(file);
|
|
65
|
+
const check = CHECKERS[path.extname(abs).toLowerCase()];
|
|
66
|
+
try {
|
|
67
|
+
if (!check)
|
|
68
|
+
throw new Error('неизвестное расширение (ожидается .puml, .iuml или .d2)');
|
|
69
|
+
await check(abs);
|
|
70
|
+
console.log(chalk.green(`✓ ${file}`));
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
failed++;
|
|
74
|
+
console.error(chalk.red(`✗ ${file}: ${e.message}`));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
await teardownD2();
|
|
78
|
+
if (failed)
|
|
79
|
+
process.exit(1);
|
|
80
|
+
};
|
package/dist/cli/dispatch.js
CHANGED
|
@@ -6,6 +6,7 @@ import Configstore from 'configstore';
|
|
|
6
6
|
import cmdHelp from "./commands/help.js";
|
|
7
7
|
import cmdNewProject from "./commands/new.js";
|
|
8
8
|
import cmdJre from "./commands/jre.js";
|
|
9
|
+
import cmdCheck from "./commands/check.js";
|
|
9
10
|
import cmdList from "./commands/list.js";
|
|
10
11
|
import cmdSite from "./commands/site.js";
|
|
11
12
|
import cmdCollect from "./wizard/collect.js";
|
|
@@ -118,12 +119,15 @@ export default async () => {
|
|
|
118
119
|
throw new InvalidArgumentError('ожидается TCP-порт: целое число 1..65535');
|
|
119
120
|
return Number(v);
|
|
120
121
|
})
|
|
121
|
-
.allowExcessArguments() // позиционные аргументы
|
|
122
|
+
.allowExcessArguments() // позиционные аргументы подкоманд `jre <action>` и `check <file...>`
|
|
122
123
|
.parse(process.argv);
|
|
123
124
|
const opts = program.opts();
|
|
124
125
|
// Прогрев JRE — до загрузки конфига проекта и intro: команда самостоятельна.
|
|
125
126
|
if (program.args[0] === 'jre')
|
|
126
127
|
return cmdJre(program.args.slice(1), { force: opts.force });
|
|
128
|
+
// Проверка отдельных диаграмм — тоже без конфига проекта.
|
|
129
|
+
if (program.args[0] === 'check')
|
|
130
|
+
return cmdCheck(program.args.slice(1));
|
|
127
131
|
let conf = { get: () => { } };
|
|
128
132
|
let cacheConf = { get: () => { }, set: () => { }, clear: () => { } };
|
|
129
133
|
// Лок сборки — рядом с конфигом проекта (см. util/lock.ts). Заполняется вместе с conf.
|
|
@@ -159,6 +159,17 @@ const buildCompileRequest = (entryAbs) => {
|
|
|
159
159
|
return { fs: fsMap, inputPath: toKey(path.resolve(entryAbs)) };
|
|
160
160
|
};
|
|
161
161
|
// Рендер .d2 → SVG (Buffer). Ошибки компиляции пробрасываются с путём файла.
|
|
162
|
+
// Ошибка компиляции движок отдаёт JSON-массивом [{range, errmsg}] — вытаскиваем
|
|
163
|
+
// человекочитаемые errmsg (`file:line:col: текст`); иной формат отдаём как есть.
|
|
164
|
+
const d2ErrorText = (message) => {
|
|
165
|
+
try {
|
|
166
|
+
const parsed = JSON.parse(message);
|
|
167
|
+
if (Array.isArray(parsed) && parsed.every((x) => typeof x?.errmsg === 'string'))
|
|
168
|
+
return parsed.map((x) => x.errmsg).join('\n');
|
|
169
|
+
}
|
|
170
|
+
catch { }
|
|
171
|
+
return message;
|
|
172
|
+
};
|
|
162
173
|
const renderD2 = async (entryAbs, { layout = 'dagre' } = {}) => {
|
|
163
174
|
const d2 = await getD2();
|
|
164
175
|
const { fs: fsMap, inputPath } = buildCompileRequest(entryAbs);
|
|
@@ -168,7 +179,7 @@ const renderD2 = async (entryAbs, { layout = 'dagre' } = {}) => {
|
|
|
168
179
|
}
|
|
169
180
|
catch (err) {
|
|
170
181
|
const e = err;
|
|
171
|
-
throw new Error(`Ошибка компиляции D2 (${entryAbs}):\n${e.message || e}`);
|
|
182
|
+
throw new Error(`Ошибка компиляции D2 (${entryAbs}):\n${d2ErrorText(e.message || String(e))}`);
|
|
172
183
|
}
|
|
173
184
|
let svg;
|
|
174
185
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfacapital-tech/c4builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-rc.1",
|
|
4
4
|
"description": "A CLI tool designed to compile a folder structure of markdowns and plant uml files into a site, single file markdown or a collection of markdowns with links",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|