@iwonz/ywtc 4.0.0 → 4.0.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/CHANGELOG.md +13 -0
- package/README.md +11 -9
- package/dist/adapters/confluence.js +19 -4
- package/dist/adapters/confluence.js.map +1 -1
- package/dist/adapters/yandex.d.ts +7 -8
- package/dist/adapters/yandex.js +68 -22
- package/dist/adapters/yandex.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/clients/yandex-wiki.js +2 -6
- package/dist/clients/yandex-wiki.js.map +1 -1
- package/dist/consistency.d.ts +1 -1
- package/dist/consistency.js +6 -1
- package/dist/consistency.js.map +1 -1
- package/dist/content-model.d.ts +1 -0
- package/dist/content-model.js +118 -37
- package/dist/content-model.js.map +1 -1
- package/dist/conversion.d.ts +2 -0
- package/dist/conversion.js +42 -17
- package/dist/conversion.js.map +1 -1
- package/dist/domain.d.ts +22 -2
- package/dist/domain.js +10 -0
- package/dist/domain.js.map +1 -1
- package/dist/exclusive-lease.d.ts +14 -0
- package/dist/exclusive-lease.js +87 -0
- package/dist/exclusive-lease.js.map +1 -0
- package/dist/filesystem-export.d.ts +2 -0
- package/dist/filesystem-export.js +188 -167
- package/dist/filesystem-export.js.map +1 -1
- package/dist/graph.js +4 -4
- package/dist/graph.js.map +1 -1
- package/dist/http.js +1 -1
- package/dist/http.js.map +1 -1
- package/dist/interactive-config.js +55 -3
- package/dist/interactive-config.js.map +1 -1
- package/dist/issues.d.ts +1 -1
- package/dist/migrator.js +439 -311
- package/dist/migrator.js.map +1 -1
- package/dist/report-catalog.d.ts +4 -0
- package/dist/report-catalog.js +4 -0
- package/dist/report-catalog.js.map +1 -1
- package/dist/report-contract.d.ts +6 -0
- package/dist/report-contract.js.map +1 -1
- package/dist/report-ui/index.html +3 -3
- package/dist/report.js +33 -22
- package/dist/report.js.map +1 -1
- package/dist/state.d.ts +16 -0
- package/dist/state.js +58 -32
- package/dist/state.js.map +1 -1
- package/package.json +1 -1
package/dist/domain.d.ts
CHANGED
|
@@ -82,6 +82,12 @@ export declare class DestinationRevisionConflictError extends Error {
|
|
|
82
82
|
readonly objectId: string;
|
|
83
83
|
constructor(objectType: 'page' | 'comment', objectId: string, options?: ErrorOptions);
|
|
84
84
|
}
|
|
85
|
+
export type OwnershipConflictReason = 'duplicate-source-mapping' | 'immutable-id-mismatch' | 'ambiguous-ownership-candidates';
|
|
86
|
+
export declare class DestinationOwnershipConflictError extends Error {
|
|
87
|
+
readonly reason: OwnershipConflictReason;
|
|
88
|
+
readonly candidateIds: string[];
|
|
89
|
+
constructor(reason: OwnershipConflictReason, candidateIds: string[], message: string, options?: ErrorOptions);
|
|
90
|
+
}
|
|
85
91
|
export interface UploadedResource {
|
|
86
92
|
id: string;
|
|
87
93
|
name: string;
|
|
@@ -175,7 +181,7 @@ export interface SourceStatusExclusion {
|
|
|
175
181
|
depth?: number;
|
|
176
182
|
discoveredFromId?: string;
|
|
177
183
|
metadataComplete?: boolean;
|
|
178
|
-
reason: 'source-status';
|
|
184
|
+
reason: 'source-status' | 'inventory-drift';
|
|
179
185
|
status?: string;
|
|
180
186
|
}
|
|
181
187
|
export interface DescendantDiscovery {
|
|
@@ -289,7 +295,7 @@ export interface HierarchyGap {
|
|
|
289
295
|
pageId: string;
|
|
290
296
|
missingParentId?: string;
|
|
291
297
|
selectedParentId: string;
|
|
292
|
-
reason: 'unavailable' | 'source-status';
|
|
298
|
+
reason: 'unavailable' | 'source-status' | 'inventory-drift';
|
|
293
299
|
}
|
|
294
300
|
export interface SourceSelection {
|
|
295
301
|
requestedStatus: SourceStatus;
|
|
@@ -335,6 +341,14 @@ export interface NormalizedNode {
|
|
|
335
341
|
sectionType?: 'single' | 'two_equal' | 'two_left_sidebar' | 'two_right_sidebar' | 'three_equal' | 'three_with_sidebars' | 'unsupported';
|
|
336
342
|
};
|
|
337
343
|
}
|
|
344
|
+
export type TransformationNoticeKind = 'normalization' | 'adaptation' | 'degradation';
|
|
345
|
+
export interface TransformationNotice {
|
|
346
|
+
code: string;
|
|
347
|
+
kind: TransformationNoticeKind;
|
|
348
|
+
message: string;
|
|
349
|
+
objectId?: string;
|
|
350
|
+
verification?: string;
|
|
351
|
+
}
|
|
338
352
|
export interface NormalizedDocument {
|
|
339
353
|
root: NormalizedNode;
|
|
340
354
|
raw: string;
|
|
@@ -342,6 +356,8 @@ export interface NormalizedDocument {
|
|
|
342
356
|
references: NormalizedReference[];
|
|
343
357
|
warnings: string[];
|
|
344
358
|
normalizations: string[];
|
|
359
|
+
adaptations: string[];
|
|
360
|
+
notices: TransformationNotice[];
|
|
345
361
|
degraded: boolean;
|
|
346
362
|
semanticallyEmpty: boolean;
|
|
347
363
|
metrics: {
|
|
@@ -349,6 +365,7 @@ export interface NormalizedDocument {
|
|
|
349
365
|
images: number;
|
|
350
366
|
tables: number;
|
|
351
367
|
codeBlocks: number;
|
|
368
|
+
preservedExternalReferences: number;
|
|
352
369
|
};
|
|
353
370
|
}
|
|
354
371
|
export interface RenderContext {
|
|
@@ -365,7 +382,10 @@ export interface RenderedDocument {
|
|
|
365
382
|
body: string;
|
|
366
383
|
warnings: string[];
|
|
367
384
|
normalizations: string[];
|
|
385
|
+
adaptations: string[];
|
|
386
|
+
notices: TransformationNotice[];
|
|
368
387
|
degraded: boolean;
|
|
369
388
|
representedReferences: number;
|
|
389
|
+
preservedExternalReferences: number;
|
|
370
390
|
}
|
|
371
391
|
export declare function assertTransferCapabilities(source: SourceAdapter, destination: DestinationAdapter | undefined, scope: TransferScope, sourceStatus?: SourceStatus, comments?: boolean): void;
|
package/dist/domain.js
CHANGED
|
@@ -18,6 +18,16 @@ export class DestinationRevisionConflictError extends Error {
|
|
|
18
18
|
this.name = 'DestinationRevisionConflictError';
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
export class DestinationOwnershipConflictError extends Error {
|
|
22
|
+
reason;
|
|
23
|
+
candidateIds;
|
|
24
|
+
constructor(reason, candidateIds, message, options) {
|
|
25
|
+
super(message, options);
|
|
26
|
+
this.reason = reason;
|
|
27
|
+
this.candidateIds = candidateIds;
|
|
28
|
+
this.name = 'DestinationOwnershipConflictError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
21
31
|
export function assertTransferCapabilities(source, destination, scope, sourceStatus = 'all', comments = true) {
|
|
22
32
|
const missing = [];
|
|
23
33
|
if (!source.capabilities.readPages)
|
package/dist/domain.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.js","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"AAiFA,MAAM,OAAO,6BAA8B,SAAQ,KAAK;IAE3C;IACA;IAFX,YACW,KAAa,EACb,MAAc,EACvB,OAAsB;QAEtB,KAAK,CAAC,+CAA+C,KAAK,GAAG,EAAE,OAAO,CAAC,CAAC;QAJ/D,UAAK,GAAL,KAAK,CAAQ;QACb,WAAM,GAAN,MAAM,CAAQ;QAIvB,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAAC;IAC9C,CAAC;CACF;AAED,MAAM,OAAO,gCAAiC,SAAQ,KAAK;IAE9C;IACA;IAFX,YACW,UAA8B,EAC9B,QAAgB,EACzB,OAAsB;QAEtB,KAAK,CAAC,eAAe,UAAU,IAAI,QAAQ,uBAAuB,EAAE,OAAO,CAAC,CAAC;QAJpE,eAAU,GAAV,UAAU,CAAoB;QAC9B,aAAQ,GAAR,QAAQ,CAAQ;QAIzB,IAAI,CAAC,IAAI,GAAG,kCAAkC,CAAC;IACjD,CAAC;CACF;
|
|
1
|
+
{"version":3,"file":"domain.js","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface LeaseMetadata {
|
|
2
|
+
pid: number;
|
|
3
|
+
hostname: string;
|
|
4
|
+
startedAt: string;
|
|
5
|
+
purpose: string;
|
|
6
|
+
target: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ExclusiveLease {
|
|
9
|
+
filename: string;
|
|
10
|
+
metadata: LeaseMetadata;
|
|
11
|
+
release(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function acquireExclusiveLease(filename: string, purpose: string, target: string): Promise<ExclusiveLease>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
import { mkdir, open, readFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function localProcessIsAlive(pid) {
|
|
5
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
6
|
+
return false;
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
return error.code === 'EPERM';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function existingLease(filename) {
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(await readFile(filename, 'utf8'));
|
|
18
|
+
if (typeof value.pid !== 'number'
|
|
19
|
+
|| typeof value.hostname !== 'string'
|
|
20
|
+
|| typeof value.startedAt !== 'string'
|
|
21
|
+
|| typeof value.purpose !== 'string'
|
|
22
|
+
|| typeof value.target !== 'string')
|
|
23
|
+
return undefined;
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function isProvablyStale(metadata) {
|
|
31
|
+
return metadata?.hostname === hostname() && !localProcessIsAlive(metadata.pid);
|
|
32
|
+
}
|
|
33
|
+
export async function acquireExclusiveLease(filename, purpose, target) {
|
|
34
|
+
await mkdir(path.dirname(filename), { recursive: true, mode: 0o700 });
|
|
35
|
+
const metadata = { pid: process.pid, hostname: hostname(), startedAt: new Date().toISOString(), purpose, target };
|
|
36
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
37
|
+
let created = false;
|
|
38
|
+
try {
|
|
39
|
+
const handle = await open(filename, 'wx', 0o600);
|
|
40
|
+
created = true;
|
|
41
|
+
try {
|
|
42
|
+
await handle.writeFile(`${JSON.stringify(metadata, null, 2)}\n`);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await handle.close();
|
|
46
|
+
}
|
|
47
|
+
let released = false;
|
|
48
|
+
return {
|
|
49
|
+
filename,
|
|
50
|
+
metadata,
|
|
51
|
+
async release() {
|
|
52
|
+
if (released)
|
|
53
|
+
return;
|
|
54
|
+
released = true;
|
|
55
|
+
const current = await existingLease(filename);
|
|
56
|
+
if (current?.pid === metadata.pid && current.hostname === metadata.hostname && current.startedAt === metadata.startedAt) {
|
|
57
|
+
await unlink(filename).catch((error) => {
|
|
58
|
+
if (error.code !== 'ENOENT')
|
|
59
|
+
throw error;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error.code !== 'EEXIST') {
|
|
67
|
+
if (created)
|
|
68
|
+
await unlink(filename).catch(() => undefined);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
const current = await existingLease(filename);
|
|
72
|
+
if (attempt === 0 && isProvablyStale(current)) {
|
|
73
|
+
await unlink(filename).catch((unlinkError) => {
|
|
74
|
+
if (unlinkError.code !== 'ENOENT')
|
|
75
|
+
throw unlinkError;
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const detail = current
|
|
80
|
+
? `PID ${current.pid} on ${current.hostname}, started ${current.startedAt}`
|
|
81
|
+
: 'the lock metadata is unreadable and cannot be proven active';
|
|
82
|
+
throw new Error(`Another ${purpose} is active for "${target}" (${detail}). Wait for it to finish or remove the lock only after proving that process is no longer running.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`Could not acquire ${purpose} lock for "${target}"`);
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=exclusive-lease.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exclusive-lease.js","sourceRoot":"","sources":["../src/exclusive-lease.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { createWriteStream } from 'node:fs';
|
|
3
|
-
import { access, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { access, mkdir, readFile, readdir, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { pipeline } from 'node:stream/promises';
|
|
6
6
|
import { ZipArchive } from '@archiver/archiver';
|
|
@@ -9,6 +9,7 @@ import { z } from 'zod';
|
|
|
9
9
|
import { appendGridSnapshots, normalizePageContent, renderMarkdown, renderStaticHtml, setSourceResourceBinding, sourceResourceBindings } from './content-model.js';
|
|
10
10
|
import { commentAttachmentUses, createCommentArchiveZip, renderCommentArchive } from './comments.js';
|
|
11
11
|
import { DestinationNameAllocator } from './resource-reconciliation.js';
|
|
12
|
+
import { acquireExclusiveLease } from './exclusive-lease.js';
|
|
12
13
|
const manifestPageSchema = z.object({
|
|
13
14
|
sourceCanonicalId: z.string(),
|
|
14
15
|
sourceUrl: z.string(),
|
|
@@ -23,6 +24,8 @@ const manifestPageSchema = z.object({
|
|
|
23
24
|
}).strict()),
|
|
24
25
|
degraded: z.boolean(),
|
|
25
26
|
warnings: z.array(z.string()),
|
|
27
|
+
adaptations: z.array(z.string()),
|
|
28
|
+
preservedExternalReferences: z.number().int().nonnegative(),
|
|
26
29
|
comments: z.object({
|
|
27
30
|
status: z.enum(['planned', 'complete', 'partial', 'failed', 'conflict', 'stale', 'skipped']),
|
|
28
31
|
threads: z.number().int().nonnegative(),
|
|
@@ -155,6 +158,8 @@ export class FilesystemExporter {
|
|
|
155
158
|
resources: [],
|
|
156
159
|
degraded: false,
|
|
157
160
|
warnings: [],
|
|
161
|
+
adaptations: [],
|
|
162
|
+
preservedExternalReferences: 0,
|
|
158
163
|
comments: {
|
|
159
164
|
status: input.commentsEnabled === false ? 'skipped' : 'planned',
|
|
160
165
|
threads: input.commentsByPage?.get(page.canonicalId)?.threads.length ?? 0,
|
|
@@ -167,186 +172,202 @@ export class FilesystemExporter {
|
|
|
167
172
|
}));
|
|
168
173
|
return { directory: input.directory, manifest, warnings, errors };
|
|
169
174
|
}
|
|
170
|
-
const
|
|
171
|
-
const staging = path.join(parent, `.${path.basename(input.directory)}.ywtc-staging-${randomUUID().slice(0, 8)}`);
|
|
172
|
-
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
173
|
-
await mkdir(staging, { recursive: false, mode: 0o700 });
|
|
174
|
-
let backup;
|
|
175
|
+
const lease = await acquireExclusiveLease(`${input.directory}.ywtc.lock`, 'ywtc filesystem export', input.directory);
|
|
175
176
|
try {
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
resourcesByPage
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
177
|
+
const parent = path.dirname(input.directory);
|
|
178
|
+
const staging = path.join(parent, `.${path.basename(input.directory)}.ywtc-staging-${randomUUID().slice(0, 8)}`);
|
|
179
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
180
|
+
await mkdir(staging, { recursive: false, mode: 0o700 });
|
|
181
|
+
let backup;
|
|
182
|
+
try {
|
|
183
|
+
const sourcePages = new Map(input.graph.pages.map((page) => [page.canonicalId, page]));
|
|
184
|
+
const queue = new PQueue({ concurrency: this.concurrency });
|
|
185
|
+
const resourceMapByPage = new Map();
|
|
186
|
+
const resourcesByPage = new Map();
|
|
187
|
+
const commentBodies = new Map();
|
|
188
|
+
await Promise.all(input.graph.pages.map((page) => queue.add(async () => {
|
|
189
|
+
if (input.signal?.aborted)
|
|
190
|
+
throw input.signal.reason ?? new Error('Export cancelled');
|
|
191
|
+
const resources = input.resourcesByPage?.get(page.canonicalId) ?? await this.source.listResources(page);
|
|
192
|
+
resourcesByPage.set(page.canonicalId, resources);
|
|
193
|
+
const pageResources = sourceResourceBindings(page, resources);
|
|
194
|
+
const nameAllocator = new DestinationNameAllocator();
|
|
195
|
+
const manifestResources = [];
|
|
196
|
+
const downloadedByResource = new Map();
|
|
197
|
+
for (const resource of resources.filter((item) => item.kind !== 'grid')) {
|
|
198
|
+
const name = nameAllocator.allocate(resource);
|
|
199
|
+
const relative = `attachments/${stableSegment(page)}/${name}`;
|
|
200
|
+
const destination = path.join(staging, relative);
|
|
201
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
202
|
+
const downloaded = await this.source.downloadResource(resource, destination);
|
|
203
|
+
downloadedByResource.set(resource.id, downloaded);
|
|
204
|
+
setSourceResourceBinding(pageResources, page, resources, resource, { kind: 'file', path: relative, name });
|
|
205
|
+
manifestResources.push({ name, path: relative, size: downloaded.size, sha256: downloaded.sha256 });
|
|
206
|
+
input.onItem?.(`attachment ${name}`);
|
|
207
|
+
}
|
|
208
|
+
resourceMapByPage.set(page.canonicalId, pageResources);
|
|
209
|
+
const inventory = input.commentsByPage?.get(page.canonicalId);
|
|
210
|
+
let comments = {
|
|
211
|
+
status: input.commentsEnabled === false ? 'skipped' : inventory?.status === 'failed' ? 'failed' : 'complete',
|
|
212
|
+
threads: inventory?.threads.length ?? 0,
|
|
213
|
+
comments: inventory?.comments ?? 0,
|
|
214
|
+
participants: inventory?.participants ?? 0,
|
|
215
|
+
attachments: inventory?.attachments ?? 0,
|
|
216
|
+
bytes: inventory?.knownBytes ?? 0,
|
|
217
|
+
...(inventory?.fingerprint ? { fingerprint: inventory.fingerprint } : {}),
|
|
218
|
+
...(inventory?.error ? { error: inventory.error } : {}),
|
|
219
|
+
};
|
|
220
|
+
if (input.commentsEnabled !== false && inventory?.status === 'complete' && inventory.fingerprint) {
|
|
221
|
+
try {
|
|
222
|
+
const resourceById = new Map(resources.map((resource) => [resource.id, resource]));
|
|
223
|
+
const files = commentAttachmentUses(inventory).flatMap((use) => {
|
|
224
|
+
const id = use.reference.resourceId;
|
|
225
|
+
const resource = id ? resourceById.get(id) : undefined;
|
|
226
|
+
const downloaded = id ? downloadedByResource.get(id) : undefined;
|
|
227
|
+
return resource && downloaded ? [{
|
|
228
|
+
commentId: use.commentId,
|
|
229
|
+
reference: use.reference.href,
|
|
230
|
+
originalName: resource.preferredName ?? resource.name,
|
|
231
|
+
...downloaded,
|
|
232
|
+
}] : [];
|
|
233
|
+
});
|
|
234
|
+
const zip = await createCommentArchiveZip({
|
|
235
|
+
pageKey: page.id,
|
|
236
|
+
fingerprint: inventory.fingerprint,
|
|
237
|
+
files,
|
|
238
|
+
destinationDirectory: path.join(staging, 'attachments', stableSegment(page)),
|
|
239
|
+
});
|
|
240
|
+
const archivePath = zip ? path.relative(staging, zip.path).split(path.sep).join('/') : undefined;
|
|
241
|
+
const names = new Map();
|
|
242
|
+
if (zip) {
|
|
243
|
+
const byHash = new Map(zip.files.map((entry) => [entry.sha256, entry.archiveName]));
|
|
244
|
+
for (const [resourceId, downloaded] of downloadedByResource) {
|
|
245
|
+
const name = byHash.get(downloaded.sha256);
|
|
246
|
+
if (name)
|
|
247
|
+
names.set(resourceId, name);
|
|
248
|
+
}
|
|
241
249
|
}
|
|
250
|
+
const pageFile = pagePaths.get(page.canonicalId);
|
|
251
|
+
commentBodies.set(page.canonicalId, renderCommentArchive(inventory, input.format, {
|
|
252
|
+
sourceUrl: page.url,
|
|
253
|
+
...(zip && archivePath ? { archiveName: zip.name, archiveHref: relativeHref(pageFile, archivePath) } : {}),
|
|
254
|
+
attachmentNames: names,
|
|
255
|
+
}));
|
|
256
|
+
comments = {
|
|
257
|
+
...comments,
|
|
258
|
+
status: inventory.warnings.length ? 'partial' : 'complete',
|
|
259
|
+
...(zip && archivePath ? { archiveName: zip.name, archivePath, bytes: zip.size } : {}),
|
|
260
|
+
...(inventory.warnings.length ? { error: inventory.warnings.join('; ') } : {}),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
265
|
+
comments = { ...comments, status: 'failed', error: message };
|
|
242
266
|
}
|
|
267
|
+
}
|
|
268
|
+
manifest.pages.push({
|
|
269
|
+
sourceCanonicalId: page.canonicalId,
|
|
270
|
+
sourceUrl: page.url,
|
|
271
|
+
title: page.title,
|
|
272
|
+
path: pagePaths.get(page.canonicalId),
|
|
273
|
+
...(page.projectedParentId ? { parentId: page.projectedParentId } : {}),
|
|
274
|
+
resources: manifestResources,
|
|
275
|
+
degraded: false,
|
|
276
|
+
warnings: [],
|
|
277
|
+
adaptations: [],
|
|
278
|
+
preservedExternalReferences: 0,
|
|
279
|
+
comments,
|
|
280
|
+
});
|
|
281
|
+
})));
|
|
282
|
+
const children = new Map();
|
|
283
|
+
for (const page of input.graph.pages) {
|
|
284
|
+
const parentId = page.projectedParentId ?? '__root__';
|
|
285
|
+
const siblings = children.get(parentId) ?? [];
|
|
286
|
+
siblings.push(page);
|
|
287
|
+
children.set(parentId, siblings);
|
|
288
|
+
}
|
|
289
|
+
const navigationFor = (currentPath) => {
|
|
290
|
+
const renderItems = (parentId) => `<ul>${(children.get(parentId) ?? []).map((page) => {
|
|
243
291
|
const pageFile = pagePaths.get(page.canonicalId);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
292
|
+
const active = pageFile === currentPath ? ' aria-current="page"' : '';
|
|
293
|
+
const title = page.title.replaceAll('&', '&').replaceAll('<', '<');
|
|
294
|
+
return `<li><a${active} href="${relativeHref(currentPath, pageFile)}">${title}</a>${renderItems(page.canonicalId)}</li>`;
|
|
295
|
+
}).join('')}</ul>`;
|
|
296
|
+
return renderItems('__root__');
|
|
297
|
+
};
|
|
298
|
+
for (const page of input.graph.pages) {
|
|
299
|
+
const fullPage = input.loadPage ? await input.loadPage(page) : page;
|
|
300
|
+
const relative = pagePaths.get(page.canonicalId);
|
|
301
|
+
const resources = resourcesByPage.get(page.canonicalId) ?? [];
|
|
302
|
+
const document = input.loadDocument
|
|
303
|
+
? await input.loadDocument(page)
|
|
304
|
+
: appendGridSnapshots(normalizePageContent(fullPage), resources);
|
|
305
|
+
const pages = new Map([...pagePaths].map(([id, file]) => [id, { path: file, title: sourcePages.get(id)?.title ?? id }]));
|
|
306
|
+
const context = {
|
|
307
|
+
pages,
|
|
308
|
+
resources: resourceMapByPage.get(page.canonicalId) ?? new Map(),
|
|
309
|
+
sourcePages,
|
|
310
|
+
currentPageId: page.canonicalId,
|
|
311
|
+
currentPath: relative,
|
|
312
|
+
};
|
|
313
|
+
const rendered = input.format === 'html' ? await renderStaticHtml(document, context) : await renderMarkdown(document, context);
|
|
314
|
+
const output = input.format === 'html'
|
|
315
|
+
? htmlPage(page.title, navigationFor(relative), `${rendered.body}${commentBodies.get(page.canonicalId) ?? ''}`, relativeHref(relative, 'assets/site.css'))
|
|
316
|
+
: `# ${page.title}\n\n${rendered.body}${commentBodies.has(page.canonicalId) ? `\n\n## Комментарии\n\n${commentBodies.get(page.canonicalId)}` : ''}`;
|
|
317
|
+
await mkdir(path.dirname(path.join(staging, relative)), { recursive: true });
|
|
318
|
+
await writeFile(path.join(staging, relative), output, { mode: 0o600 });
|
|
319
|
+
const manifestPage = manifest.pages.find((item) => item.sourceCanonicalId === page.canonicalId);
|
|
320
|
+
manifestPage.degraded = rendered.notices.some((notice) => notice.kind === 'degradation');
|
|
321
|
+
manifestPage.warnings.push(...rendered.warnings);
|
|
322
|
+
manifestPage.adaptations.push(...rendered.adaptations);
|
|
323
|
+
manifestPage.preservedExternalReferences = rendered.preservedExternalReferences;
|
|
324
|
+
warnings.push(...rendered.warnings.map((warning) => `${page.title}: ${warning}`));
|
|
325
|
+
if (manifestPage.degraded) {
|
|
326
|
+
const snapshot = `_source/${stableSegment(page)}.${page.rawFormat === 'yfm' ? 'yfm' : 'storage.xml'}`;
|
|
327
|
+
await mkdir(path.dirname(path.join(staging, snapshot)), { recursive: true });
|
|
328
|
+
await writeFile(path.join(staging, snapshot), fullPage.rawContent, { mode: 0o600 });
|
|
329
|
+
}
|
|
330
|
+
input.onItem?.(`page ${page.title}`);
|
|
331
|
+
}
|
|
332
|
+
manifest.pages.sort((left, right) => left.path.localeCompare(right.path));
|
|
333
|
+
if (input.format === 'html') {
|
|
334
|
+
await mkdir(path.join(staging, 'assets'), { recursive: true });
|
|
335
|
+
await writeFile(path.join(staging, 'assets/site.css'), SITE_CSS, { mode: 0o600 });
|
|
336
|
+
}
|
|
337
|
+
await writeFile(path.join(staging, '.ywtc-export.json'), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
338
|
+
await verifyGeneratedTree(staging, manifest);
|
|
339
|
+
if (await exists(input.directory)) {
|
|
340
|
+
const selected = await stat(input.directory);
|
|
341
|
+
const empty = selected.isDirectory() && (await readdir(input.directory)).length === 0;
|
|
342
|
+
if (!selected.isDirectory() || (!empty && !(await managedManifest(input.directory, input.identity)))) {
|
|
343
|
+
throw new Error(`Refusing to replace unmanaged destination "${input.directory}". Choose an empty path or a directory with a matching .ywtc-export.json.`);
|
|
344
|
+
}
|
|
345
|
+
if (empty) {
|
|
346
|
+
await rmdir(input.directory);
|
|
255
347
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
348
|
+
else {
|
|
349
|
+
backup = `${input.directory}.backup-${timestamp()}`;
|
|
350
|
+
await rename(input.directory, backup);
|
|
259
351
|
}
|
|
260
352
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
sourceUrl: page.url,
|
|
264
|
-
title: page.title,
|
|
265
|
-
path: pagePaths.get(page.canonicalId),
|
|
266
|
-
...(page.projectedParentId ? { parentId: page.projectedParentId } : {}),
|
|
267
|
-
resources: manifestResources,
|
|
268
|
-
degraded: false,
|
|
269
|
-
warnings: [],
|
|
270
|
-
comments,
|
|
271
|
-
});
|
|
272
|
-
})));
|
|
273
|
-
const children = new Map();
|
|
274
|
-
for (const page of input.graph.pages) {
|
|
275
|
-
const parentId = page.projectedParentId ?? '__root__';
|
|
276
|
-
const siblings = children.get(parentId) ?? [];
|
|
277
|
-
siblings.push(page);
|
|
278
|
-
children.set(parentId, siblings);
|
|
279
|
-
}
|
|
280
|
-
const navigationFor = (currentPath) => {
|
|
281
|
-
const renderItems = (parentId) => `<ul>${(children.get(parentId) ?? []).map((page) => {
|
|
282
|
-
const pageFile = pagePaths.get(page.canonicalId);
|
|
283
|
-
const active = pageFile === currentPath ? ' aria-current="page"' : '';
|
|
284
|
-
const title = page.title.replaceAll('&', '&').replaceAll('<', '<');
|
|
285
|
-
return `<li><a${active} href="${relativeHref(currentPath, pageFile)}">${title}</a>${renderItems(page.canonicalId)}</li>`;
|
|
286
|
-
}).join('')}</ul>`;
|
|
287
|
-
return renderItems('__root__');
|
|
288
|
-
};
|
|
289
|
-
for (const page of input.graph.pages) {
|
|
290
|
-
const fullPage = input.loadPage ? await input.loadPage(page) : page;
|
|
291
|
-
const relative = pagePaths.get(page.canonicalId);
|
|
292
|
-
const resources = resourcesByPage.get(page.canonicalId) ?? [];
|
|
293
|
-
const document = input.loadDocument
|
|
294
|
-
? await input.loadDocument(page)
|
|
295
|
-
: appendGridSnapshots(normalizePageContent(fullPage), resources);
|
|
296
|
-
const pages = new Map([...pagePaths].map(([id, file]) => [id, { path: file, title: sourcePages.get(id)?.title ?? id }]));
|
|
297
|
-
const context = {
|
|
298
|
-
pages,
|
|
299
|
-
resources: resourceMapByPage.get(page.canonicalId) ?? new Map(),
|
|
300
|
-
sourcePages,
|
|
301
|
-
currentPageId: page.canonicalId,
|
|
302
|
-
currentPath: relative,
|
|
303
|
-
};
|
|
304
|
-
const rendered = input.format === 'html' ? await renderStaticHtml(document, context) : await renderMarkdown(document, context);
|
|
305
|
-
const output = input.format === 'html'
|
|
306
|
-
? htmlPage(page.title, navigationFor(relative), `${rendered.body}${commentBodies.get(page.canonicalId) ?? ''}`, relativeHref(relative, 'assets/site.css'))
|
|
307
|
-
: `# ${page.title}\n\n${rendered.body}${commentBodies.has(page.canonicalId) ? `\n\n## Комментарии\n\n${commentBodies.get(page.canonicalId)}` : ''}`;
|
|
308
|
-
await mkdir(path.dirname(path.join(staging, relative)), { recursive: true });
|
|
309
|
-
await writeFile(path.join(staging, relative), output, { mode: 0o600 });
|
|
310
|
-
const manifestPage = manifest.pages.find((item) => item.sourceCanonicalId === page.canonicalId);
|
|
311
|
-
manifestPage.degraded = rendered.degraded;
|
|
312
|
-
manifestPage.warnings.push(...rendered.warnings);
|
|
313
|
-
warnings.push(...rendered.warnings.map((warning) => `${page.title}: ${warning}`));
|
|
314
|
-
if (rendered.degraded) {
|
|
315
|
-
const snapshot = `_source/${stableSegment(page)}.${page.rawFormat === 'yfm' ? 'yfm' : 'storage.xml'}`;
|
|
316
|
-
await mkdir(path.dirname(path.join(staging, snapshot)), { recursive: true });
|
|
317
|
-
await writeFile(path.join(staging, snapshot), fullPage.rawContent, { mode: 0o600 });
|
|
353
|
+
try {
|
|
354
|
+
await rename(staging, input.directory);
|
|
318
355
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
await mkdir(path.join(staging, 'assets'), { recursive: true });
|
|
324
|
-
await writeFile(path.join(staging, 'assets/site.css'), SITE_CSS, { mode: 0o600 });
|
|
325
|
-
}
|
|
326
|
-
await writeFile(path.join(staging, '.ywtc-export.json'), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
327
|
-
await verifyGeneratedTree(staging, manifest);
|
|
328
|
-
if (await exists(input.directory)) {
|
|
329
|
-
const selected = await stat(input.directory);
|
|
330
|
-
if (!selected.isDirectory() || !(await managedManifest(input.directory, input.identity))) {
|
|
331
|
-
throw new Error(`Refusing to replace unmanaged destination "${input.directory}". Choose an empty path or a directory with a matching .ywtc-export.json.`);
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (backup && !(await exists(input.directory)))
|
|
358
|
+
await rename(backup, input.directory);
|
|
359
|
+
throw error;
|
|
332
360
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
try {
|
|
337
|
-
await rename(staging, input.directory);
|
|
361
|
+
const archive = input.archive ? await createArchive(input.directory) : undefined;
|
|
362
|
+
return { directory: input.directory, ...(archive ? { archive } : {}), ...(backup ? { backup } : {}), manifest, warnings, errors };
|
|
338
363
|
}
|
|
339
364
|
catch (error) {
|
|
340
|
-
|
|
341
|
-
await rename(backup, input.directory);
|
|
365
|
+
await rm(staging, { recursive: true, force: true });
|
|
342
366
|
throw error;
|
|
343
367
|
}
|
|
344
|
-
const archive = input.archive ? await createArchive(input.directory) : undefined;
|
|
345
|
-
return { directory: input.directory, ...(archive ? { archive } : {}), ...(backup ? { backup } : {}), manifest, warnings, errors };
|
|
346
368
|
}
|
|
347
|
-
|
|
348
|
-
await
|
|
349
|
-
throw error;
|
|
369
|
+
finally {
|
|
370
|
+
await lease.release();
|
|
350
371
|
}
|
|
351
372
|
}
|
|
352
373
|
}
|