@thallylabs/migrate 0.2.28 → 0.2.30
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/dist/index.js +123 -22
- package/dist/index.js.map +1 -1
- package/dist/url-migration.js +123 -22
- package/dist/url-migration.js.map +1 -1
- package/package.json +1 -1
package/dist/url-migration.js
CHANGED
|
@@ -463,6 +463,73 @@ function isPassiveSvg(content) {
|
|
|
463
463
|
return isPassive && sawRoot && depth === 0;
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
+
// src/url-asset-cache.ts
|
|
467
|
+
function createMigrationAssetCache() {
|
|
468
|
+
const settled = /* @__PURE__ */ new Map();
|
|
469
|
+
const pending = /* @__PURE__ */ new Map();
|
|
470
|
+
const remember = (url, asset) => {
|
|
471
|
+
settled.delete(url);
|
|
472
|
+
settled.set(url, asset);
|
|
473
|
+
if (settled.size > 480) settled.delete(settled.keys().next().value);
|
|
474
|
+
return asset;
|
|
475
|
+
};
|
|
476
|
+
const get = (url) => {
|
|
477
|
+
const asset = settled.get(url);
|
|
478
|
+
if (asset) return Promise.resolve(remember(url, asset));
|
|
479
|
+
return pending.get(url);
|
|
480
|
+
};
|
|
481
|
+
return {
|
|
482
|
+
remember,
|
|
483
|
+
get,
|
|
484
|
+
has(url) {
|
|
485
|
+
return settled.has(url) || pending.has(url);
|
|
486
|
+
},
|
|
487
|
+
get size() {
|
|
488
|
+
return settled.size + pending.size;
|
|
489
|
+
},
|
|
490
|
+
load(url, acquire) {
|
|
491
|
+
const existing = get(url);
|
|
492
|
+
if (existing) return existing;
|
|
493
|
+
const request = Promise.resolve().then(acquire).then((asset) => {
|
|
494
|
+
pending.delete(url);
|
|
495
|
+
return remember(url, asset);
|
|
496
|
+
}, (error) => {
|
|
497
|
+
pending.delete(url);
|
|
498
|
+
throw error;
|
|
499
|
+
});
|
|
500
|
+
pending.set(url, request);
|
|
501
|
+
return request;
|
|
502
|
+
},
|
|
503
|
+
delete(url) {
|
|
504
|
+
settled.delete(url);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/url-journal-clone.ts
|
|
510
|
+
function cloneUrlMigrationCheckpoint(checkpoint) {
|
|
511
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
512
|
+
const clone = (value) => {
|
|
513
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
514
|
+
if (typeof value === "number") return Number.isFinite(value) ? value === 0 ? 0 : value : null;
|
|
515
|
+
if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") return void 0;
|
|
516
|
+
if (typeof value !== "object") throw new TypeError("Checkpoint must contain JSON-safe values.");
|
|
517
|
+
if (ancestors.has(value)) throw new TypeError("Checkpoint must not contain circular references.");
|
|
518
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("Checkpoint must contain plain JSON containers.");
|
|
519
|
+
ancestors.add(value);
|
|
520
|
+
try {
|
|
521
|
+
if (Array.isArray(value)) return Array.from(value, (entry) => clone(entry) ?? null);
|
|
522
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => {
|
|
523
|
+
const copied = clone(entry);
|
|
524
|
+
return copied === void 0 ? [] : [[key, copied]];
|
|
525
|
+
}));
|
|
526
|
+
} finally {
|
|
527
|
+
ancestors.delete(value);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
return clone(checkpoint);
|
|
531
|
+
}
|
|
532
|
+
|
|
466
533
|
// src/url-images.ts
|
|
467
534
|
function migrationImageUrl(value, base) {
|
|
468
535
|
const url = new URL(value, base);
|
|
@@ -3191,7 +3258,8 @@ async function runUrlMigration(options) {
|
|
|
3191
3258
|
const batchSize = Math.max(1, Math.min(500, Math.floor(options.batchSize ?? 50)));
|
|
3192
3259
|
if (!Number.isFinite(concurrency) || !Number.isFinite(batchSize) || options.maxCaptures !== void 0 && (!Number.isInteger(options.maxCaptures) || options.maxCaptures < 1) || options.maxTotalBytes !== void 0 && (!Number.isFinite(options.maxTotalBytes) || options.maxTotalBytes < 1)) throw new Error("Migration batch, concurrency and byte limits must be positive finite numbers.");
|
|
3193
3260
|
const captureVersion = options.captureVersion ?? "fetch-v1";
|
|
3194
|
-
|
|
3261
|
+
if (options.checkpoint && options.checkpoint.records.length > MAX_RECORDS) throw new Error("Checkpoint inventory exceeds its bound.");
|
|
3262
|
+
const checkpoint = options.checkpoint ? cloneUrlMigrationCheckpoint(options.checkpoint) : { schemaVersion: 1, converterVersion: CONVERTER_VERSION, sourceUrl, scopePath, captureVersion, records: [], navigation: [] };
|
|
3195
3263
|
if (checkpoint.schemaVersion !== 1 || checkpoint.converterVersion !== CONVERTER_VERSION || checkpoint.sourceUrl !== sourceUrl || checkpoint.scopePath !== scopePath || checkpoint.captureVersion !== captureVersion) throw new Error("Checkpoint does not match this source, scope or capture/converter version.");
|
|
3196
3264
|
if (checkpoint.records.length > MAX_RECORDS) throw new Error("Checkpoint inventory exceeds its bound.");
|
|
3197
3265
|
const records = /* @__PURE__ */ new Map();
|
|
@@ -3232,6 +3300,33 @@ async function runUrlMigration(options) {
|
|
|
3232
3300
|
enqueue(sourceUrl, sourceUrl, "submitted");
|
|
3233
3301
|
for (const path of ["llms.txt", "sitemap.xml", "robots.txt"]) enqueue(`${scopePath === "/" ? "" : scopePath}/${path}`, sourceUrl, "discovery-index");
|
|
3234
3302
|
if (scopePath !== "/") for (const path of ["llms.txt", "sitemap.xml", "robots.txt"]) enqueue(`/${path}`, sourceUrl, "discovery-index");
|
|
3303
|
+
const enqueueArticleLinks = (links, base, discoveredBy) => {
|
|
3304
|
+
for (const link of links) {
|
|
3305
|
+
try {
|
|
3306
|
+
const url = new URL(link, base);
|
|
3307
|
+
if (!/\.(?:png|jpe?g|gif|svg|webp|avif|pdf|zip|mp4|webm|css|js)$/i.test(url.pathname)) enqueue(link, base, discoveredBy);
|
|
3308
|
+
} catch {
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
};
|
|
3312
|
+
const enqueueNavigation = (nodes, base) => {
|
|
3313
|
+
for (const node of nodes) {
|
|
3314
|
+
if (node.url) enqueue(node.url, base, "source-navigation");
|
|
3315
|
+
enqueueNavigation(node.children ?? [], base);
|
|
3316
|
+
}
|
|
3317
|
+
};
|
|
3318
|
+
for (const record of [...records.values()]) {
|
|
3319
|
+
if (!record.evidence) continue;
|
|
3320
|
+
const final = canonical(record.evidence.finalUrl, sourceUrl);
|
|
3321
|
+
if (!final) continue;
|
|
3322
|
+
if (record.outcome === "imported") enqueueArticleLinks(record.evidence.links, final, record.url);
|
|
3323
|
+
else if (record.outcome === "excluded") {
|
|
3324
|
+
if (classifyMigrationResponse({ finalUrl: new URL(final), contentType: record.evidence.contentType, body: "" }) === "index") {
|
|
3325
|
+
for (const link of record.evidence.links) enqueue(link, final, record.url);
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
enqueueNavigation(checkpoint.navigation, sourceUrl);
|
|
3235
3330
|
const fetcher = options.fetcher ?? defaultMigrationFetcher;
|
|
3236
3331
|
const capture = options.captureProvider?.capture.bind(options.captureProvider) ?? (async ({ url }) => fetcher(url, { accept: ACCEPT }));
|
|
3237
3332
|
const assetFetcher = options.assetFetcher ?? (!options.fetcher && !options.captureProvider ? defaultAssetFetcher : void 0);
|
|
@@ -3239,9 +3334,21 @@ async function runUrlMigration(options) {
|
|
|
3239
3334
|
const finalUrls = new Map([...records.values()].filter((record) => record.evidence && record.page).map((record) => [canonical(record.evidence.finalUrl, sourceUrl), record.url]));
|
|
3240
3335
|
let imported = usedIds.size;
|
|
3241
3336
|
const assetPayloads = new Map(checkpoint.records.flatMap((record) => (record.assets ?? []).map((asset) => [asset.sha256, asset])));
|
|
3242
|
-
const
|
|
3337
|
+
const assetBytes = /* @__PURE__ */ new Map();
|
|
3338
|
+
const retainAsset = (asset) => {
|
|
3339
|
+
if (!acceptedAsset(asset)) throw new Error("Asset MIME/signature is unsupported or unsafe to serve.");
|
|
3340
|
+
const sha256 = hash(asset.content);
|
|
3341
|
+
const content = assetBytes.get(sha256) ?? Buffer.from(asset.content);
|
|
3342
|
+
assetBytes.set(sha256, content);
|
|
3343
|
+
return { ...asset, content };
|
|
3344
|
+
};
|
|
3345
|
+
const assetRequests = createMigrationAssetCache();
|
|
3243
3346
|
for (const record of checkpoint.records) for (const asset of record.assets ?? []) {
|
|
3244
|
-
if (!assetRequests.has(asset.url))
|
|
3347
|
+
if (!assetRequests.has(asset.url)) {
|
|
3348
|
+
const content = assetBytes.get(asset.sha256) ?? Buffer.from(asset.base64, "base64");
|
|
3349
|
+
assetBytes.set(asset.sha256, content);
|
|
3350
|
+
assetRequests.remember(asset.url, { url: asset.url, contentType: asset.contentType, content });
|
|
3351
|
+
}
|
|
3245
3352
|
}
|
|
3246
3353
|
let totalBytes = checkpoint.records.reduce((sum, record) => sum + (record.evidence?.byteLength ?? 0), 0) + [...assetPayloads.values()].reduce((sum, asset) => sum + Buffer.byteLength(asset.base64, "base64"), 0);
|
|
3247
3354
|
let budgetExhausted = false;
|
|
@@ -3353,10 +3460,10 @@ ${difference}`;
|
|
|
3353
3460
|
try {
|
|
3354
3461
|
let assetRequest = assetRequests.get(url);
|
|
3355
3462
|
if (!suppliedAssets.has(url) && !assetRequest && assetFetcher) {
|
|
3356
|
-
assetRequest = assetFetcher(new URL(url), options.signal);
|
|
3357
|
-
assetRequests.set(url, assetRequest);
|
|
3463
|
+
assetRequest = assetRequests.load(url, () => assetFetcher(new URL(url), options.signal).then(retainAsset));
|
|
3358
3464
|
}
|
|
3359
|
-
const
|
|
3465
|
+
const supplied = suppliedAssets.get(url);
|
|
3466
|
+
const asset = supplied ? retainAsset(supplied) : assetRequest ? await assetRequest : null;
|
|
3360
3467
|
if (!asset) throw new Error("Capture host did not supply asset bytes or a binary fetch boundary.");
|
|
3361
3468
|
const extension = acceptedAsset(asset);
|
|
3362
3469
|
if (!extension) throw new Error("Asset MIME/signature is unsupported or unsafe to serve.");
|
|
@@ -3373,7 +3480,7 @@ ${difference}`;
|
|
|
3373
3480
|
const receipt = { url, path, sha256, contentType: asset.contentType, base64: existing?.base64 ?? Buffer.from(asset.content).toString("base64") };
|
|
3374
3481
|
record.assets.push(receipt);
|
|
3375
3482
|
if (!existing) assetPayloads.set(sha256, receipt);
|
|
3376
|
-
if (!assetRequests.has(url)) assetRequests.
|
|
3483
|
+
if (!assetRequests.has(url)) assetRequests.remember(url, asset);
|
|
3377
3484
|
page.body = rewriteMarkdownDestinations(page.body, (destination) => canonical(destination, final) === url ? `/${path}${new URL(destination, final).hash}` : destination);
|
|
3378
3485
|
} catch (error) {
|
|
3379
3486
|
assetRequests.delete(url);
|
|
@@ -3406,20 +3513,8 @@ ${difference}`;
|
|
|
3406
3513
|
checkpoint.navigation = navigation;
|
|
3407
3514
|
navigationChanged = true;
|
|
3408
3515
|
}
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
const url = new URL(link, final);
|
|
3412
|
-
if (!/\.(?:png|jpe?g|gif|svg|webp|avif|pdf|zip|mp4|webm|css|js)$/i.test(url.pathname)) enqueue(link, final, record.url);
|
|
3413
|
-
} catch {
|
|
3414
|
-
}
|
|
3415
|
-
}
|
|
3416
|
-
const visit = (nodes) => {
|
|
3417
|
-
for (const node of nodes) {
|
|
3418
|
-
if (node.url) enqueue(node.url, final, "source-navigation");
|
|
3419
|
-
visit(node.children ?? []);
|
|
3420
|
-
}
|
|
3421
|
-
};
|
|
3422
|
-
visit(navigation);
|
|
3516
|
+
enqueueArticleLinks(evidence.links, final, record.url);
|
|
3517
|
+
enqueueNavigation(navigation, final);
|
|
3423
3518
|
} catch (error) {
|
|
3424
3519
|
if (options.signal?.aborted) return;
|
|
3425
3520
|
const wasImported = record.outcome === "imported";
|
|
@@ -3489,7 +3584,13 @@ ${difference}`;
|
|
|
3489
3584
|
const validation = validateUrlMigration(checkpoint, Boolean(options.signal?.aborted));
|
|
3490
3585
|
validation.issues.push(...navigationIssues);
|
|
3491
3586
|
if (navigationIssues.length && validation.status === "passed") validation.status = "partial";
|
|
3492
|
-
const assets =
|
|
3587
|
+
const assets = /* @__PURE__ */ new Map();
|
|
3588
|
+
for (const record of checkpoint.records) for (const asset of record.assets ?? []) {
|
|
3589
|
+
if (!assets.has(asset.path)) {
|
|
3590
|
+
const content = assetBytes.get(asset.sha256) ?? Buffer.from(asset.base64, "base64");
|
|
3591
|
+
assets.set(asset.path, { path: asset.path, content: Buffer.from(content.buffer, content.byteOffset, content.byteLength) });
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3493
3594
|
const bundle = {
|
|
3494
3595
|
sourceUrl,
|
|
3495
3596
|
sourceKind: "url",
|