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

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.
@@ -3,8 +3,16 @@ import { z } from 'zod';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { defaultConfig } from "../../config/defaults.js";
6
- // Валидатор ответа визарда: zod safeParse → boolean (true = валидно) для inquirer.
7
- const validate = (schema) => (answer) => schema.safeParse(answer).success;
6
+ // Непустая строка: в zod `.trim()` — трансформ, а не проверка, поэтому пустую строку
7
+ // и пробелы отсекает явный `.min(1)`.
8
+ const nonEmptyString = z.string().trim().min(1);
9
+ // Валидатор ответа визарда: true при успешном safeParse, иначе текст ошибки для inquirer.
10
+ // Пустой Enter уже подставил дефолт (answer = value || default) — сообщение получают
11
+ // только явно введённые пробелы/пустая строка при отсутствии дефолта.
12
+ const validate = (schema, message = 'значение обязательно') => (answer) => schema.safeParse(answer).success || message;
13
+ // Пункт default-списка чекбокса inquirer: ключ отмечен, если он включён в текущем
14
+ // конфиге, а при незаданном значении — если включён по умолчанию.
15
+ const checkedKey = (current, dflt, key) => (current === undefined ? dflt : current) ? key : null;
8
16
  export default async (currentConfiguration, conf, program) => {
9
17
  // ESM исполняется в strict mode: под CommonJS `responses` был неявным глобалом,
10
18
  // здесь объявляем его явно (иначе присваивание бросит ReferenceError).
@@ -15,7 +23,7 @@ export default async (currentConfiguration, conf, program) => {
15
23
  name: 'projectName',
16
24
  message: 'Project Name',
17
25
  default: currentConfiguration.PROJECT_NAME || path.parse(process.cwd()).name,
18
- validate: validate(z.string().trim().optional())
26
+ validate: validate(nonEmptyString, 'укажите имя проекта')
19
27
  });
20
28
  conf.set('projectName', responses.projectName);
21
29
  }
@@ -25,7 +33,7 @@ export default async (currentConfiguration, conf, program) => {
25
33
  name: 'homepageName',
26
34
  message: 'HomePage Name',
27
35
  default: currentConfiguration.HOMEPAGE_NAME || defaultConfig.homepageName,
28
- validate: validate(z.string().trim().optional())
36
+ validate: validate(nonEmptyString, 'укажите имя главной страницы')
29
37
  });
30
38
  conf.set('homepageName', responses.homepageName);
31
39
  }
@@ -35,17 +43,21 @@ export default async (currentConfiguration, conf, program) => {
35
43
  name: 'rootFolder',
36
44
  message: 'Root documentation folder',
37
45
  default: currentConfiguration.ROOT_FOLDER || defaultConfig.rootFolder,
38
- validate: (answers) => {
39
- const isValid = validate(z.string().trim().optional())(answers);
40
- if (isValid) {
41
- if (answers.indexOf('/') !== -1 || answers.indexOf('\\') !== -1)
42
- return false;
43
- //check it's an actual folder
44
- const isDirectory = fs.statSync(path.join(process.cwd(), answers)).isDirectory();
45
- if (isDirectory)
46
- return true;
46
+ validate: (answer) => {
47
+ if (!nonEmptyString.safeParse(answer).success)
48
+ return 'укажите папку с документацией';
49
+ if (answer.indexOf('/') !== -1 || answer.indexOf('\\') !== -1)
50
+ return 'имя папки не должно содержать «/» или «\\»';
51
+ // statSync бросает ENOENT для несуществующего пути — ловим и превращаем в
52
+ // сообщение, иначе async-хендлер inquirer уронит процесс сырым стектрейсом.
53
+ try {
54
+ if (!fs.statSync(path.join(process.cwd(), answer)).isDirectory())
55
+ return 'указанный путь не является каталогом';
47
56
  }
48
- return false;
57
+ catch {
58
+ return 'папка не найдена';
59
+ }
60
+ return true;
49
61
  }
50
62
  });
51
63
  conf.set('rootFolder', responses.rootFolder);
@@ -74,27 +86,9 @@ export default async (currentConfiguration, conf, program) => {
74
86
  currentConfiguration.GENERATE_WEBSITE === undefined ||
75
87
  program.config) {
76
88
  const defaults = [
77
- currentConfiguration.GENERATE_MD === undefined
78
- ? defaultConfig.generateMD
79
- ? 'generateMD'
80
- : null
81
- : currentConfiguration.GENERATE_MD
82
- ? 'generateMD'
83
- : null,
84
- currentConfiguration.GENERATE_COMPLETE_MD_FILE === undefined
85
- ? defaultConfig.generateCompleteMD
86
- ? 'generateCompleteMD'
87
- : null
88
- : currentConfiguration.GENERATE_COMPLETE_MD_FILE
89
- ? 'generateCompleteMD'
90
- : null,
91
- currentConfiguration.GENERATE_WEBSITE === undefined
92
- ? defaultConfig.generateWEB
93
- ? 'generateWEB'
94
- : null
95
- : currentConfiguration.GENERATE_WEBSITE
96
- ? 'generateWEB'
97
- : null
89
+ checkedKey(currentConfiguration.GENERATE_MD, defaultConfig.generateMD, 'generateMD'),
90
+ checkedKey(currentConfiguration.GENERATE_COMPLETE_MD_FILE, defaultConfig.generateCompleteMD, 'generateCompleteMD'),
91
+ checkedKey(currentConfiguration.GENERATE_WEBSITE, defaultConfig.generateWEB, 'generateWEB')
98
92
  ];
99
93
  responses = await inquirer.prompt({
100
94
  type: 'checkbox',
@@ -116,10 +110,10 @@ export default async (currentConfiguration, conf, program) => {
116
110
  }
117
111
  ]
118
112
  });
119
- conf.set('generateMD', !!responses.generate.find((x) => x === 'generateMD'));
120
- conf.set('generateCompleteMD', !!responses.generate.find((x) => x === 'generateCompleteMD'));
121
- conf.set('generateWEB', !!responses.generate.find((x) => x === 'generateWEB'));
122
- if (responses.generate.find((x) => x === 'generateMD')) {
113
+ conf.set('generateMD', responses.generate.includes('generateMD'));
114
+ conf.set('generateCompleteMD', responses.generate.includes('generateCompleteMD'));
115
+ conf.set('generateWEB', responses.generate.includes('generateWEB'));
116
+ if (responses.generate.includes('generateMD')) {
123
117
  let mdOptions = await inquirer.prompt({
124
118
  type: 'confirm',
125
119
  name: 'includeNavigation',
@@ -139,7 +133,7 @@ export default async (currentConfiguration, conf, program) => {
139
133
  });
140
134
  conf.set('includeTableOfContents', mdOptions.includeTableOfContents);
141
135
  }
142
- if (responses.generate.find((x) => x === 'generateWEB')) {
136
+ if (responses.generate.includes('generateWEB')) {
143
137
  let webOptions = await inquirer.prompt({
144
138
  type: 'input',
145
139
  name: 'webTheme',
@@ -148,7 +142,9 @@ export default async (currentConfiguration, conf, program) => {
148
142
  });
149
143
  conf.set('webTheme', webOptions.webTheme);
150
144
  webOptions = await inquirer.prompt({
151
- type: 'input',
145
+ // confirm возвращает boolean — схема требует строгий z.boolean();
146
+ // input сохранял бы строку ('y') и валил последующий safeParse конфига.
147
+ type: 'confirm',
152
148
  name: 'supportSearch',
153
149
  message: 'Support search on navbar?',
154
150
  default: defaultConfig.supportSearch
@@ -195,48 +191,12 @@ export default async (currentConfiguration, conf, program) => {
195
191
  currentConfiguration.EXCLUDE_OTHER_FILES === undefined ||
196
192
  program.config) {
197
193
  const defaults = [
198
- currentConfiguration.INCLUDE_BREADCRUMBS === undefined
199
- ? defaultConfig.includeBreadcrumbs
200
- ? 'includeBreadcrumbs'
201
- : null
202
- : currentConfiguration.INCLUDE_BREADCRUMBS
203
- ? 'includeBreadcrumbs'
204
- : null,
205
- currentConfiguration.GENERATE_LOCAL_IMAGES === undefined
206
- ? defaultConfig.generateLocalImages
207
- ? 'generateLocalImages'
208
- : null
209
- : currentConfiguration.GENERATE_LOCAL_IMAGES
210
- ? 'generateLocalImages'
211
- : null,
212
- currentConfiguration.INCLUDE_LINK_TO_DIAGRAM === undefined
213
- ? defaultConfig.includeLinkToDiagram
214
- ? 'includeLinkToDiagram'
215
- : null
216
- : currentConfiguration.INCLUDE_LINK_TO_DIAGRAM
217
- ? 'includeLinkToDiagram'
218
- : null,
219
- currentConfiguration.DIAGRAMS_ON_TOP === undefined
220
- ? defaultConfig.diagramsOnTop
221
- ? 'diagramsOnTop'
222
- : null
223
- : currentConfiguration.DIAGRAMS_ON_TOP
224
- ? 'diagramsOnTop'
225
- : null,
226
- currentConfiguration.EMBED_DIAGRAM === undefined
227
- ? defaultConfig.embedDiagram
228
- ? 'embedDiagram'
229
- : null
230
- : currentConfiguration.EMBED_DIAGRAM
231
- ? 'embedDiagram'
232
- : null,
233
- currentConfiguration.EXCLUDE_OTHER_FILES === undefined
234
- ? defaultConfig.excludeOtherFiles
235
- ? 'excludeOtherFiles'
236
- : null
237
- : currentConfiguration.EXCLUDE_OTHER_FILES
238
- ? 'excludeOtherFiles'
239
- : null
194
+ checkedKey(currentConfiguration.INCLUDE_BREADCRUMBS, defaultConfig.includeBreadcrumbs, 'includeBreadcrumbs'),
195
+ checkedKey(currentConfiguration.GENERATE_LOCAL_IMAGES, defaultConfig.generateLocalImages, 'generateLocalImages'),
196
+ checkedKey(currentConfiguration.INCLUDE_LINK_TO_DIAGRAM, defaultConfig.includeLinkToDiagram, 'includeLinkToDiagram'),
197
+ checkedKey(currentConfiguration.DIAGRAMS_ON_TOP, defaultConfig.diagramsOnTop, 'diagramsOnTop'),
198
+ checkedKey(currentConfiguration.EMBED_DIAGRAM, defaultConfig.embedDiagram, 'embedDiagram'),
199
+ checkedKey(currentConfiguration.EXCLUDE_OTHER_FILES, defaultConfig.excludeOtherFiles, 'excludeOtherFiles')
240
200
  ];
241
201
  const choices = [
242
202
  {
@@ -271,12 +231,12 @@ export default async (currentConfiguration, conf, program) => {
271
231
  default: defaults,
272
232
  choices: choices
273
233
  });
274
- conf.set('includeBreadcrumbs', !!responses.generate.find((x) => x === 'includeBreadcrumbs'));
275
- conf.set('includeLinkToDiagram', !!responses.generate.find((x) => x === 'includeLinkToDiagram'));
276
- conf.set('diagramsOnTop', !!responses.generate.find((x) => x === 'diagramsOnTop'));
277
- conf.set('embedDiagram', !!responses.generate.find((x) => x === 'embedDiagram'));
278
- conf.set('excludeOtherFiles', !!responses.generate.find((x) => x === 'excludeOtherFiles'));
279
- conf.set('generateLocalImages', !!responses.generate.find((x) => x === 'generateLocalImages'));
234
+ conf.set('includeBreadcrumbs', responses.generate.includes('includeBreadcrumbs'));
235
+ conf.set('includeLinkToDiagram', responses.generate.includes('includeLinkToDiagram'));
236
+ conf.set('diagramsOnTop', responses.generate.includes('diagramsOnTop'));
237
+ conf.set('embedDiagram', responses.generate.includes('embedDiagram'));
238
+ conf.set('excludeOtherFiles', responses.generate.includes('excludeOtherFiles'));
239
+ conf.set('generateLocalImages', responses.generate.includes('generateLocalImages'));
280
240
  }
281
241
  if (!currentConfiguration.PLANTUML_SERVER_URL || program.config) {
282
242
  responses = await inquirer.prompt({
@@ -284,7 +244,7 @@ export default async (currentConfiguration, conf, program) => {
284
244
  name: 'plantumlServerUrl',
285
245
  message: 'PlantUML Server URL',
286
246
  default: currentConfiguration.PLANTUML_SERVER_URL || defaultConfig.plantumlServerUrl,
287
- validate: validate(z.string().trim().optional())
247
+ validate: validate(nonEmptyString, 'укажите URL сервера PlantUML')
288
248
  });
289
249
  conf.set('plantumlServerUrl', responses.plantumlServerUrl);
290
250
  }
@@ -294,7 +254,7 @@ export default async (currentConfiguration, conf, program) => {
294
254
  name: 'diagramFormat',
295
255
  message: 'Diagram Image Format',
296
256
  default: currentConfiguration.DIAGRAM_FORMAT || defaultConfig.diagramFormat,
297
- validate: validate(z.string().trim().optional())
257
+ validate: validate(nonEmptyString, 'укажите формат изображения диаграмм')
298
258
  });
299
259
  conf.set('diagramFormat', responses.diagramFormat);
300
260
  }
@@ -22,12 +22,14 @@ 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
- charset: 'UTF-8'
31
+ d2Layout: 'dagre',
32
+ charset: 'UTF-8',
33
+ useSystemFonts: false
32
34
  };
33
35
  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,66 @@
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),
56
+ useSystemFonts: bool(defaultConfig.useSystemFonts),
37
57
  // Задаются явно (флаг/промпт) либо тонкой правкой файла — в 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(),
58
+ projectName: optStr(),
59
+ webFileName: optStr(),
60
+ // Не-строки внутри массива молча отбрасываются (совместимость с master: null среди
61
+ // путей не валит сборку) — runtime дальше работает с чистым string[].
62
+ excludeSidebarFolderByPath: nullish(z.preprocess((v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : v), z.array(z.string()).optional())),
63
+ hasRun: optBool(),
43
64
  // Легаси-ключи старых `.c4builder` — читаются лишь для однократного предупреждения.
44
- plantumlVersion: z.coerce.string().optional(),
45
- generatePDF: z.boolean().optional(),
46
- generateCompletePDF: z.boolean().optional(),
65
+ plantumlVersion: optStr(),
66
+ generatePDF: optBool(),
67
+ generateCompletePDF: optBool(),
47
68
  checksums: z.unknown().optional()
48
69
  });
@@ -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>`;