@alfacapital-tech/c4builder 0.3.0-rc.1 → 0.3.0-rc.2

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.
@@ -22,12 +22,13 @@ const defaultConfig = {
22
22
  webPort: '3000',
23
23
  includeBreadcrumbs: true,
24
24
  includeLinkToDiagram: false,
25
- diagramsOnTop: false,
25
+ diagramsOnTop: true,
26
26
  embedDiagram: false,
27
27
  excludeOtherFiles: false,
28
28
  generateLocalImages: true,
29
29
  plantumlServerUrl: 'https://www.plantuml.com/plantuml',
30
30
  diagramFormat: 'svg',
31
+ d2Layout: 'dagre',
31
32
  charset: 'UTF-8'
32
33
  };
33
34
  export { defaultConfig };
@@ -1,4 +1,32 @@
1
1
  // Опорные типы конфигурации c4builder. Объявлены здесь (первой группой порта),
2
2
  // потому что результат getOptions() (BuildOptions) потребляет build() — он
3
3
  // портируется раньше, чем cli/dispatch, где getOptions() физически живёт.
4
- export {};
4
+ // Форма файла `.c4builder` (camelCase) — единый тип задаёт zod-схема (config/schema),
5
+ // её же валидацией опирается загрузка опций сборки. Ре-экспорт: потребители типа
6
+ // (defaults, dispatch) продолжают импортировать его отсюда.
7
+ import { configSchema } from "./schema.js";
8
+ /**
9
+ * Разбирает сырой объект `.c4builder`, разделяя строгий и щадящий пути. Сам НЕ печатает
10
+ * и НЕ завершает процесс — решение (ошибка+выход либо предупреждение) принимает CLI.
11
+ * Битые ключи определяются по первому сегменту `issue.path`; `salvaged` получается
12
+ * повторным разбором очищенного от них конфига — без дублирования схемы.
13
+ */
14
+ export function parseConfig(raw) {
15
+ const parsed = configSchema.safeParse(raw);
16
+ if (parsed.success)
17
+ return { ok: true, value: parsed.data };
18
+ const issues = [];
19
+ const brokenKeys = new Set();
20
+ for (const issue of parsed.error.issues) {
21
+ const key = issue.path.length ? String(issue.path[0]) : '(корень)';
22
+ if (brokenKeys.has(key))
23
+ continue; // один ключ — одно сообщение (напр. массив с N битых элементов)
24
+ brokenKeys.add(key);
25
+ issues.push({ key, value: raw[key], message: issue.message });
26
+ }
27
+ const cleaned = Object.fromEntries(Object.entries(raw).filter(([k]) => !brokenKeys.has(k)));
28
+ const salvagedParse = configSchema.safeParse(cleaned);
29
+ // cleaned свободен от всех проблемных ключей → валиден; фолбэк на пустой конфиг — страховка.
30
+ const salvaged = salvagedParse.success ? salvagedParse.data : configSchema.parse({});
31
+ return { ok: false, issues, salvaged };
32
+ }
@@ -4,45 +4,65 @@
4
4
  // валидируется `safeParse` — неверный тип значения отклоняется понятной ошибкой с
5
5
  // путём ключа, отсутствующие поля дополняются дефолтами.
6
6
  //
7
- // Строки коэрсятся (`z.coerce.string`: число `webPort` из рукописного конфига → строка),
7
+ // Числа в строковых полях коэрсятся (`webPort: 8000` из рукописного конфига → строка),
8
8
  // булевы поля строгие (`z.boolean`: строка вместо булева — ошибка, а не тихое truthy).
9
9
  import { z } from 'zod';
10
10
  import { defaultConfig } from "./defaults.js";
11
+ // В JSON-конфиге `null` означает «ключ не задан» (а не строку "null"): гоним его в
12
+ // `undefined` до внутренней схемы, чтобы срабатывал `.default`/`.optional`, а не строгий тип.
13
+ const nullish = (s) => z.preprocess((v) => (v === null ? undefined : v), s);
14
+ // Строковое поле: null И false = «не задано» → дефолт (false — легаси-идиома отключения,
15
+ // напр. `docsifyTemplate: false` в конфигах 0.2.x); число коэрсится в строку. Прочие типы
16
+ // (true, объекты) — понятная ошибка, а не тихая строка 'true' (z.coerce.string превращал
17
+ // false в 'false', и сборка падала на require('<cwd>/false')).
18
+ const strPre = (v) => v === null || v === false ? undefined : typeof v === 'number' ? String(v) : v;
19
+ const str = (def) => z.preprocess(strPre, z.string().default(def));
20
+ const optStr = () => z.preprocess(strPre, z.string().optional());
21
+ const bool = (def) => nullish(z.boolean().default(def));
22
+ const optBool = () => nullish(z.boolean().optional());
23
+ // TCP-порт: нечисловая строка ('30OO') иначе доехала бы до app.listen(), который
24
+ // трактует её как имя unix-сокета. Экспортируется для проверки CLI-флага -p.
25
+ export const isValidPort = (s) => /^\d+$/.test(s) && Number(s) >= 1 && Number(s) <= 65535;
11
26
  export const configSchema = z.object({
12
27
  // Базовые поля: опциональны в файле, но с дефолтом из defaultConfig — частичный
13
28
  // или пустой конфиг остаётся валидным (недостающее берёт значение по умолчанию).
14
- homepageName: z.coerce.string().default(defaultConfig.homepageName),
15
- rootFolder: z.coerce.string().default(defaultConfig.rootFolder),
16
- distFolder: z.coerce.string().default(defaultConfig.distFolder),
17
- generateMD: z.boolean().default(defaultConfig.generateMD),
18
- generateCompleteMD: z.boolean().default(defaultConfig.generateCompleteMD),
19
- generateWEB: z.boolean().default(defaultConfig.generateWEB),
20
- includeNavigation: z.boolean().default(defaultConfig.includeNavigation),
21
- includeTableOfContents: z.boolean().default(defaultConfig.includeTableOfContents),
22
- webTheme: z.coerce.string().default(defaultConfig.webTheme),
23
- supportSearch: z.boolean().default(defaultConfig.supportSearch),
24
- repoUrl: z.coerce.string().default(defaultConfig.repoUrl),
25
- executeScript: z.boolean().default(defaultConfig.executeScript),
26
- docsifyTemplate: z.coerce.string().default(defaultConfig.docsifyTemplate),
27
- webPort: z.coerce.string().default(defaultConfig.webPort),
28
- includeBreadcrumbs: z.boolean().default(defaultConfig.includeBreadcrumbs),
29
- includeLinkToDiagram: z.boolean().default(defaultConfig.includeLinkToDiagram),
30
- diagramsOnTop: z.boolean().default(defaultConfig.diagramsOnTop),
31
- embedDiagram: z.boolean().default(defaultConfig.embedDiagram),
32
- excludeOtherFiles: z.boolean().default(defaultConfig.excludeOtherFiles),
33
- generateLocalImages: z.boolean().default(defaultConfig.generateLocalImages),
34
- plantumlServerUrl: z.coerce.string().default(defaultConfig.plantumlServerUrl),
35
- diagramFormat: z.coerce.string().default(defaultConfig.diagramFormat),
36
- charset: z.coerce.string().default(defaultConfig.charset),
29
+ homepageName: str(defaultConfig.homepageName),
30
+ rootFolder: str(defaultConfig.rootFolder),
31
+ distFolder: str(defaultConfig.distFolder),
32
+ generateMD: bool(defaultConfig.generateMD),
33
+ generateCompleteMD: bool(defaultConfig.generateCompleteMD),
34
+ generateWEB: bool(defaultConfig.generateWEB),
35
+ includeNavigation: bool(defaultConfig.includeNavigation),
36
+ includeTableOfContents: bool(defaultConfig.includeTableOfContents),
37
+ webTheme: str(defaultConfig.webTheme),
38
+ supportSearch: bool(defaultConfig.supportSearch),
39
+ repoUrl: str(defaultConfig.repoUrl),
40
+ executeScript: bool(defaultConfig.executeScript),
41
+ docsifyTemplate: str(defaultConfig.docsifyTemplate),
42
+ webPort: z.preprocess(strPre, z
43
+ .string()
44
+ .refine(isValidPort, 'ожидается TCP-порт: целое число 1..65535')
45
+ .default(defaultConfig.webPort)),
46
+ includeBreadcrumbs: bool(defaultConfig.includeBreadcrumbs),
47
+ includeLinkToDiagram: bool(defaultConfig.includeLinkToDiagram),
48
+ diagramsOnTop: bool(defaultConfig.diagramsOnTop),
49
+ embedDiagram: bool(defaultConfig.embedDiagram),
50
+ excludeOtherFiles: bool(defaultConfig.excludeOtherFiles),
51
+ generateLocalImages: bool(defaultConfig.generateLocalImages),
52
+ plantumlServerUrl: str(defaultConfig.plantumlServerUrl),
53
+ diagramFormat: str(defaultConfig.diagramFormat),
54
+ d2Layout: str(defaultConfig.d2Layout),
55
+ charset: str(defaultConfig.charset),
37
56
  // Задаются явно (флаг/промпт) либо тонкой правкой файла — в defaultConfig не входят.
38
- projectName: z.coerce.string().optional(),
39
- d2Layout: z.coerce.string().optional(),
40
- webFileName: z.coerce.string().optional(),
41
- excludeSidebarFolderByPath: z.array(z.string()).optional(),
42
- hasRun: z.boolean().optional(),
57
+ projectName: optStr(),
58
+ webFileName: optStr(),
59
+ // Не-строки внутри массива молча отбрасываются (совместимость с master: null среди
60
+ // путей не валит сборку) — runtime дальше работает с чистым string[].
61
+ excludeSidebarFolderByPath: nullish(z.preprocess((v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : v), z.array(z.string()).optional())),
62
+ hasRun: optBool(),
43
63
  // Легаси-ключи старых `.c4builder` — читаются лишь для однократного предупреждения.
44
- plantumlVersion: z.coerce.string().optional(),
45
- generatePDF: z.boolean().optional(),
46
- generateCompletePDF: z.boolean().optional(),
64
+ plantumlVersion: optStr(),
65
+ generatePDF: optBool(),
66
+ generateCompletePDF: optBool(),
47
67
  checksums: z.unknown().optional()
48
68
  });
@@ -1,13 +1,14 @@
1
1
  import chalk from 'chalk';
2
2
  import path from 'node:path';
3
3
  import fsextra from 'fs-extra';
4
- import { makeDirectory } from "../util/utils.js";
5
- // teardownD2: освобождение webworker D2 в конце сборки.
6
- import { teardownD2 } from "./render/d2renderer.js";
4
+ import { makeDirectory, writeOnSameLine } from "../util/utils.js";
5
+ // clearD2FileCache: сброс кеша графа D2-импортов на границах сборки (сам webworker
6
+ // D2 гасит CLI после одиночной сборки — в watch-режиме он переживает ребилды).
7
+ import { clearD2FileCache } from "./render/d2renderer.js";
7
8
  // Фазы сборки: scan (дерево исходников) → render (диаграммы) → compose (markdown/сайт).
8
- import { generateTree } from "./scan/tree.js";
9
+ import { generateTree, engineSupportsRemote, clearIncludeCache } from "./scan/tree.js";
9
10
  import { generateImages } from "./render/diagrams.js";
10
- import { generateMD, generateWebMD, generateCompleteMD } from "./compose/markdown.js";
11
+ import { generateMD, generateWebMD, generateCompleteMD, clearDiagramCache } from "./compose/markdown.js";
11
12
  // Оркестратор владеет lifecycle выходного каталога: dist бэкапится в dist_bk, откуда
12
13
  // generateImages восстанавливает неизменённые по чексумме картинки; в конце бэкап сносится.
13
14
  const DIST_BACKUP_FOLDER_SUFFIX = '_bk';
@@ -26,50 +27,75 @@ const build = async (options, cacheConf) => {
26
27
  await fsextra.emptyDir(options.DIST_FOLDER);
27
28
  }
28
29
  await makeDirectory(path.join(options.DIST_FOLDER));
29
- //actual build
30
- console.log(chalk.green(`\nbuilding documentation in ./${options.DIST_FOLDER}`));
31
- const tree = await generateTree(options.ROOT_FOLDER, options);
32
- console.log(chalk.blue(`parsed ${tree.length} folders`));
33
- // У D2 нет онлайн-сервера рендера (в отличие от PlantUML): без локальной
34
- // генерации .d2 не во что превратить — ссылки на SVG вели бы в никуда.
35
- // Падаем сразу с понятной ошибкой, а не молча битым выводом.
36
- if (!options.GENERATE_LOCAL_IMAGES && tree.some((item) => item.diagrams.some((d) => d.engine === 'd2'))) {
37
- throw new Error('В проекте есть .d2-диаграммы, но generateLocalImages выключен. У D2 нет ' +
38
- 'онлайн-сервера рендера — включите локальную генерацию изображений (generateLocalImages).');
39
- }
40
- if (options.GENERATE_LOCAL_IMAGES) {
41
- console.log(chalk.blue('generating images'));
42
- await generateImages(tree, options, (count, total) => {
43
- process.stdout.write(`processed ${count}/${total} images\r`);
44
- }, cacheConf, bkFolderName);
45
- console.log('');
46
- }
47
- if (options.GENERATE_MD) {
48
- console.log(chalk.blue('generating markdown files'));
49
- await generateMD(tree, options, (count, total) => {
50
- process.stdout.write(`processed ${count}/${total} files\r`);
51
- });
52
- console.log('');
53
- }
54
- if (options.GENERATE_WEBSITE) {
55
- console.log(chalk.blue('generating docsify site'));
56
- await generateWebMD(tree, options);
57
- }
58
- if (options.GENERATE_COMPLETE_MD_FILE) {
59
- console.log(chalk.blue('generating complete markdown file'));
60
- await generateCompleteMD(tree, options);
30
+ // Сбросить пофайловые кеши прошлой сборки (актуально для watch-режима): base64
31
+ // диаграмм, граф D2-импортов, контент include-файлов PlantUML.
32
+ clearDiagramCache();
33
+ clearD2FileCache();
34
+ clearIncludeCache();
35
+ let ok = false;
36
+ try {
37
+ //actual build
38
+ console.log(chalk.green(`\nbuilding documentation in ./${options.DIST_FOLDER}`));
39
+ const tree = await generateTree(options.ROOT_FOLDER, options);
40
+ console.log(chalk.blue(`parsed ${tree.length} folders`));
41
+ // У движков без онлайн-рендера (D2, см. remoteRender в DIAGRAM_ENGINES) при
42
+ // выключенной локальной генерации диаграммы не во что превратить — ссылки на
43
+ // SVG вели бы в никуда. Падаем сразу с понятной ошибкой, а не молча битым выводом.
44
+ if (!options.GENERATE_LOCAL_IMAGES) {
45
+ const offline = new Set(tree.flatMap((item) => item.diagrams.filter((d) => !engineSupportsRemote(d.engine)).map((d) => d.engine)));
46
+ if (offline.size) {
47
+ throw new Error(`В проекте есть диаграммы без онлайн-сервера рендера (${[...offline].join(', ')}), ` +
48
+ 'но generateLocalImages выключен — включите локальную генерацию изображений ' +
49
+ '(generateLocalImages).');
50
+ }
51
+ }
52
+ if (options.GENERATE_LOCAL_IMAGES) {
53
+ console.log(chalk.blue('generating images'));
54
+ await generateImages(tree, options, (count, total) => {
55
+ writeOnSameLine(`processed ${count}/${total} images`);
56
+ }, cacheConf, bkFolderName);
57
+ console.log('');
58
+ }
59
+ if (options.GENERATE_MD) {
60
+ console.log(chalk.blue('generating markdown files'));
61
+ await generateMD(tree, options, (count, total) => {
62
+ writeOnSameLine(`processed ${count}/${total} files`);
63
+ });
64
+ console.log('');
65
+ }
66
+ if (options.GENERATE_WEBSITE) {
67
+ console.log(chalk.blue('generating docsify site'));
68
+ await generateWebMD(tree, options);
69
+ }
70
+ if (options.GENERATE_COMPLETE_MD_FILE) {
71
+ console.log(chalk.blue('generating complete markdown file'));
72
+ await generateCompleteMD(tree, options);
73
+ }
74
+ // PDF-вывод удалён. Легаси-конфиг с truthy generatePDF/generateCompletePDF не
75
+ // роняем: печатаем предупреждение с реально присутствующими ключами и собираем
76
+ // остальные выходы (exit 0). Конфиг НЕ мутируем — только подсказка пользователю.
77
+ if (options.LEGACY_PDF_KEYS?.length) {
78
+ console.log(chalk.bold(chalk.yellow('WARNING:')));
79
+ console.log(chalk.yellow(`PDF-вывод больше не поддерживается. Удалите вручную из .c4builder: ${options.LEGACY_PDF_KEYS.join(', ')}.`));
80
+ }
81
+ console.log(chalk.green(`built in ${(Date.now() - start_date.getTime()) / 1000} seconds`));
82
+ ok = true;
61
83
  }
62
- // PDF-вывод удалён. Легаси-конфиг с truthy generatePDF/generateCompletePDF не
63
- // роняем: печатаем предупреждение с реально присутствующими ключами и собираем
64
- // остальные выходы (exit 0). Конфиг НЕ мутируем — только подсказка пользователю.
65
- if (options.LEGACY_PDF_KEYS?.length) {
66
- console.log(chalk.bold(chalk.yellow('WARNING:')));
67
- console.log(chalk.yellow(`PDF-вывод больше не поддерживается. Удалите вручную из .c4builder: ${options.LEGACY_PDF_KEYS.join(', ')}.`));
84
+ finally {
85
+ // Бэкап dist сносим ТОЛЬКО при успехе: при падении фазы dist_bk — единственная
86
+ // полная копия прошлого удачного билда, её нельзя терять (иначе первый же
87
+ // сломавшийся рендер уничтожает рабочий вывод).
88
+ if (ok) {
89
+ await fsextra.removeSync(bkFolderName);
90
+ }
91
+ else if (options.GENERATE_LOCAL_IMAGES && fsextra.existsSync(bkFolderName)) {
92
+ console.log(chalk.yellow(`\nсборка прервана — бэкап предыдущего билда сохранён в ./${bkFolderName}`));
93
+ }
94
+ // Пофайловые кеши нужны только внутри сборки — чистим и на выходе, чтобы
95
+ // watch-процесс не держал base64 всех диаграмм в памяти между ребилдами.
96
+ clearDiagramCache();
97
+ clearD2FileCache();
98
+ clearIncludeCache();
68
99
  }
69
- // Remove image backup folder
70
- await fsextra.removeSync(bkFolderName);
71
- // Освободить D2-инстанс (webworker), иначе процесс не завершится. No-op, если .d2 не было.
72
- await teardownD2();
73
- console.log(chalk.green(`built in ${(Date.now() - start_date.getTime()) / 1000} seconds`));
74
100
  };
75
101
  export { build };
@@ -20,8 +20,8 @@ export default (options) => {
20
20
  <script src="vendor/docsify.min.js"></script>
21
21
  <script src="vendor/docsify-plantuml.min.js"></script>
22
22
  <script src="vendor/zoom-image.min.js"></script>
23
- ${!!options.supportSearch && `<script src="vendor/search.min.js"></script>`}
24
- ${!!options.executeScript && `<script src="vendor/swagger-ui-bundle.js"></script>`}
23
+ ${options.supportSearch ? `<script src="vendor/search.min.js"></script>` : ''}
24
+ ${options.executeScript ? `<script src="vendor/swagger-ui-bundle.js"></script>` : ''}
25
25
  </body>
26
26
 
27
27
  </html>`;