@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.
- package/README.MD +11 -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 +135 -37
- package/dist/cli/wizard/collect.js +51 -91
- package/dist/config/defaults.js +4 -2
- package/dist/config/options.js +29 -1
- package/dist/config/schema.js +53 -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 +256 -100
- package/dist/core/render/fonts.js +8 -0
- package/dist/core/render/jre.js +155 -51
- package/dist/core/render/pngraster.js +18 -24
- 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/vendor/fonts/NimbusSans-Bold.otf +0 -0
- package/vendor/fonts/NimbusSans-BoldItalic.otf +0 -0
- package/vendor/fonts/NimbusSans-Italic.otf +0 -0
- package/vendor/fonts/NimbusSans-Regular.otf +0 -0
- package/vendor/fonts/README.md +13 -10
- package/vendor/fonts/LiberationSans-Bold.ttf +0 -0
- package/vendor/fonts/LiberationSans-BoldItalic.ttf +0 -0
- package/vendor/fonts/LiberationSans-Italic.ttf +0 -0
- package/vendor/fonts/LiberationSans-Regular.ttf +0 -0
package/README.MD
CHANGED
|
@@ -42,6 +42,11 @@ With `diagramFormat: png`, diagrams are produced by rasterizing their SVG with
|
|
|
42
42
|
native binary, no browser). This gives a single deterministic PNG for both PlantUML and
|
|
43
43
|
D2, using the bundled font. ditaa diagrams stay native PlantUML PNG.
|
|
44
44
|
|
|
45
|
+
**Fonts.** Diagrams use the bundled Nimbus Sans (Helvetica metrics, Cyrillic and Greek —
|
|
46
|
+
see `vendor/fonts/README.md`), so diagram geometry is identical on any machine.
|
|
47
|
+
`useSystemFonts: true` in `.c4builder` (or `--system-fonts` for one run) renders with the
|
|
48
|
+
machine's fonts instead — machine-dependent by design.
|
|
49
|
+
|
|
45
50
|
The easiest way to get started is with the _new project_ template.
|
|
46
51
|
|
|
47
52
|

|
|
@@ -110,7 +115,7 @@ Following the C4 model, folders inside the **source** represent Systems/Containe
|
|
|
110
115
|
class.1.md
|
|
111
116
|
```
|
|
112
117
|
|
|
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
|
|
118
|
+
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
119
|
|
|
115
120
|
## The build
|
|
116
121
|
|
|
@@ -210,18 +215,6 @@ The generated markdown file makes for an easy single page readme for the reposit
|
|
|
210
215
|
|
|
211
216
|

|
|
212
217
|
|
|
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
218
|
### Docsify site with a sidebar
|
|
226
219
|
|
|
227
220
|
After choosing to generate a docsify site, it can be viewed locally by running `c4builder site` and accessing `http://localhost:3000`;
|
|
@@ -232,7 +225,7 @@ The sidebar is generated manually so that it only contains the main entities def
|
|
|
232
225
|
|
|
233
226
|

|
|
234
227
|
|
|
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
|
|
228
|
+
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
229
|
|
|
237
230
|

|
|
238
231
|
|
|
@@ -256,10 +249,6 @@ By default this is disabled. If enabled, the output will no longer include the a
|
|
|
256
249
|
|
|
257
250
|
The current entity structure is inserted after the header
|
|
258
251
|
|
|
259
|
-
### Custom pdf
|
|
260
|
-
|
|
261
|
-
If defined, this css is used in the generation of the pdf files.
|
|
262
|
-
|
|
263
252
|
# Future plans
|
|
264
253
|
|
|
265
254
|
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 +289,10 @@ Publishes to dist-tag `latest`.
|
|
|
300
289
|
|
|
301
290
|
# Change log
|
|
302
291
|
|
|
292
|
+
## v0.3.0
|
|
293
|
+
|
|
294
|
+
PDF output removed — use the docsify site or the single/collection markdown outputs instead (the `pdf`/`pdfCss` options no longer exist).
|
|
295
|
+
|
|
303
296
|
## v0.2.20
|
|
304
297
|
|
|
305
298
|
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,12 +87,13 @@ 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,
|
|
71
94
|
EXECUTE_SCRIPT: c.executeScript,
|
|
72
|
-
EXCLUDE_OTHER_FILES: c.excludeOtherFiles
|
|
95
|
+
EXCLUDE_OTHER_FILES: c.excludeOtherFiles,
|
|
96
|
+
USE_SYSTEM_FONTS: c.useSystemFonts
|
|
73
97
|
};
|
|
74
98
|
}
|
|
75
99
|
export default async () => {
|
|
@@ -87,7 +111,13 @@ export default async () => {
|
|
|
87
111
|
.option('-w, --watch', 'watch for changes and rebuild')
|
|
88
112
|
.option('-o, --open', 'open the generated site in the browser (with --site)')
|
|
89
113
|
.option('--docs', 'a brief explanation for the available configuration options')
|
|
90
|
-
.option('-
|
|
114
|
+
.option('--system-fonts', 'render with system fonts instead of the bundled one (not deterministic)')
|
|
115
|
+
.option('-p, --port <n>', 'port used for serving the generated site', (v) => {
|
|
116
|
+
// parseInt молча резал бы опечатку ('30OO' → 30) — валидируем строго.
|
|
117
|
+
if (!isValidPort(v))
|
|
118
|
+
throw new InvalidArgumentError('ожидается TCP-порт: целое число 1..65535');
|
|
119
|
+
return Number(v);
|
|
120
|
+
})
|
|
91
121
|
.allowExcessArguments() // позиционные аргументы подкоманды `jre <action>`
|
|
92
122
|
.parse(process.argv);
|
|
93
123
|
const opts = program.opts();
|
|
@@ -96,9 +126,12 @@ export default async () => {
|
|
|
96
126
|
return cmdJre(program.args.slice(1), { force: opts.force });
|
|
97
127
|
let conf = { get: () => { } };
|
|
98
128
|
let cacheConf = { get: () => { }, set: () => { }, clear: () => { } };
|
|
129
|
+
// Лок сборки — рядом с конфигом проекта (см. util/lock.ts). Заполняется вместе с conf.
|
|
130
|
+
let lockPath = '';
|
|
99
131
|
if (!opts.new) {
|
|
100
132
|
const projectKey = process.cwd().split(path.sep).splice(1).join('_');
|
|
101
133
|
const configPath = path.join(process.cwd(), opts.configFile ?? '.c4builder');
|
|
134
|
+
lockPath = `${configPath}.lock`;
|
|
102
135
|
conf = new Configstore(projectKey, {}, { configPath });
|
|
103
136
|
cacheConf = new Configstore(`${projectKey}_cache`, {}, { configPath: `${configPath}.cache` });
|
|
104
137
|
// Миграция: чексуммы раньше жили в .c4builder, переносим их в .c4builder.cache,
|
|
@@ -113,9 +146,20 @@ export default async () => {
|
|
|
113
146
|
}
|
|
114
147
|
if (opts.docs)
|
|
115
148
|
return cmdHelp();
|
|
116
|
-
//
|
|
117
|
-
// чтобы
|
|
118
|
-
|
|
149
|
+
// --reset чистит .c4builder при ЛЮБОМ его содержимом — до строгой проверки конфига,
|
|
150
|
+
// чтобы сломанное значение не мешало собственному сбросу.
|
|
151
|
+
if (opts.reset && !opts.new) {
|
|
152
|
+
clearConsole();
|
|
153
|
+
intro();
|
|
154
|
+
conf.clear();
|
|
155
|
+
cacheConf.clear();
|
|
156
|
+
console.log(`configuration was reset`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Начальный конфиг для визарда/--list: щадящий разбор (незаданные и битые поля =
|
|
160
|
+
// undefined), чтобы визард спросил недостающее, а --list всё равно показал конфиг.
|
|
161
|
+
const { config: rawConfig, issues: configIssues, salvaged } = readLenientConfig(conf);
|
|
162
|
+
const currentConfig = getOptions(rawConfig);
|
|
119
163
|
if (opts.new || opts.config || !currentConfig.HAS_RUN)
|
|
120
164
|
clearConsole();
|
|
121
165
|
intro();
|
|
@@ -126,20 +170,42 @@ export default async () => {
|
|
|
126
170
|
if (opts.new)
|
|
127
171
|
return cmdNewProject(opts);
|
|
128
172
|
if (opts.list)
|
|
129
|
-
return cmdList(currentConfig);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
173
|
+
return cmdList(currentConfig, configIssues);
|
|
174
|
+
// Повторная сборка установленного проекта (есть HAS_RUN, не --config) — строгий путь:
|
|
175
|
+
// битый конфиг обязан упасть с внятной ошибкой сразу, а не быть тихо «починен» визардом.
|
|
176
|
+
// Первый запуск (!HAS_RUN) и --config остаются щадящими — это штатный путь восстановления.
|
|
177
|
+
const strictBuild = !opts.config && !!currentConfig.HAS_RUN;
|
|
178
|
+
if (configIssues.length) {
|
|
179
|
+
if (strictBuild)
|
|
180
|
+
failOnConfigIssues(configIssues);
|
|
181
|
+
warnConfigIssues(configIssues);
|
|
182
|
+
// Битые ключи чиним физически (salvage-значение = дефолт схемы, либо удаление):
|
|
183
|
+
// визард переспрашивает не всё (вложенные блоки гейтятся generate*-флагами), а
|
|
184
|
+
// строгий parseConfig после него обязан пройти — иначе exit 1 на каждом запуске.
|
|
185
|
+
for (const { key } of configIssues) {
|
|
186
|
+
const v = salvaged[key];
|
|
187
|
+
if (v === undefined)
|
|
188
|
+
conf.delete(key);
|
|
189
|
+
else
|
|
190
|
+
conf.set(key, v);
|
|
191
|
+
}
|
|
135
192
|
}
|
|
136
193
|
await cmdCollect(currentConfig, conf, opts);
|
|
137
194
|
if (!opts.config) {
|
|
138
|
-
conf.set('hasRun', true);
|
|
139
195
|
let isBuilding = false;
|
|
140
196
|
let attemptedWatchBuild = false;
|
|
141
|
-
// Опции сборки после визарда:
|
|
142
|
-
|
|
197
|
+
// Опции сборки после визарда: строгий разбор итогового конфига. Штатно валиден
|
|
198
|
+
// (визард только что записал корректные значения; повторная сборка с битым конфигом
|
|
199
|
+
// отсеяна выше) — падение здесь страхует от иного невалидного состояния.
|
|
200
|
+
const built = parseConfig((conf.all ?? {}));
|
|
201
|
+
if (!built.ok)
|
|
202
|
+
failOnConfigIssues(built.issues);
|
|
203
|
+
// hasRun — только ПОСЛЕ успешной валидации: битый конфиг с hasRun=true навсегда
|
|
204
|
+
// уводил бы последующие запуски на строгий путь (exit 1 до визарда).
|
|
205
|
+
conf.set('hasRun', true);
|
|
206
|
+
const options = getOptions(built.value);
|
|
207
|
+
if (opts.systemFonts)
|
|
208
|
+
options.USE_SYSTEM_FONTS = true; // флаг сильнее ключа конфига
|
|
143
209
|
const reloadEmitter = new EventEmitter();
|
|
144
210
|
reloadEmitter.setMaxListeners(0);
|
|
145
211
|
if (opts.watch) {
|
|
@@ -159,17 +225,31 @@ export default async () => {
|
|
|
159
225
|
isBuilding = true;
|
|
160
226
|
let buildOk = true;
|
|
161
227
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
228
|
+
// Лок на всю серию ребилдов: параллельная ручная сборка в этом же
|
|
229
|
+
// каталоге не потопчет dist_bk и не потеряет чексуммы кеша.
|
|
230
|
+
const release = acquireBuildLock(lockPath);
|
|
231
|
+
try {
|
|
165
232
|
await build(options, cacheConf);
|
|
233
|
+
while (attemptedWatchBuild) {
|
|
234
|
+
attemptedWatchBuild = false;
|
|
235
|
+
await build(options, cacheConf);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
release();
|
|
166
240
|
}
|
|
167
241
|
}
|
|
168
242
|
catch (err) {
|
|
169
243
|
buildOk = false;
|
|
170
244
|
attemptedWatchBuild = false;
|
|
171
|
-
|
|
172
|
-
|
|
245
|
+
if (err instanceof BuildLockHeldError) {
|
|
246
|
+
// Соседняя сборка ещё идёт — ребилд пропущен, watch жив.
|
|
247
|
+
console.log(chalk.yellow(`ребилд пропущен: ${err.message}`));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
const e = err;
|
|
251
|
+
console.log(chalk.red(`build failed: ${e?.stack ? e.stack : e}`));
|
|
252
|
+
}
|
|
173
253
|
}
|
|
174
254
|
finally {
|
|
175
255
|
isBuilding = false;
|
|
@@ -179,7 +259,25 @@ export default async () => {
|
|
|
179
259
|
});
|
|
180
260
|
}
|
|
181
261
|
isBuilding = true;
|
|
182
|
-
|
|
262
|
+
let release;
|
|
263
|
+
try {
|
|
264
|
+
release = acquireBuildLock(lockPath);
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
// Занято живой сборкой — падаем сразу с внятным сообщением, без стектрейса.
|
|
268
|
+
console.error(chalk.red(e.message));
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
await build(options, cacheConf);
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
release();
|
|
276
|
+
// Одиночная сборка: D2-воркер больше не нужен, без teardown он держал бы
|
|
277
|
+
// процесс. В watch-режиме воркер живёт до конца процесса (Ctrl+C).
|
|
278
|
+
if (!opts.watch)
|
|
279
|
+
await teardownD2();
|
|
280
|
+
}
|
|
183
281
|
isBuilding = false;
|
|
184
282
|
if (opts.site)
|
|
185
283
|
return await cmdSite(options, opts, reloadEmitter);
|