@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 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
- **Release candidate** (from `refactor/phoenix`, no merge to `master` needed):
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 0.3.0-rc.1 -m "rc: %s"
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 0.3.0 -m "release: %s"
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 `![..](x.puml)` 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
+ };
@@ -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() // позиционные аргументы подкоманды `jre <action>`
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.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",