confluence-md-sync 0.1.1 → 0.2.1

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
@@ -48,10 +48,11 @@ Markdown + three placeholder kinds:
48
48
  ```markdown
49
49
  # Отчёт за месяц
50
50
 
51
- {{img:flow.png}} <!-- inline image from images[] -->
51
+ {{img:chart.png}} <!-- inline image from images[] -->
52
+ {{img:flow.bpmn}} <!-- BPMN diagram — rendered to PNG automatically -->
52
53
  Исходник: {{file:data.csv}} <!-- attachment link from files[] -->
53
54
 
54
- {{table:summary}} <!-- table injected from tables[] -->
55
+ {{table:summary}} <!-- table injected from tables[] -->
55
56
  ```
56
57
 
57
58
  ```ts
@@ -66,7 +67,10 @@ const summary = renderMarkdownTable(rows, [
66
67
  await publishPage({
67
68
  pageId: '123456789',
68
69
  markdownPath: 'docs/report.md',
69
- images: ['build/flow.png'],
70
+ images: [
71
+ 'build/chart.png',
72
+ 'docs/flow.bpmn', // converted to flow.png on the fly (see BPMN section)
73
+ ],
70
74
  files: ['build/data.csv'],
71
75
  tables: [{
72
76
  name: 'summary',
@@ -95,6 +99,59 @@ const { storage } = await publishPage({ pageId, markdownPath, dryRun: true }, cf
95
99
  await publishPage({ pageId, markdown: '# Generated\n\ntext' }, cfg);
96
100
  ```
97
101
 
102
+ ## BPMN diagrams out of the box
103
+
104
+ Pass a `.bpmn` file as an image — it is rendered to PNG at publish time
105
+ (headless Chromium via [bpmn-to-image](https://npmjs.com/package/bpmn-to-image),
106
+ an optional peer dependency).
107
+
108
+ Setup — install the converter **and** force a current puppeteer:
109
+
110
+ ```bash
111
+ npm install -D bpmn-to-image puppeteer
112
+ ```
113
+
114
+ ```jsonc
115
+ // package.json — required: bpmn-to-image pins puppeteer 21, whose bundled
116
+ // Chromium fails to launch on recent OSes ("socket hang up"). The override
117
+ // must reference the direct dependency ("$puppeteer"), otherwise `npm ci`
118
+ // fails with "Override for puppeteer conflicts with direct dependency".
119
+ {
120
+ "devDependencies": {
121
+ "bpmn-to-image": "^0.7.0",
122
+ "puppeteer": "^24.0.0"
123
+ },
124
+ "overrides": {
125
+ "puppeteer": "$puppeteer"
126
+ }
127
+ }
128
+ ```
129
+
130
+ ```markdown
131
+ Процесс выпуска релиза:
132
+
133
+ {{img:release-flow.bpmn}}
134
+ ```
135
+
136
+ ```ts
137
+ await publishPage({
138
+ pageId: '123456789',
139
+ markdownPath: 'docs/process.md',
140
+ images: ['docs/release-flow.bpmn'], // converted to release-flow.png automatically
141
+ bpmnOutDir: 'build', // optional; default: temp dir per run
142
+ }, cfg);
143
+ ```
144
+
145
+ Chromium PNG output is not byte-stable between runs, so dedup is pinned to
146
+ the SHA-256 of the *source* `.bpmn` via a `.src-sha256` sidecar — unchanged
147
+ diagrams never create new attachment versions. Batch pre-conversion is also
148
+ available:
149
+
150
+ ```ts
151
+ import { convertBpmnFolder } from 'confluence-md-sync';
152
+ await convertBpmnFolder({ srcDir: 'docs/diagrams', outDir: 'build' });
153
+ ```
154
+
98
155
  ## Read pages and tables
99
156
 
100
157
  ```ts
@@ -0,0 +1,46 @@
1
+ /**
2
+ * BPMN → image conversion (out-of-the-box BPMN support).
3
+ *
4
+ * Тяжёлые зависимости (`bpmn-to-image` + puppeteer/Chromium) — опциональный
5
+ * peer dependency: импортируются динамически только когда конвертация
6
+ * реально запрошена. Ядро библиотеки остаётся лёгким.
7
+ *
8
+ * npm install -D bpmn-to-image
9
+ */
10
+ export type BpmnImageFormat = 'png' | 'svg' | 'pdf';
11
+ export declare const BPMN_FILE_RE: RegExp;
12
+ export declare function isBpmnFile(path: string): boolean;
13
+ /** `p1.bpmn` → `p1.png` (имя выходного файла для формата). */
14
+ export declare function bpmnOutputName(file: string, format?: BpmnImageFormat): string;
15
+ export interface BpmnConversion {
16
+ input: string;
17
+ output: string;
18
+ }
19
+ /**
20
+ * Конвертирует BPMN-файлы в изображения по явному списку input → output.
21
+ *
22
+ * Рядом с каждым результатом пишется sidecar `<output>.src-sha256` с SHA-256
23
+ * исходного BPMN — AttachmentService использует его как dedup-tag вместо SHA
24
+ * результата (PNG от Chromium байт-нестабилен между запусками, BPMN —
25
+ * стабилен). Так Confluence не плодит лишние версии аттача, когда диаграмма
26
+ * не менялась.
27
+ */
28
+ export declare function convertBpmn(conversions: BpmnConversion[]): Promise<void>;
29
+ export interface ConvertBpmnFolderOptions {
30
+ /** Папка-источник с `*.bpmn`. */
31
+ srcDir: string;
32
+ /** Папка-приёмник для сгенерированных изображений. Будет создана, если её нет. */
33
+ outDir: string;
34
+ /** Формат выхода. Дефолт — `png`. */
35
+ format?: BpmnImageFormat;
36
+ /** Регулярка для фильтра входов. Дефолт — `/\.bpmn$/i`. */
37
+ pattern?: RegExp;
38
+ }
39
+ /**
40
+ * Конвертирует все BPMN-файлы из `srcDir` в изображения в `outDir`.
41
+ * Файлы получают то же базовое имя с расширением формата (`p1.bpmn` → `p1.png`).
42
+ *
43
+ * Сгенерированные файлы — артефакты сборки: коммитить их не нужно.
44
+ * В Confluence они улетают как аттачи на этапе публикации.
45
+ */
46
+ export declare function convertBpmnFolder(opts: ConvertBpmnFolderOptions): Promise<BpmnConversion[]>;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * BPMN → image conversion (out-of-the-box BPMN support).
3
+ *
4
+ * Тяжёлые зависимости (`bpmn-to-image` + puppeteer/Chromium) — опциональный
5
+ * peer dependency: импортируются динамически только когда конвертация
6
+ * реально запрошена. Ядро библиотеки остаётся лёгким.
7
+ *
8
+ * npm install -D bpmn-to-image
9
+ */
10
+ import { createHash } from 'node:crypto';
11
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { basename, dirname, extname, join } from 'node:path';
13
+ import { SRC_SHA_SIDECAR_SUFFIX } from '../attachments/attachment.js';
14
+ export const BPMN_FILE_RE = /\.bpmn$/i;
15
+ export function isBpmnFile(path) {
16
+ return BPMN_FILE_RE.test(path);
17
+ }
18
+ /** `p1.bpmn` → `p1.png` (имя выходного файла для формата). */
19
+ export function bpmnOutputName(file, format = 'png') {
20
+ return `${basename(file, extname(file))}.${format}`;
21
+ }
22
+ let puppeteerPatched = false;
23
+ /**
24
+ * bpmn-to-image вызывает puppeteer.launch() без флагов. В CI Chromium
25
+ * запускается из-под root и без --no-sandbox просто не стартует. Патчим
26
+ * глобальный puppeteer.launch один раз — best-effort: если puppeteer
27
+ * не резолвится из нашего контекста, bpmn-to-image разберётся сам.
28
+ */
29
+ async function patchPuppeteerForCI() {
30
+ if (puppeteerPatched)
31
+ return;
32
+ puppeteerPatched = true;
33
+ try {
34
+ const { default: puppeteer } = await import('puppeteer');
35
+ const origLaunch = puppeteer.launch.bind(puppeteer);
36
+ puppeteer.launch = ((options = {}) => {
37
+ const existing = options?.args ?? [];
38
+ const patched = [
39
+ ...existing,
40
+ '--no-sandbox',
41
+ '--disable-setuid-sandbox',
42
+ '--disable-dev-shm-usage',
43
+ ];
44
+ return origLaunch({ ...options, args: patched });
45
+ });
46
+ }
47
+ catch {
48
+ // puppeteer не установлен отдельно — положимся на bpmn-to-image
49
+ }
50
+ }
51
+ async function loadConverter() {
52
+ try {
53
+ const mod = await import('bpmn-to-image');
54
+ await patchPuppeteerForCI();
55
+ return mod;
56
+ }
57
+ catch (err) {
58
+ if (err.code === 'ERR_MODULE_NOT_FOUND') {
59
+ throw new Error("BPMN conversion requires the optional peer dependency 'bpmn-to-image'. " +
60
+ 'Install it with: npm install -D bpmn-to-image');
61
+ }
62
+ throw err;
63
+ }
64
+ }
65
+ /**
66
+ * Конвертирует BPMN-файлы в изображения по явному списку input → output.
67
+ *
68
+ * Рядом с каждым результатом пишется sidecar `<output>.src-sha256` с SHA-256
69
+ * исходного BPMN — AttachmentService использует его как dedup-tag вместо SHA
70
+ * результата (PNG от Chromium байт-нестабилен между запусками, BPMN —
71
+ * стабилен). Так Confluence не плодит лишние версии аттача, когда диаграмма
72
+ * не менялась.
73
+ */
74
+ export async function convertBpmn(conversions) {
75
+ if (conversions.length === 0)
76
+ return;
77
+ const { convertAll } = await loadConverter();
78
+ for (const c of conversions) {
79
+ mkdirSync(dirname(c.output), { recursive: true });
80
+ }
81
+ console.log(`[bpmn] converting ${conversions.length} diagram(s)`);
82
+ await convertAll(conversions.map((c) => ({ input: c.input, outputs: [c.output] })));
83
+ for (const c of conversions) {
84
+ const sha = createHash('sha256').update(readFileSync(c.input)).digest('hex');
85
+ writeFileSync(c.output + SRC_SHA_SIDECAR_SUFFIX, sha);
86
+ }
87
+ }
88
+ /**
89
+ * Конвертирует все BPMN-файлы из `srcDir` в изображения в `outDir`.
90
+ * Файлы получают то же базовое имя с расширением формата (`p1.bpmn` → `p1.png`).
91
+ *
92
+ * Сгенерированные файлы — артефакты сборки: коммитить их не нужно.
93
+ * В Confluence они улетают как аттачи на этапе публикации.
94
+ */
95
+ export async function convertBpmnFolder(opts) {
96
+ const format = opts.format ?? 'png';
97
+ const pattern = opts.pattern ?? BPMN_FILE_RE;
98
+ const files = readdirSync(opts.srcDir).filter((f) => pattern.test(f));
99
+ if (files.length === 0) {
100
+ console.log(`[bpmn] no files matching ${pattern} in ${opts.srcDir}`);
101
+ return [];
102
+ }
103
+ const conversions = files.map((f) => ({
104
+ input: join(opts.srcDir, f),
105
+ output: join(opts.outDir, bpmnOutputName(f, format)),
106
+ }));
107
+ await convertBpmn(conversions);
108
+ return conversions;
109
+ }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export { ConfluenceClient, ConfluenceApiError, type ConfluencePage, type ConfluencePageStorage, type ConfluenceAttachment, type ConfluenceLabel, type AttachmentVersionData, type CreatePageOptions, } from './client/client.js';
2
2
  export { authHeader, loadConfigFromEnv, type ConfluenceAuthType, type ConfluenceConfig, type LoadConfigOptions, } from './client/config.js';
3
3
  export { Markdown } from './markdown/markdown.js';
4
- export { renderToStorage, extractPlaceholders, MissingAttachmentUrlError, PLACEHOLDER_RE, type AttachmentUrls, type ExtractedPlaceholders, } from './markdown/render.js';
4
+ export { renderToStorage, extractPlaceholders, renameImagePlaceholders, MissingAttachmentUrlError, PLACEHOLDER_RE, type AttachmentUrls, type ExtractedPlaceholders, } from './markdown/render.js';
5
5
  export { validateMarkdown, MarkdownValidationError, type ValidateOptions } from './markdown/validate.js';
6
6
  export * from './macros/index.js';
7
+ export { convertBpmn, convertBpmnFolder, isBpmnFile, bpmnOutputName, BPMN_FILE_RE, type BpmnConversion, type BpmnImageFormat, type ConvertBpmnFolderOptions, } from './bpmn/convert.js';
7
8
  export { fileSha256, HASH_TAG_PREFIX } from './attachments/hash.js';
8
9
  export { Attachment, AttachmentService, toAttachmentVersion, SRC_SHA_SIDECAR_SUFFIX, type AttachmentVersion, type EnsuredAttachment, } from './attachments/attachment.js';
9
10
  export { Page, Table } from './pages/page.js';
package/dist/index.js CHANGED
@@ -3,10 +3,12 @@ export { ConfluenceClient, ConfluenceApiError, } from './client/client.js';
3
3
  export { authHeader, loadConfigFromEnv, } from './client/config.js';
4
4
  // Markdown
5
5
  export { Markdown } from './markdown/markdown.js';
6
- export { renderToStorage, extractPlaceholders, MissingAttachmentUrlError, PLACEHOLDER_RE, } from './markdown/render.js';
6
+ export { renderToStorage, extractPlaceholders, renameImagePlaceholders, MissingAttachmentUrlError, PLACEHOLDER_RE, } from './markdown/render.js';
7
7
  export { validateMarkdown, MarkdownValidationError } from './markdown/validate.js';
8
8
  // Macros (pluggable)
9
9
  export * from './macros/index.js';
10
+ // BPMN (optional peer dep 'bpmn-to-image' is loaded lazily on use)
11
+ export { convertBpmn, convertBpmnFolder, isBpmnFile, bpmnOutputName, BPMN_FILE_RE, } from './bpmn/convert.js';
10
12
  // Attachments
11
13
  export { fileSha256, HASH_TAG_PREFIX } from './attachments/hash.js';
12
14
  export { Attachment, AttachmentService, toAttachmentVersion, SRC_SHA_SIDECAR_SUFFIX, } from './attachments/attachment.js';
@@ -4,6 +4,12 @@ export interface ExtractedPlaceholders {
4
4
  files: string[];
5
5
  }
6
6
  export declare function extractPlaceholders(markdown: string): ExtractedPlaceholders;
7
+ /**
8
+ * Переименовывает `{{img:...}}`-плейсхолдеры по карте старое→новое имя.
9
+ * Используется BPMN-конвейером: автор пишет `{{img:p1.bpmn}}`, публикация
10
+ * подменяет на `{{img:p1.png}}` после конвертации диаграммы.
11
+ */
12
+ export declare function renameImagePlaceholders(markdown: string, renames: Map<string, string>): string;
7
13
  export interface AttachmentUrls {
8
14
  /** filename → абсолютный URL аттача, полученный из Confluence после аплоада. */
9
15
  images: Map<string, string>;
@@ -34,6 +34,19 @@ function escapeXmlAttr(s) {
34
34
  }
35
35
  });
36
36
  }
37
+ /**
38
+ * Переименовывает `{{img:...}}`-плейсхолдеры по карте старое→новое имя.
39
+ * Используется BPMN-конвейером: автор пишет `{{img:p1.bpmn}}`, публикация
40
+ * подменяет на `{{img:p1.png}}` после конвертации диаграммы.
41
+ */
42
+ export function renameImagePlaceholders(markdown, renames) {
43
+ if (renames.size === 0)
44
+ return markdown;
45
+ return markdown.replace(/\{\{img:([^}]+)\}\}/g, (full, rawName) => {
46
+ const renamed = renames.get(rawName.trim());
47
+ return renamed ? `{{img:${renamed}}}` : full;
48
+ });
49
+ }
37
50
  export class MissingAttachmentUrlError extends Error {
38
51
  constructor(message) {
39
52
  super(message);
@@ -37,6 +37,12 @@ export interface PublishPageOptions {
37
37
  versionMessage?: string;
38
38
  /** Свой реестр макросов (default: встроенные core + table-filter). */
39
39
  registry?: MacroRegistry;
40
+ /**
41
+ * Каталог для PNG, сгенерированных из `*.bpmn` в images[].
42
+ * Default: временный каталог на каждый запуск — дедуп аттачей всё равно
43
+ * идёт по SHA-256 исходного BPMN через sidecar, лишних версий не будет.
44
+ */
45
+ bpmnOutDir?: string;
40
46
  /**
41
47
  * Ключ content property для хранения content-hash.
42
48
  * Default: 'confluence-md-sync-content-hash'.
@@ -1,8 +1,11 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { mkdtempSync, readFileSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
+ import { tmpdir } from 'node:os';
4
+ import { basename, join } from 'node:path';
3
5
  import { ConfluenceClient } from '../client/client.js';
4
6
  import { AttachmentService } from '../attachments/attachment.js';
5
- import { renderToStorage } from '../markdown/render.js';
7
+ import { bpmnOutputName, convertBpmn, isBpmnFile } from '../bpmn/convert.js';
8
+ import { renameImagePlaceholders, renderToStorage } from '../markdown/render.js';
6
9
  import { validateMarkdown } from '../markdown/validate.js';
7
10
  import { processMacros } from '../macros/registry.js';
8
11
  import { defaultMacroRegistry } from '../macros/index.js';
@@ -17,14 +20,26 @@ import { Markdown } from '../markdown/markdown.js';
17
20
  export const DEFAULT_HASH_PROPERTY_KEY = 'confluence-md-sync-content-hash';
18
21
  export function computeContentHash(storage) {
19
22
  // Перед хешированием вычищаем query (?version=N&modificationDate=…) из
20
- // attachment download-URL'ов. Иначе любой ребамп аттача (новая версия
21
- // от не-детерминированной генерации картинок новый SHA новый
22
- // version N в URL) ломал бы hash страницы, хотя тело логически
23
- // не изменилось — Confluence сам отдаст последнюю версию аттача по
24
- // base-URL без query.
23
+ // attachment download-URL'ов защита для storage, писанного схемой 1
24
+ // (см. HASH_SCHEME): там в body лежали URL с пином версии. Начиная со
25
+ // схемы 2 в body подставляются канонические URL без query, и replace —
26
+ // no-op.
25
27
  const canonical = storage.replace(/(\/download\/attachments\/[^"?\s]+)\?[^"\s]*/g, '$1');
26
28
  return createHash('sha256').update(canonical, 'utf-8').digest('hex');
27
29
  }
30
+ // Схема записи download-URL в body. Схема 1 подставляла URL из
31
+ // _links.download как есть — с ?version=N&modificationDate=…; при ребампе
32
+ // аттача без изменения текста страница оставалась UNCHANGED и продолжала
33
+ // отдавать старую, пиненную версию картинки. Схема 2 подставляет
34
+ // канонический URL без query — Confluence по нему отдаёт последнюю версию
35
+ // аттача, и обновление диаграммы видно без переписывания body. Property со
36
+ // схемой ≠ текущей (в т.ч. без поля scheme) считается устаревшей — страница
37
+ // один раз переписывается каноническими URL.
38
+ const HASH_SCHEME = 2;
39
+ /** Канонический download-URL аттача: без query (?version=N&…). */
40
+ function canonicalDownloadUrl(url) {
41
+ return url.split('?')[0];
42
+ }
28
43
  async function resolvePage(client, opts) {
29
44
  if (opts.pageId)
30
45
  return { pageId: opts.pageId, created: false };
@@ -60,11 +75,29 @@ export async function publishPage(opts, cfg) {
60
75
  else {
61
76
  throw new Error('publishPage: either markdownPath or markdown must be provided');
62
77
  }
63
- const images = opts.images ?? [];
78
+ let images = opts.images ?? [];
64
79
  const files = opts.files ?? [];
65
80
  const tables = opts.tables ?? [];
66
81
  const registry = opts.registry ?? defaultMacroRegistry;
67
82
  const hashKey = opts.hashPropertyKey ?? DEFAULT_HASH_PROPERTY_KEY;
83
+ // 0. BPMN из коробки: *.bpmn в images[] конвертируются в PNG, а
84
+ // {{img:p1.bpmn}} в markdown подменяется на {{img:p1.png}} ДО валидации.
85
+ // Сама конвертация (puppeteer) — после валидации, чтобы падать быстро.
86
+ const bpmnConversions = [];
87
+ const bpmnInputs = images.filter(isBpmnFile);
88
+ if (bpmnInputs.length > 0) {
89
+ const outDir = opts.bpmnOutDir ?? mkdtempSync(join(tmpdir(), 'confluence-md-sync-bpmn-'));
90
+ const renames = new Map();
91
+ const outputByInput = new Map();
92
+ for (const input of bpmnInputs) {
93
+ const output = join(outDir, bpmnOutputName(input));
94
+ bpmnConversions.push({ input, output });
95
+ outputByInput.set(input, output);
96
+ renames.set(basename(input), basename(output));
97
+ }
98
+ images = images.map((p) => outputByInput.get(p) ?? p);
99
+ markdown = renameImagePlaceholders(markdown, renames);
100
+ }
68
101
  validateMarkdown({
69
102
  markdown,
70
103
  imagePaths: images,
@@ -77,6 +110,11 @@ export async function publishPage(opts, cfg) {
77
110
  const tableMarkdown = table.markdown instanceof Markdown ? table.markdown.toString() : table.markdown;
78
111
  markdown = markdown.replace(new RegExp(`\\{\\{table:${escapeRegex(table.name)}\\}\\}`, 'g'), () => tableMarkdown);
79
112
  }
113
+ // Конвертация диаграмм — до аплоада; в dry-run пропускается (аттачи всё
114
+ // равно не загружаются, URL подставляются фиктивные).
115
+ if (bpmnConversions.length > 0 && !opts.dryRun) {
116
+ await convertBpmn(bpmnConversions);
117
+ }
80
118
  const client = new ConfluenceClient(cfg);
81
119
  const attachmentSvc = new AttachmentService(client);
82
120
  const { pageId, created } = await resolvePage(client, opts);
@@ -89,20 +127,23 @@ export async function publishPage(opts, cfg) {
89
127
  // В dry-run не трогаем Confluence — подставляем фиктивные URL, чтобы
90
128
  // рендер и валидация плейсхолдеров отработали полностью.
91
129
  for (const p of images)
92
- urls.images.set(basenameOf(p), `dry-run://img/${basenameOf(p)}`);
130
+ urls.images.set(basename(p), `dry-run://img/${basename(p)}`);
93
131
  for (const p of files)
94
- urls.files.set(basenameOf(p), `dry-run://file/${basenameOf(p)}`);
132
+ urls.files.set(basename(p), `dry-run://file/${basename(p)}`);
95
133
  }
96
134
  else {
135
+ // В body уходит канонический URL без query (см. HASH_SCHEME) — страница
136
+ // всегда отдаёт последнюю версию аттача. Полный URL с версией остаётся
137
+ // в результате для caller'а.
97
138
  for (const p of images) {
98
139
  const r = await attachmentSvc.ensure(pageId, p);
99
- urls.images.set(r.filename, r.downloadUrl);
140
+ urls.images.set(r.filename, canonicalDownloadUrl(r.downloadUrl));
100
141
  attachments.push({ filename: r.filename, id: r.id, reused: r.reused, url: r.downloadUrl });
101
142
  console.log(`[attachment] ${r.filename}: ${r.reused ? 'reused' : 'uploaded'} (id=${r.id})`);
102
143
  }
103
144
  for (const p of files) {
104
145
  const r = await attachmentSvc.ensure(pageId, p);
105
- urls.files.set(r.filename, r.downloadUrl);
146
+ urls.files.set(r.filename, canonicalDownloadUrl(r.downloadUrl));
106
147
  attachments.push({ filename: r.filename, id: r.id, reused: r.reused, url: r.downloadUrl });
107
148
  console.log(`[attachment] ${r.filename}: ${r.reused ? 'reused' : 'uploaded'} (id=${r.id})`);
108
149
  }
@@ -130,8 +171,14 @@ export async function publishPage(opts, cfg) {
130
171
  client.getContentProperty(pageId, hashKey),
131
172
  ]);
132
173
  const title = opts.title ?? existing.title;
133
- const existingHash = hashProp && typeof hashProp.value === 'object' && hashProp.value !== null
134
- ? hashProp.value.hash ?? null
174
+ // Property, писанная другой схемой (или до появления scheme), не считается
175
+ // совпадением: body мог быть записан с пином версий аттачей — его нужно
176
+ // один раз переписать каноническими URL.
177
+ const propValue = hashProp && typeof hashProp.value === 'object' && hashProp.value !== null
178
+ ? hashProp.value
179
+ : null;
180
+ const existingHash = propValue && propValue.scheme === HASH_SCHEME
181
+ ? (propValue.hash ?? null)
135
182
  : null;
136
183
  if (existingHash === newHash && title === existing.title) {
137
184
  console.log(`[publish] ${pageId} "${title}" → UNCHANGED (hash ${newHash.slice(0, 12)}, v${existing.version})`);
@@ -149,16 +196,12 @@ export async function publishPage(opts, cfg) {
149
196
  });
150
197
  // 5. Запись/обновление content property с новым hash. Делаем ПОСЛЕ
151
198
  // updatePage чтобы при сбое publish hash не «опередил» реальное содержимое.
152
- await client.setContentProperty(pageId, hashKey, { hash: newHash }, hashProp ? hashProp.version : null);
199
+ await client.setContentProperty(pageId, hashKey, { hash: newHash, scheme: HASH_SCHEME }, hashProp ? hashProp.version : null);
153
200
  if (opts.labels?.length)
154
201
  await client.addLabels(pageId, opts.labels);
155
202
  console.log(`[publish] ${pageId} "${title}" → v${nextVersion} (hash ${newHash.slice(0, 12)})`);
156
203
  return { pageId, title, version: nextVersion, attachments, updated: true, created, storage };
157
204
  }
158
- function basenameOf(p) {
159
- const idx = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'));
160
- return idx === -1 ? p : p.slice(idx + 1);
161
- }
162
205
  function escapeRegex(s) {
163
206
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
164
207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-md-sync",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Publish Markdown to Confluence (Data Center & Cloud): idempotent page sync, attachment dedup, tables and a pluggable macro system",
5
5
  "keywords": [
6
6
  "confluence",
@@ -63,7 +63,20 @@
63
63
  "devDependencies": {
64
64
  "@types/markdown-it": "^14.1.2",
65
65
  "@types/node": "^20.14.0",
66
+ "bpmn-to-image": "^0.7.0",
67
+ "puppeteer": "^24.43.1",
66
68
  "typescript": "^5.6.0",
67
69
  "vitest": "^3.0.0"
70
+ },
71
+ "peerDependencies": {
72
+ "bpmn-to-image": ">=0.7.0"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "bpmn-to-image": {
76
+ "optional": true
77
+ }
78
+ },
79
+ "overrides": {
80
+ "puppeteer": "$puppeteer"
68
81
  }
69
82
  }