@alfacapital-tech/c4builder 0.4.0-rc.2 → 0.4.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 +15 -3
- package/dist/cli/dispatch.js +4 -0
- package/dist/core/plugins/source.js +7 -1
- package/dist/core/plugins/tree.js +3 -1
- package/dist/core/render/diagrams.js +29 -1
- package/dist/plugins/openspec/fences.js +5 -2
- package/dist/plugins/openspec/index.js +2 -1
- package/dist/plugins/openspec/render.js +12 -4
- package/dist/util/glob.js +6 -3
- package/package.json +1 -1
package/README.MD
CHANGED
|
@@ -92,7 +92,8 @@ Options:
|
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
There is also a `jre` subcommand: `c4builder jre install [--force]` pre-downloads the
|
|
95
|
-
private JRE cache, `c4builder jre info` prints which Java would be used.
|
|
95
|
+
private JRE cache, `c4builder jre info` prints which Java would be used. `c4builder site`
|
|
96
|
+
is accepted as a positional subcommand too, equivalent to `--site`.
|
|
96
97
|
|
|
97
98
|
### Validating single diagrams
|
|
98
99
|
|
|
@@ -324,7 +325,9 @@ spec deltas live under `specs/…` mirroring the folder layout (a folder that ha
|
|
|
324
325
|
```` ```plantuml ```` / ```` ```d2 ```` fences inside artifacts, deltas and store specs are
|
|
325
326
|
rendered by the **local** engines (nothing is sent to plantuml.com); the image name is derived
|
|
326
327
|
from the fence content, so inserting or reordering fences never reuses a neighbour's cached
|
|
327
|
-
image.
|
|
328
|
+
image. A block that fails to render does not fail the build (these blocks used to be rendered by
|
|
329
|
+
the browser, which showed an error image): the page gets a placeholder image and the build prints
|
|
330
|
+
a warning naming the artifact file. Relative links between artifacts of a change point to their pages, referenced files
|
|
328
331
|
(images, notes) are copied next to the page (and to the output root for the complete markdown;
|
|
329
332
|
`excludeOtherFiles` applies). The store folder is watched in `-w`.
|
|
330
333
|
|
|
@@ -348,7 +351,9 @@ needed by readers. The plugin forces `executeScript` (logged) and injects `swagg
|
|
|
348
351
|
`swagger-ui-bundle.js` into `index.html` after the template is rendered (skipped when the template
|
|
349
352
|
already links the bundle), so custom `docsifyTemplate`s need no change. The output folder is never
|
|
350
353
|
treated as a source (`dir: "."` is fine). Archives are downloaded once per process (watch mode
|
|
351
|
-
reuses them).
|
|
354
|
+
reuses them). Behind a TLS-intercepting proxy the download fails with `self-signed certificate in
|
|
355
|
+
certificate chain` — point `NODE_EXTRA_CA_CERTS` at the CA bundle (Node reads neither the system
|
|
356
|
+
store nor `SSL_CERT_FILE`); the error message says so as well.
|
|
352
357
|
|
|
353
358
|
| Option | Default | Meaning |
|
|
354
359
|
|-----------|----------------------------------|------------------------------------------|
|
|
@@ -486,6 +491,13 @@ Publishes to dist-tag `latest`.
|
|
|
486
491
|
The first build after upgrading re-renders everything once.
|
|
487
492
|
- Watch mode ignores the output folder, its backup and `.c4builder*`, and de-duplicates nested
|
|
488
493
|
watch roots (a plugin `dir` covering the project no longer loops the rebuild)
|
|
494
|
+
- A broken ```` ```plantuml ````/```` ```d2 ```` block inside an OpenSpec artifact no longer fails
|
|
495
|
+
the build: the page gets a placeholder image and the warning names the source file
|
|
496
|
+
- Diagram errors name the file the diagram came from (fence names are content hashes)
|
|
497
|
+
- `{a,b}` glob alternatives accept `*`/`?` inside them (`{*.yaml,*.yml}`)
|
|
498
|
+
- Archive download failures caused by a TLS-intercepting proxy suggest `NODE_EXTRA_CA_CERTS`
|
|
499
|
+
- `c4builder site` works as a positional subcommand, the same as `--site` (it is the form the
|
|
500
|
+
post-build hint prints)
|
|
489
501
|
|
|
490
502
|
## v0.3.0
|
|
491
503
|
|
package/dist/cli/dispatch.js
CHANGED
|
@@ -131,6 +131,10 @@ export default async () => {
|
|
|
131
131
|
// Проверка отдельных диаграмм — тоже без конфига проекта.
|
|
132
132
|
if (program.args[0] === 'check')
|
|
133
133
|
return cmdCheck(program.args.slice(1));
|
|
134
|
+
// `c4builder site` — подсказка после сборки печатает именно эту форму; без
|
|
135
|
+
// этой строки allowExcessArguments молча глотал бы её и просто пересобирал.
|
|
136
|
+
if (program.args[0] === 'site')
|
|
137
|
+
opts.site = true;
|
|
134
138
|
let conf = { get: () => { } };
|
|
135
139
|
let cacheConf = { get: () => { }, set: () => { }, clear: () => { } };
|
|
136
140
|
// Лок сборки — рядом с конфигом проекта (см. util/lock.ts). Заполняется вместе с conf.
|
|
@@ -22,6 +22,11 @@ export const redactUrl = (url) => {
|
|
|
22
22
|
return url;
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
|
+
// За корпоративным TLS-перехватом Node не доверяет подменённой цепочке: свой набор
|
|
26
|
+
// корней он берёт не из системы и не из SSL_CERT_FILE — только NODE_EXTRA_CA_CERTS.
|
|
27
|
+
export const tlsHint = (reason) => /certificate|self.signed|unable to (?:verify|get local issuer)/i.test(reason)
|
|
28
|
+
? ' — TLS-перехват? укажите путь к CA-бандлу в NODE_EXTRA_CA_CERTS'
|
|
29
|
+
: '';
|
|
25
30
|
const downloadArchive = async (url, headers) => {
|
|
26
31
|
const dir = path.join(os.tmpdir(), `c4builder-src-${crypto.createHash('sha1').update(url).digest('hex')}`);
|
|
27
32
|
// Распаковываем в свой staging и заменяем кэш атомарным rename — параллельная
|
|
@@ -33,7 +38,8 @@ const downloadArchive = async (url, headers) => {
|
|
|
33
38
|
body = await httpGetBuffer(url, { headers });
|
|
34
39
|
}
|
|
35
40
|
catch (e) {
|
|
36
|
-
|
|
41
|
+
const reason = e.message.replaceAll(url, redactUrl(url));
|
|
42
|
+
throw new Error(`архив недоступен: ${reason}${tlsHint(reason)}`);
|
|
37
43
|
}
|
|
38
44
|
fs.rmSync(stage, { recursive: true, force: true });
|
|
39
45
|
fs.mkdirSync(stage, { recursive: true });
|
|
@@ -76,7 +76,9 @@ export const addPage = (tree, options, page) => {
|
|
|
76
76
|
ext,
|
|
77
77
|
engine,
|
|
78
78
|
content: d.content,
|
|
79
|
-
isDitaa: engine === 'plantuml' && /@startditaa/i.test(d.content)
|
|
79
|
+
isDitaa: engine === 'plantuml' && /@startditaa/i.test(d.content),
|
|
80
|
+
source: d.source,
|
|
81
|
+
soft: d.soft
|
|
80
82
|
});
|
|
81
83
|
}
|
|
82
84
|
return item;
|
|
@@ -188,6 +188,22 @@ export const renderDiagram = (content, options) => new Promise((resolve, reject)
|
|
|
188
188
|
// bkFolderName — каталог бэкапа dist, откуда восстанавливаются неизменённые картинки
|
|
189
189
|
// по чексумме. Его lifecycle (создание/удаление) держит оркестратор build(); сюда
|
|
190
190
|
// путь приходит явным параметром, а не пересчитывается из общей константы.
|
|
191
|
+
// Картинка-заглушка вместо упавшей «мягкой» диаграммы: ошибка видна на странице,
|
|
192
|
+
// сайт остаётся собранным (так же вёл себя онлайн-рендер docsify-plantuml).
|
|
193
|
+
const errorPlaceholderSvg = (name, reason) => {
|
|
194
|
+
const esc = (s) => s.replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' })[c]);
|
|
195
|
+
const lines = [`${name}: диаграмма не отрендерена`, ...reason.split('\n')]
|
|
196
|
+
.flatMap((l) => l.match(/.{1,88}/g) ?? [''])
|
|
197
|
+
.slice(0, 8);
|
|
198
|
+
const w = 720;
|
|
199
|
+
const h = 34 + lines.length * 20;
|
|
200
|
+
const text = lines
|
|
201
|
+
.map((l, i) => `<text x="16" y="${34 + i * 20}" font-size="14">${esc(l)}</text>`)
|
|
202
|
+
.join('');
|
|
203
|
+
return (`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" ` +
|
|
204
|
+
`font-family="${DEFAULT_FONT_NAME}" fill="#8b0000">` +
|
|
205
|
+
`<rect x="1" y="1" width="${w - 2}" height="${h - 2}" fill="#fff5f5" stroke="#8b0000"/>${text}</svg>`);
|
|
206
|
+
};
|
|
191
207
|
export const generateImages = async (tree, options, onImageGenerated, cacheConf, bkFolderName) => {
|
|
192
208
|
// Чексуммы прошлого прогона — в Set: попадание проверяется O(1) на диаграмму
|
|
193
209
|
// (было O(N) find → O(N²) на всё дерево).
|
|
@@ -298,6 +314,10 @@ export const generateImages = async (tree, options, onImageGenerated, cacheConf,
|
|
|
298
314
|
const diagramContent = diagram.content;
|
|
299
315
|
const includePath = item.dir;
|
|
300
316
|
const isDitaa = diagram.isDitaa;
|
|
317
|
+
// Имя картинки диаграммы виртуальной страницы производно от контента —
|
|
318
|
+
// без исходного файла в сообщении его не найти.
|
|
319
|
+
const source = diagram.source;
|
|
320
|
+
const soft = diagram.soft === true;
|
|
301
321
|
const task = withProgress(async () => {
|
|
302
322
|
// render diagram to image: D2 через WASM, PlantUML — прямым вызовом java.
|
|
303
323
|
// Для растеризации PlantUML не-ditaa рендерим в svg (не -tpng), затем resvg.
|
|
@@ -328,7 +348,15 @@ export const generateImages = async (tree, options, onImageGenerated, cacheConf,
|
|
|
328
348
|
}
|
|
329
349
|
catch (err) {
|
|
330
350
|
// Имя диаграммы в ошибку: renderDiagram/renderD2 сами его не знают.
|
|
331
|
-
|
|
351
|
+
const reason = `${err.message || err}`;
|
|
352
|
+
const msg = `Диаграмма "${outName}"${source ? ` (${source})` : ''}: ${reason}`;
|
|
353
|
+
if (!soft)
|
|
354
|
+
throw new Error(msg);
|
|
355
|
+
// Диаграммы из markdown-блоков плагинов писались под онлайн-рендер,
|
|
356
|
+
// где битый блок давал картинку с ошибкой, а не падение сборки.
|
|
357
|
+
console.log(chalk.yellow(`⚠ ${msg}`));
|
|
358
|
+
const svg = Buffer.from(errorPlaceholderSvg(outName, reason), 'utf-8');
|
|
359
|
+
await writeFile(filePath, outFormat === 'png' ? await rasterizeSvgToPng(svg, options.USE_SYSTEM_FONTS) : svg);
|
|
332
360
|
}
|
|
333
361
|
});
|
|
334
362
|
if (entryPath !== null)
|
|
@@ -26,7 +26,7 @@ export const mapOutsideFences = (md, fn) => {
|
|
|
26
26
|
export const createFenceExtractor = () => {
|
|
27
27
|
const seen = new Set();
|
|
28
28
|
return {
|
|
29
|
-
extract(md, base) {
|
|
29
|
+
extract(md, base, source) {
|
|
30
30
|
const diagrams = [];
|
|
31
31
|
const markdown = md.replace(FENCE_RE, (whole, indent, _fence, info, body) => {
|
|
32
32
|
const lang = info.trim().split(/\s+/)[0]?.toLowerCase() ?? '';
|
|
@@ -45,7 +45,10 @@ export const createFenceExtractor = () => {
|
|
|
45
45
|
const file = `${base}-${hash}${ext}`;
|
|
46
46
|
if (!seen.has(file)) {
|
|
47
47
|
seen.add(file);
|
|
48
|
-
|
|
48
|
+
// soft: блок в артефакте — не файл проекта; синтаксическая
|
|
49
|
+
// ошибка в нём не должна ронять весь сайт (раньше такие блоки
|
|
50
|
+
// рендерил браузер и показывал картинку с ошибкой).
|
|
51
|
+
diagrams.push({ file, content, source, soft: true });
|
|
49
52
|
}
|
|
50
53
|
return `${indent}`;
|
|
51
54
|
});
|
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { definePlugin } from "../../core/plugins/types.js";
|
|
7
7
|
import { scanStore } from "./scan.js";
|
|
8
|
-
import { renderArchiveIndex, renderChange, renderChangesIndex, renderMarkdown, renderSpecTree, renderSummary } from "./render.js";
|
|
8
|
+
import { renderArchiveIndex, renderChange, renderChangesIndex, relSource, renderMarkdown, renderSpecTree, renderSummary } from "./render.js";
|
|
9
9
|
const optionsSchema = z
|
|
10
10
|
.object({
|
|
11
11
|
dir: z.string().default('openspec'),
|
|
@@ -61,6 +61,7 @@ export default definePlugin({
|
|
|
61
61
|
addPages(renderSpecTree(store.specs, [mount, 'Specs'], options, (pagePath, spec) => renderMarkdown(pagePath, `spec-${spec.path.join('-')}`, spec.content, {
|
|
62
62
|
srcRoot: specsDir,
|
|
63
63
|
fromDir: spec.path.join('/'),
|
|
64
|
+
source: relSource(specsDir, `${spec.path.join('/')}/spec.md`),
|
|
64
65
|
options
|
|
65
66
|
})), specsDir);
|
|
66
67
|
ctx.addPage({
|
|
@@ -78,6 +78,8 @@ const ignoreTitle = (title) => {
|
|
|
78
78
|
const t = title.trim().replace(/^(["'])(.*)\1$/s, '$2');
|
|
79
79
|
return ` ':ignore${t ? ` ${t}` : ''}'`;
|
|
80
80
|
};
|
|
81
|
+
/** Путь файла для сообщений — от cwd, чтобы его можно было открыть как есть. */
|
|
82
|
+
export const relSource = (root, rel) => path.relative(process.cwd(), path.join(root, rel)).split(path.sep).join('/');
|
|
81
83
|
/**
|
|
82
84
|
* Markdown одного исходного файла → страница сайта: fenced-диаграммы вырезаются, ссылки
|
|
83
85
|
* на другие страницы источника (`target`, `#x` → `?id=x`) — на их адреса, прочие
|
|
@@ -85,8 +87,8 @@ const ignoreTitle = (title) => {
|
|
|
85
87
|
* странице (картинки docsify грузит относительно страницы, обычные ссылки — от корня сайта).
|
|
86
88
|
* `fromDir` — posix-каталог файла относительно `srcRoot` (ссылки в нём резолвятся от файла).
|
|
87
89
|
*/
|
|
88
|
-
export const renderMarkdown = (pagePath, base, content, { srcRoot, fromDir = '', target = () => undefined, options }) => {
|
|
89
|
-
const { markdown, diagrams } = createFenceExtractor().extract(content, base);
|
|
90
|
+
export const renderMarkdown = (pagePath, base, content, { srcRoot, fromDir = '', target = () => undefined, source, options }) => {
|
|
91
|
+
const { markdown, diagrams } = createFenceExtractor().extract(content, base, source);
|
|
90
92
|
const files = [];
|
|
91
93
|
const rewrite = (t) => t.replace(LINK_RE, (whole, bang, text, href, title = '') => {
|
|
92
94
|
const inner = text.includes('![') ? rewrite(text) : text;
|
|
@@ -172,7 +174,13 @@ export const renderChange = (change, segments, order, options) => {
|
|
|
172
174
|
const delta = deltas.find((d) => `specs/${d.path.join('/')}/spec.md` === rel);
|
|
173
175
|
return delta && [...specsBase, ...delta.path];
|
|
174
176
|
};
|
|
175
|
-
const render = (pagePath, base, content, fromDir = '') => renderMarkdown(pagePath, base, content, {
|
|
177
|
+
const render = (pagePath, base, content, fromDir = '', file = `${base}.md`) => renderMarkdown(pagePath, base, content, {
|
|
178
|
+
srcRoot: change.dir,
|
|
179
|
+
fromDir,
|
|
180
|
+
target,
|
|
181
|
+
source: relSource(change.dir, path.posix.join(fromDir, file)),
|
|
182
|
+
options
|
|
183
|
+
});
|
|
176
184
|
const subpages = ordered
|
|
177
185
|
.filter((a) => a !== inline)
|
|
178
186
|
.map((a) => render([...segments, a.name], a.name, a.content));
|
|
@@ -201,7 +209,7 @@ export const renderChange = (change, segments, order, options) => {
|
|
|
201
209
|
...subpages
|
|
202
210
|
];
|
|
203
211
|
if (deltas.length) {
|
|
204
|
-
const tree = renderSpecTree(deltas, specsBase, options, (pagePath, spec) => render(pagePath, `spec-${spec.path.join('-')}`, spec.content, `specs/${spec.path.join('/')}
|
|
212
|
+
const tree = renderSpecTree(deltas, specsBase, options, (pagePath, spec) => render(pagePath, `spec-${spec.path.join('-')}`, spec.content, `specs/${spec.path.join('/')}`, 'spec.md'));
|
|
205
213
|
// Артефакт specs.md и корень дельт — один путь: сливаем в страницу артефакта.
|
|
206
214
|
const artifact = pages.find((p) => p.path.length === segments.length + 1 && p.path.at(-1) === 'specs');
|
|
207
215
|
for (const p of tree) {
|
package/dist/util/glob.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `**/openapi.{yaml,yml,json}`; `fs.glob` появился только в Node 22, engines — ≥20.19.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
|
|
6
|
+
const convert = (glob) => {
|
|
7
7
|
let re = '';
|
|
8
8
|
for (let i = 0; i < glob.length; i++) {
|
|
9
9
|
const c = glob[i];
|
|
@@ -28,10 +28,12 @@ export const globToRegExp = (glob) => {
|
|
|
28
28
|
if (end === -1)
|
|
29
29
|
re += '\\{';
|
|
30
30
|
else {
|
|
31
|
+
// Альтернативы — тем же конвертером: `{*.yaml,*.yml}` должен работать
|
|
32
|
+
// как glob, а не как буквальная звёздочка.
|
|
31
33
|
re += `(?:${glob
|
|
32
34
|
.slice(i + 1, end)
|
|
33
35
|
.split(',')
|
|
34
|
-
.map(
|
|
36
|
+
.map(convert)
|
|
35
37
|
.join('|')})`;
|
|
36
38
|
i = end;
|
|
37
39
|
}
|
|
@@ -39,8 +41,9 @@ export const globToRegExp = (glob) => {
|
|
|
39
41
|
else
|
|
40
42
|
re += c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
41
43
|
}
|
|
42
|
-
return
|
|
44
|
+
return re;
|
|
43
45
|
};
|
|
46
|
+
export const globToRegExp = (glob) => new RegExp(`^${convert(glob)}$`);
|
|
44
47
|
/**
|
|
45
48
|
* Файлы под root, чьи относительные posix-пути совпадают с glob (сортировано).
|
|
46
49
|
* `.git`/`node_modules` и абсолютные каталоги `skipDirs` не обходятся.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfacapital-tech/c4builder",
|
|
3
|
-
"version": "0.4.0-rc.
|
|
3
|
+
"version": "0.4.0-rc.3",
|
|
4
4
|
"description": "A CLI tool designed to compile a folder structure of markdowns and plant uml files into a site, single file markdown or a collection of markdowns with links",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|