@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 +6 -18
- package/dist/cli/commands/help.js +0 -2
- package/dist/cli/commands/jre.js +10 -3
- package/dist/cli/commands/list.js +9 -1
- package/dist/cli/commands/new.js +10 -29
- package/dist/cli/dispatch.js +130 -36
- package/dist/cli/wizard/collect.js +51 -91
- package/dist/config/defaults.js +2 -1
- package/dist/config/options.js +29 -1
- package/dist/config/schema.js +52 -32
- package/dist/core/build.js +74 -48
- package/dist/core/compose/docsify.template.js +2 -2
- package/dist/core/compose/markdown.js +146 -135
- package/dist/core/render/d2renderer.js +67 -11
- package/dist/core/render/diagrams.js +232 -85
- package/dist/core/render/fonts.js +8 -0
- package/dist/core/render/jre.js +155 -51
- package/dist/core/render/pngraster.js +14 -17
- package/dist/core/scan/tree.js +64 -16
- package/dist/util/http.js +55 -0
- package/dist/util/lock.js +58 -0
- package/dist/util/utils.js +10 -25
- package/package.json +5 -1
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
|
|
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
|

|
|
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
|
-

|
|
218
|
-
|
|
219
|
-
### Single pdf file with a table of contents
|
|
220
|
-
|
|
221
|
-
The easiest format to share the full documentation
|
|
222
|
-
|
|
223
|
-

|
|
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
|

|
|
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
|
|
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
|

|
|
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')}
|
package/dist/cli/commands/jre.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { resolveJava, detectSystemJava, TEMURIN_FEATURE } from "../../core/render/jre.js";
|
|
3
|
-
//
|
|
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
|
};
|
package/dist/cli/commands/new.js
CHANGED
|
@@ -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)
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 без имени — фатально ---
|
package/dist/cli/dispatch.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
28
|
+
// Читает сырой `.c4builder` и разбирает его щадяще (config/parseConfig): для визарда и
|
|
29
|
+
// `--list`. Возвращает лишь ЗАДАННЫЕ пользователем и ВАЛИДНЫЕ ключи (недостающее и битое
|
|
30
|
+
// = undefined, как читалось до валидации), чтобы визард спросил их на первом запуске,
|
|
31
|
+
// а `--list` всё равно показал конфиг. Битые ключи — в `issues` для предупреждения.
|
|
32
|
+
function readLenientConfig(conf) {
|
|
23
33
|
const raw = (conf.all ?? {});
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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',
|
|
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
|
-
//
|
|
117
|
-
// чтобы
|
|
118
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
7
|
-
|
|
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(
|
|
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(
|
|
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: (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
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',
|
|
120
|
-
conf.set('generateCompleteMD',
|
|
121
|
-
conf.set('generateWEB',
|
|
122
|
-
if (responses.generate.
|
|
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.
|
|
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
|
-
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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',
|
|
275
|
-
conf.set('includeLinkToDiagram',
|
|
276
|
-
conf.set('diagramsOnTop',
|
|
277
|
-
conf.set('embedDiagram',
|
|
278
|
-
conf.set('excludeOtherFiles',
|
|
279
|
-
conf.set('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(
|
|
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(
|
|
257
|
+
validate: validate(nonEmptyString, 'укажите формат изображения диаграмм')
|
|
298
258
|
});
|
|
299
259
|
conf.set('diagramFormat', responses.diagramFormat);
|
|
300
260
|
}
|