confluence-md-sync 0.2.0 → 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 +26 -12
- package/dist/publish/publish.js +31 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,10 +48,11 @@ Markdown + three placeholder kinds:
|
|
|
48
48
|
```markdown
|
|
49
49
|
# Отчёт за месяц
|
|
50
50
|
|
|
51
|
-
{{img:
|
|
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}}
|
|
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: [
|
|
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',
|
|
@@ -99,19 +103,29 @@ await publishPage({ pageId, markdown: '# Generated\n\ntext' }, cfg);
|
|
|
99
103
|
|
|
100
104
|
Pass a `.bpmn` file as an image — it is rendered to PNG at publish time
|
|
101
105
|
(headless Chromium via [bpmn-to-image](https://npmjs.com/package/bpmn-to-image),
|
|
102
|
-
an optional peer dependency)
|
|
106
|
+
an optional peer dependency).
|
|
107
|
+
|
|
108
|
+
Setup — install the converter **and** force a current puppeteer:
|
|
103
109
|
|
|
104
110
|
```bash
|
|
105
|
-
npm install -D bpmn-to-image
|
|
111
|
+
npm install -D bpmn-to-image puppeteer
|
|
106
112
|
```
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
```
|
|
115
129
|
|
|
116
130
|
```markdown
|
|
117
131
|
Процесс выпуска релиза:
|
package/dist/publish/publish.js
CHANGED
|
@@ -20,14 +20,26 @@ import { Markdown } from '../markdown/markdown.js';
|
|
|
20
20
|
export const DEFAULT_HASH_PROPERTY_KEY = 'confluence-md-sync-content-hash';
|
|
21
21
|
export function computeContentHash(storage) {
|
|
22
22
|
// Перед хешированием вычищаем query (?version=N&modificationDate=…) из
|
|
23
|
-
// attachment download-URL'
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// base-URL без query.
|
|
23
|
+
// attachment download-URL'ов — защита для storage, писанного схемой 1
|
|
24
|
+
// (см. HASH_SCHEME): там в body лежали URL с пином версии. Начиная со
|
|
25
|
+
// схемы 2 в body подставляются канонические URL без query, и replace —
|
|
26
|
+
// no-op.
|
|
28
27
|
const canonical = storage.replace(/(\/download\/attachments\/[^"?\s]+)\?[^"\s]*/g, '$1');
|
|
29
28
|
return createHash('sha256').update(canonical, 'utf-8').digest('hex');
|
|
30
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
|
+
}
|
|
31
43
|
async function resolvePage(client, opts) {
|
|
32
44
|
if (opts.pageId)
|
|
33
45
|
return { pageId: opts.pageId, created: false };
|
|
@@ -120,15 +132,18 @@ export async function publishPage(opts, cfg) {
|
|
|
120
132
|
urls.files.set(basename(p), `dry-run://file/${basename(p)}`);
|
|
121
133
|
}
|
|
122
134
|
else {
|
|
135
|
+
// В body уходит канонический URL без query (см. HASH_SCHEME) — страница
|
|
136
|
+
// всегда отдаёт последнюю версию аттача. Полный URL с версией остаётся
|
|
137
|
+
// в результате для caller'а.
|
|
123
138
|
for (const p of images) {
|
|
124
139
|
const r = await attachmentSvc.ensure(pageId, p);
|
|
125
|
-
urls.images.set(r.filename, r.downloadUrl);
|
|
140
|
+
urls.images.set(r.filename, canonicalDownloadUrl(r.downloadUrl));
|
|
126
141
|
attachments.push({ filename: r.filename, id: r.id, reused: r.reused, url: r.downloadUrl });
|
|
127
142
|
console.log(`[attachment] ${r.filename}: ${r.reused ? 'reused' : 'uploaded'} (id=${r.id})`);
|
|
128
143
|
}
|
|
129
144
|
for (const p of files) {
|
|
130
145
|
const r = await attachmentSvc.ensure(pageId, p);
|
|
131
|
-
urls.files.set(r.filename, r.downloadUrl);
|
|
146
|
+
urls.files.set(r.filename, canonicalDownloadUrl(r.downloadUrl));
|
|
132
147
|
attachments.push({ filename: r.filename, id: r.id, reused: r.reused, url: r.downloadUrl });
|
|
133
148
|
console.log(`[attachment] ${r.filename}: ${r.reused ? 'reused' : 'uploaded'} (id=${r.id})`);
|
|
134
149
|
}
|
|
@@ -156,8 +171,14 @@ export async function publishPage(opts, cfg) {
|
|
|
156
171
|
client.getContentProperty(pageId, hashKey),
|
|
157
172
|
]);
|
|
158
173
|
const title = opts.title ?? existing.title;
|
|
159
|
-
|
|
160
|
-
|
|
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)
|
|
161
182
|
: null;
|
|
162
183
|
if (existingHash === newHash && title === existing.title) {
|
|
163
184
|
console.log(`[publish] ${pageId} "${title}" → UNCHANGED (hash ${newHash.slice(0, 12)}, v${existing.version})`);
|
|
@@ -175,7 +196,7 @@ export async function publishPage(opts, cfg) {
|
|
|
175
196
|
});
|
|
176
197
|
// 5. Запись/обновление content property с новым hash. Делаем ПОСЛЕ
|
|
177
198
|
// updatePage чтобы при сбое publish hash не «опередил» реальное содержимое.
|
|
178
|
-
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);
|
|
179
200
|
if (opts.labels?.length)
|
|
180
201
|
await client.addLabels(pageId, opts.labels);
|
|
181
202
|
console.log(`[publish] ${pageId} "${title}" → v${nextVersion} (hash ${newHash.slice(0, 12)})`);
|
package/package.json
CHANGED