@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.
package/README.MD CHANGED
@@ -110,7 +110,7 @@ Following the C4 model, folders inside the **source** represent Systems/Containe
110
110
  class.1.md
111
111
  ```
112
112
 
113
- The **destination** folder is automatically generated by the **build** process and can have various formats. The folder can provide a drill down style view of the Systems/Containers/Components in different ways. Either by pushing it as a site to gitpages, using it as a navigable markdown directly in the repository or by generating pdfs and using the traditional mail.
113
+ The **destination** folder is automatically generated by the **build** process and can have various formats. The folder can provide a drill down style view of the Systems/Containers/Components in different ways. Either by pushing it as a site to gitpages or using it as a navigable markdown directly in the repository.
114
114
 
115
115
  ## The build
116
116
 
@@ -210,18 +210,6 @@ The generated markdown file makes for an easy single page readme for the reposit
210
210
 
211
211
  ![singlemd](docs/images/single-md.jpg)
212
212
 
213
- ### Collection of pdf files each describing one entity
214
-
215
- A fast way to pass around information regarding only one particular System/Container/Component
216
-
217
- ![pdf](docs/images/pdf.jpg)
218
-
219
- ### Single pdf file with a table of contents
220
-
221
- The easiest format to share the full documentation
222
-
223
- ![pdffull](docs/images/full-pdf.jpg)
224
-
225
213
  ### Docsify site with a sidebar
226
214
 
227
215
  After choosing to generate a docsify site, it can be viewed locally by running `c4builder site` and accessing `http://localhost:3000`;
@@ -232,7 +220,7 @@ The sidebar is generated manually so that it only contains the main entities def
232
220
 
233
221
  ![site](docs/images/site.jpg)
234
222
 
235
- By going to the github repository settings and enabling GitHub pages the site be pushed with ease and provide an overview to all people involved in the project without having to worry about which version of pdf file got sent when.
223
+ By going to the github repository settings and enabling GitHub pages the site be pushed with ease and provide an overview to all people involved in the project.
236
224
 
237
225
  ![github](docs/images/githubpages.gif)
238
226
 
@@ -256,10 +244,6 @@ By default this is disabled. If enabled, the output will no longer include the a
256
244
 
257
245
  The current entity structure is inserted after the header
258
246
 
259
- ### Custom pdf
260
-
261
- If defined, this css is used in the generation of the pdf files.
262
-
263
247
  # Future plans
264
248
 
265
249
  I would like to bind the **build** phase to the C4 model. It could parse the diagrams and know what Person/System/Component/Container is related to what and help with additional validation. Maybe even export to [structurizr](https://structurizr.com/) or vice versa.
@@ -300,6 +284,10 @@ Publishes to dist-tag `latest`.
300
284
 
301
285
  # Change log
302
286
 
287
+ ## v0.3.0
288
+
289
+ PDF output removed — use the docsify site or the single/collection markdown outputs instead (the `pdf`/`pdfCss` options no longer exist).
290
+
303
291
  ## v0.2.20
304
292
 
305
293
  Fork by [Alfa Capital Technologies](https://alfacapital.tech)
@@ -30,8 +30,6 @@ ${chalk.cyan('Generate website')}
30
30
  Uses docsify to generate a website with a sidebar for navigation. The site can be easily deployed to github pages.
31
31
  ${chalk.cyan('Website docsify theme')}
32
32
  Changes the default theme of the generated docsify website
33
- ${chalk.cyan('PlantUML version')}
34
- Uses the specific plantuml version jar file for generating images.
35
33
  ${chalk.cyan('Generate diagram images locally')}
36
34
  Uses the localy installed plantuml package to build svg images for each diagram.
37
35
  ${chalk.cyan('Replace diagrams with a link')}
@@ -1,13 +1,20 @@
1
1
  import chalk from 'chalk';
2
- import { resolveJava, detectSystemJava, TEMURIN_FEATURE } from "../../core/render/jre.js";
3
- // Прогрев кеша: `c4builder jre install [--force]`. Резолвит/скачивает JRE заранее
2
+ import { resolveJava, detectSystemJava, TEMURIN_FEATURE, JRE_CACHE_SCHEMA, jreCacheDir } from "../../core/render/jre.js";
3
+ // `c4builder jre install [--force]` — прогрев кеша: резолвит/скачивает JRE заранее
4
4
  // (CI/офлайн) без полной сборки. При годной системной java по умолчанию сообщает,
5
5
  // что скачивание не требуется; `--force` форсирует загрузку Temurin в кеш.
6
+ // `c4builder jre info` — машиночитаемые параметры managed-JRE (JSON): путь кеша и
7
+ // материал ключа для внешних кешей (actions/cache в CI) из единственного источника
8
+ // истины, вместо grep констант по исходникам и хардкода пути.
6
9
  export default async (args = [], { force = false } = {}) => {
7
10
  const action = args[0];
11
+ if (action === 'info') {
12
+ console.log(JSON.stringify({ feature: TEMURIN_FEATURE, schema: JRE_CACHE_SCHEMA, cacheDir: jreCacheDir() }));
13
+ return;
14
+ }
8
15
  if (action !== 'install') {
9
16
  console.log(chalk.red(`неизвестная подкоманда: c4builder jre ${action || ''}`.trim()));
10
- console.log(chalk.gray('доступно: c4builder jre install [--force]'));
17
+ console.log(chalk.gray('доступно: c4builder jre install [--force] | jre info'));
11
18
  process.exit(1);
12
19
  }
13
20
  try {
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- export default (currentConfiguration) => {
2
+ export default (currentConfiguration, issues = []) => {
3
3
  console.log(`
4
4
  CURRENT CONFIGURATION
5
5
 
@@ -74,5 +74,13 @@ Exclude other files: ${currentConfiguration.EXCLUDE_OTHER_FILES !== undefined
74
74
  ? chalk.green(currentConfiguration.EXCLUDE_OTHER_FILES)
75
75
  : chalk.red('not set')}
76
76
  `);
77
+ // Битые ключи показаны выше как «not set» (щадящий разбор их выбросил); помечаем их
78
+ // сырым значением и причиной, чтобы было видно, что и почему требует правки/визарда.
79
+ if (issues.length) {
80
+ console.log(chalk.yellow('⚠ Невалидные ключи в .c4builder (показаны как «not set», чините через `c4builder --config`):'));
81
+ for (const { key, value, message } of issues)
82
+ console.log(chalk.yellow(` ⚠ ${key}: ${JSON.stringify(value)} — ${message}`));
83
+ console.log('');
84
+ }
77
85
  return;
78
86
  };
@@ -15,37 +15,18 @@ const validateProjectName = (name) => {
15
15
  if (name.indexOf('/') !== -1 || name.indexOf('\\') !== -1)
16
16
  return 'имя проекта не должно содержать «/» или «\\»';
17
17
  const target = path.join(process.cwd(), name);
18
- if (fs.existsSync(target) && fs.readdirSync(target).length > 0)
19
- return `папка «${name}» уже существует и не пуста`;
18
+ if (fs.existsSync(target)) {
19
+ // readdirSync по файлу бросает ENOTDIR — сперва различаем файл и каталог.
20
+ if (!fs.statSync(target).isDirectory())
21
+ return `«${name}» уже существует и является файлом`;
22
+ if (fs.readdirSync(target).length > 0)
23
+ return `папка «${name}» уже существует и не пуста`;
24
+ }
20
25
  return null;
21
26
  };
22
- const generateTemplate = async (dir, projectName) => {
23
- const build = async (dir, _parent) => {
24
- const files = fs.readdirSync(dir);
25
- for (const file of files) {
26
- if (fs.statSync(path.join(dir, file)).isDirectory()) {
27
- await makeDirectory(path.join(process.cwd(), projectName, dir.replace(TEMPLATE_DIR, ''), file));
28
- await build(path.join(dir, file), dir);
29
- }
30
- }
31
- const mdFiles = files.filter((x) => path.extname(x).toLowerCase() === '.md');
32
- for (const mdFile of mdFiles) {
33
- await fsextra.copy(path.join(dir, mdFile), path.join(process.cwd(), projectName, dir.replace(TEMPLATE_DIR, ''), mdFile));
34
- }
35
- const pumlFiles = files.filter((x) => path.extname(x).toLowerCase() === '.puml');
36
- for (const pumlFile of pumlFiles) {
37
- const fileContents = await readFile(path.join(dir, pumlFile));
38
- await writeFile(path.join(process.cwd(), projectName, dir.replace(TEMPLATE_DIR, ''), pumlFile), fileContents);
39
- }
40
- const otherFiles = files.filter((x) => ['.md', '.puml'].indexOf(path.extname(x).toLowerCase()) === -1);
41
- for (const otherFile of otherFiles) {
42
- if (fs.statSync(path.join(dir, otherFile)).isDirectory())
43
- continue;
44
- await fsextra.copy(path.join(dir, otherFile), path.join(process.cwd(), projectName, dir.replace(TEMPLATE_DIR, ''), otherFile));
45
- }
46
- };
47
- await build(dir);
48
- };
27
+ // Копия шаблона в новый проект как есть (включая dot-файлы) — раньше это была ручная
28
+ // рекурсия на 40 строк, повторявшая fsextra.copy по типам файлов.
29
+ const generateTemplate = (dir, projectName) => fsextra.copy(dir, path.join(process.cwd(), projectName));
49
30
  export default async (opts = {}) => {
50
31
  const nonInteractive = !!opts.yes;
51
32
  // --- имя проекта: флаг --name пропускает промпт; --yes без имени — фатально ---
@@ -1,5 +1,5 @@
1
1
  import figlet from 'figlet';
2
- import { program } from 'commander';
2
+ import { InvalidArgumentError, program } from 'commander';
3
3
  import chalk from 'chalk';
4
4
  import path from 'node:path';
5
5
  import Configstore from 'configstore';
@@ -13,27 +13,50 @@ import { build } from "../core/build.js";
13
13
  import watch from 'node-watch';
14
14
  import { EventEmitter } from 'node:events';
15
15
  import { clearConsole } from "../util/utils.js";
16
+ import { acquireBuildLock, BuildLockHeldError } from "../util/lock.js";
17
+ // teardownD2 живёт здесь, а не в finally build(): в watch-режиме D2-воркер должен
18
+ // переживать ребилды (его пересоздание — секунды), гасим его только когда сборок
19
+ // больше не будет (одиночный запуск).
20
+ import { teardownD2 } from "../core/render/d2renderer.js";
16
21
  import { packageJson as pkg } from "../util/paths.js";
17
- import { configSchema } from "../config/schema.js";
22
+ import { parseConfig } from "../config/options.js";
23
+ import { isValidPort } from "../config/schema.js";
18
24
  const intro = () => {
19
25
  console.log(chalk.blue(figlet.textSync('c4builder')));
20
26
  console.log(chalk.gray('Blow up your software documentation writing skills'));
21
27
  };
22
- function getOptions(conf, applyDefaults) {
28
+ // Читает сырой `.c4builder` и разбирает его щадяще (config/parseConfig): для визарда и
29
+ // `--list`. Возвращает лишь ЗАДАННЫЕ пользователем и ВАЛИДНЫЕ ключи (недостающее и битое
30
+ // = undefined, как читалось до валидации), чтобы визард спросил их на первом запуске,
31
+ // а `--list` всё равно показал конфиг. Битые ключи — в `issues` для предупреждения.
32
+ function readLenientConfig(conf) {
23
33
  const raw = (conf.all ?? {});
24
- const parsed = configSchema.safeParse(raw);
25
- if (!parsed.success) {
26
- const issue = parsed.error.issues[0];
27
- const key = issue?.path.join('.') || '(корень)';
28
- console.error(chalk.red(`Ошибка в .c4builder: ключ «${key}» — ${issue?.message ?? 'неверное значение'}`));
29
- process.exit(1);
30
- }
31
- const full = parsed.data;
32
- const c = applyDefaults === false
33
- ? Object.fromEntries(Object.keys(full)
34
- .filter((k) => k in raw)
35
- .map((k) => [k, full[k]]))
36
- : full;
34
+ const result = parseConfig(raw);
35
+ const source = result.ok ? result.value : result.salvaged;
36
+ const broken = new Set(result.ok ? [] : result.issues.map((i) => i.key));
37
+ const config = Object.fromEntries(Object.keys(source)
38
+ // null в файле = «не задан» (как отсутствие ключа): схема уже подставила
39
+ // дефолт, но визард обязан спросить значение, а не молча принять его.
40
+ .filter((k) => k in raw && raw[k] !== null && !broken.has(k))
41
+ .map((k) => [k, source[k]]));
42
+ return { config, issues: result.ok ? [] : result.issues, salvaged: source };
43
+ }
44
+ // Строгий путь (сборка) не терпит битого конфига: печатает внятную ошибку и завершает
45
+ // процесс с ненулевым кодом. Держим решение о выходе здесь, в CLI, а не в config/*.
46
+ function failOnConfigIssues(issues) {
47
+ console.error(chalk.red('Ошибка в .c4builder — невалидные значения не дают собрать документацию:'));
48
+ for (const { key, message } of issues)
49
+ console.error(chalk.red(` ${key} — ${message}`));
50
+ process.exit(1);
51
+ }
52
+ // Щадящий путь (--config/первый запуск визарда) предупреждает о битых ключах: они
53
+ // трактуются как «не задано» и будут перезаписаны валидными значениями после визарда.
54
+ function warnConfigIssues(issues) {
55
+ console.log(chalk.yellow('\n⚠ Невалидные ключи в .c4builder (трактуются как «не задано», визард их перезапишет):'));
56
+ for (const { key, value, message } of issues)
57
+ console.log(chalk.yellow(` ⚠ ${key}: ${JSON.stringify(value)} — ${message}`));
58
+ }
59
+ function getOptions(c) {
37
60
  return {
38
61
  // Легаси-детект: выбор версии PlantUML удалён; ключ plantumlVersion из старых
39
62
  // .c4builder отдаём build.js только для однократного предупреждения на пине.
@@ -64,7 +87,7 @@ function getOptions(conf, applyDefaults) {
64
87
  HAS_RUN: c.hasRun,
65
88
  PLANTUML_SERVER_URL: c.plantumlServerUrl,
66
89
  DIAGRAM_FORMAT: c.diagramFormat,
67
- D2_LAYOUT: c.d2Layout || 'dagre',
90
+ D2_LAYOUT: c.d2Layout,
68
91
  MD_FILE_NAME: 'README',
69
92
  WEB_FILE_NAME: c.webFileName,
70
93
  SUPPORT_SEARCH: c.supportSearch,
@@ -87,7 +110,12 @@ export default async () => {
87
110
  .option('-w, --watch', 'watch for changes and rebuild')
88
111
  .option('-o, --open', 'open the generated site in the browser (with --site)')
89
112
  .option('--docs', 'a brief explanation for the available configuration options')
90
- .option('-p, --port <n>', 'port used for serving the generated site', parseInt)
113
+ .option('-p, --port <n>', 'port used for serving the generated site', (v) => {
114
+ // parseInt молча резал бы опечатку ('30OO' → 30) — валидируем строго.
115
+ if (!isValidPort(v))
116
+ throw new InvalidArgumentError('ожидается TCP-порт: целое число 1..65535');
117
+ return Number(v);
118
+ })
91
119
  .allowExcessArguments() // позиционные аргументы подкоманды `jre <action>`
92
120
  .parse(process.argv);
93
121
  const opts = program.opts();
@@ -96,9 +124,12 @@ export default async () => {
96
124
  return cmdJre(program.args.slice(1), { force: opts.force });
97
125
  let conf = { get: () => { } };
98
126
  let cacheConf = { get: () => { }, set: () => { }, clear: () => { } };
127
+ // Лок сборки — рядом с конфигом проекта (см. util/lock.ts). Заполняется вместе с conf.
128
+ let lockPath = '';
99
129
  if (!opts.new) {
100
130
  const projectKey = process.cwd().split(path.sep).splice(1).join('_');
101
131
  const configPath = path.join(process.cwd(), opts.configFile ?? '.c4builder');
132
+ lockPath = `${configPath}.lock`;
102
133
  conf = new Configstore(projectKey, {}, { configPath });
103
134
  cacheConf = new Configstore(`${projectKey}_cache`, {}, { configPath: `${configPath}.cache` });
104
135
  // Миграция: чексуммы раньше жили в .c4builder, переносим их в .c4builder.cache,
@@ -113,9 +144,20 @@ export default async () => {
113
144
  }
114
145
  if (opts.docs)
115
146
  return cmdHelp();
116
- // Начальный конфиг для визарда/--list: raw-вид (незаданные поля = undefined),
117
- // чтобы визард спросил недостающее на первом запуске.
118
- const currentConfig = getOptions(conf, false);
147
+ // --reset чистит .c4builder при ЛЮБОМ его содержимом — до строгой проверки конфига,
148
+ // чтобы сломанное значение не мешало собственному сбросу.
149
+ if (opts.reset && !opts.new) {
150
+ clearConsole();
151
+ intro();
152
+ conf.clear();
153
+ cacheConf.clear();
154
+ console.log(`configuration was reset`);
155
+ return;
156
+ }
157
+ // Начальный конфиг для визарда/--list: щадящий разбор (незаданные и битые поля =
158
+ // undefined), чтобы визард спросил недостающее, а --list всё равно показал конфиг.
159
+ const { config: rawConfig, issues: configIssues, salvaged } = readLenientConfig(conf);
160
+ const currentConfig = getOptions(rawConfig);
119
161
  if (opts.new || opts.config || !currentConfig.HAS_RUN)
120
162
  clearConsole();
121
163
  intro();
@@ -126,20 +168,40 @@ export default async () => {
126
168
  if (opts.new)
127
169
  return cmdNewProject(opts);
128
170
  if (opts.list)
129
- return cmdList(currentConfig);
130
- if (opts.reset) {
131
- conf.clear();
132
- cacheConf.clear();
133
- console.log(`configuration was reset`);
134
- return;
171
+ return cmdList(currentConfig, configIssues);
172
+ // Повторная сборка установленного проекта (есть HAS_RUN, не --config) — строгий путь:
173
+ // битый конфиг обязан упасть с внятной ошибкой сразу, а не быть тихо «починен» визардом.
174
+ // Первый запуск (!HAS_RUN) и --config остаются щадящими — это штатный путь восстановления.
175
+ const strictBuild = !opts.config && !!currentConfig.HAS_RUN;
176
+ if (configIssues.length) {
177
+ if (strictBuild)
178
+ failOnConfigIssues(configIssues);
179
+ warnConfigIssues(configIssues);
180
+ // Битые ключи чиним физически (salvage-значение = дефолт схемы, либо удаление):
181
+ // визард переспрашивает не всё (вложенные блоки гейтятся generate*-флагами), а
182
+ // строгий parseConfig после него обязан пройти — иначе exit 1 на каждом запуске.
183
+ for (const { key } of configIssues) {
184
+ const v = salvaged[key];
185
+ if (v === undefined)
186
+ conf.delete(key);
187
+ else
188
+ conf.set(key, v);
189
+ }
135
190
  }
136
191
  await cmdCollect(currentConfig, conf, opts);
137
192
  if (!opts.config) {
138
- conf.set('hasRun', true);
139
193
  let isBuilding = false;
140
194
  let attemptedWatchBuild = false;
141
- // Опции сборки после визарда: полный вид с дефолтами (базовые поля гарантированы).
142
- const options = getOptions(conf);
195
+ // Опции сборки после визарда: строгий разбор итогового конфига. Штатно валиден
196
+ // (визард только что записал корректные значения; повторная сборка с битым конфигом
197
+ // отсеяна выше) — падение здесь страхует от иного невалидного состояния.
198
+ const built = parseConfig((conf.all ?? {}));
199
+ if (!built.ok)
200
+ failOnConfigIssues(built.issues);
201
+ // hasRun — только ПОСЛЕ успешной валидации: битый конфиг с hasRun=true навсегда
202
+ // уводил бы последующие запуски на строгий путь (exit 1 до визарда).
203
+ conf.set('hasRun', true);
204
+ const options = getOptions(built.value);
143
205
  const reloadEmitter = new EventEmitter();
144
206
  reloadEmitter.setMaxListeners(0);
145
207
  if (opts.watch) {
@@ -159,17 +221,31 @@ export default async () => {
159
221
  isBuilding = true;
160
222
  let buildOk = true;
161
223
  try {
162
- await build(options, cacheConf);
163
- while (attemptedWatchBuild) {
164
- attemptedWatchBuild = false;
224
+ // Лок на всю серию ребилдов: параллельная ручная сборка в этом же
225
+ // каталоге не потопчет dist_bk и не потеряет чексуммы кеша.
226
+ const release = acquireBuildLock(lockPath);
227
+ try {
165
228
  await build(options, cacheConf);
229
+ while (attemptedWatchBuild) {
230
+ attemptedWatchBuild = false;
231
+ await build(options, cacheConf);
232
+ }
233
+ }
234
+ finally {
235
+ release();
166
236
  }
167
237
  }
168
238
  catch (err) {
169
239
  buildOk = false;
170
240
  attemptedWatchBuild = false;
171
- const e = err;
172
- console.log(chalk.red(`build failed: ${e?.stack ? e.stack : e}`));
241
+ if (err instanceof BuildLockHeldError) {
242
+ // Соседняя сборка ещё идёт — ребилд пропущен, watch жив.
243
+ console.log(chalk.yellow(`ребилд пропущен: ${err.message}`));
244
+ }
245
+ else {
246
+ const e = err;
247
+ console.log(chalk.red(`build failed: ${e?.stack ? e.stack : e}`));
248
+ }
173
249
  }
174
250
  finally {
175
251
  isBuilding = false;
@@ -179,7 +255,25 @@ export default async () => {
179
255
  });
180
256
  }
181
257
  isBuilding = true;
182
- await build(options, cacheConf);
258
+ let release;
259
+ try {
260
+ release = acquireBuildLock(lockPath);
261
+ }
262
+ catch (e) {
263
+ // Занято живой сборкой — падаем сразу с внятным сообщением, без стектрейса.
264
+ console.error(chalk.red(e.message));
265
+ process.exit(1);
266
+ }
267
+ try {
268
+ await build(options, cacheConf);
269
+ }
270
+ finally {
271
+ release();
272
+ // Одиночная сборка: D2-воркер больше не нужен, без teardown он держал бы
273
+ // процесс. В watch-режиме воркер живёт до конца процесса (Ctrl+C).
274
+ if (!opts.watch)
275
+ await teardownD2();
276
+ }
183
277
  isBuilding = false;
184
278
  if (opts.site)
185
279
  return await cmdSite(options, opts, reloadEmitter);
@@ -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
  }