@d-zero/page-cluster 0.5.7 → 0.6.0
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/cli.js +78 -69
- package/package.json +8 -4
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// animated header on a TTY, appended `[page-cluster] …` lines otherwise.
|
|
7
7
|
import { writeFile } from 'node:fs/promises';
|
|
8
8
|
import process from 'node:process';
|
|
9
|
+
import { unwrapSuppressedError } from '@d-zero/cli-core';
|
|
9
10
|
import { Lanes } from '@d-zero/dealer';
|
|
10
11
|
import { resolvePageClusterKeys } from './resolve-page-cluster-keys.js';
|
|
11
12
|
const HELP_TEXT = `Usage:
|
|
@@ -298,6 +299,20 @@ function errorLine(message) {
|
|
|
298
299
|
verbose: `error: ${message}`,
|
|
299
300
|
};
|
|
300
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Formats a caught error for `errorLine()`. `SuppressedError` (thrown when a
|
|
304
|
+
* `using`-scoped body error and a disposal error occur together) hides the
|
|
305
|
+
* real cause behind a generic message, so its underlying causes are
|
|
306
|
+
* unwrapped and joined into one line — `errorLine()`/`renderProgress()` must
|
|
307
|
+
* still be called exactly once per catch site, since Lanes' TTY repaint only
|
|
308
|
+
* keeps the latest frame (see {@link errorLine}'s JSDoc).
|
|
309
|
+
* @param error
|
|
310
|
+
*/
|
|
311
|
+
function formatErrorMessage(error) {
|
|
312
|
+
return unwrapSuppressedError(error)
|
|
313
|
+
.map((cause) => (cause instanceof Error ? cause.message : String(cause)))
|
|
314
|
+
.join(' / ');
|
|
315
|
+
}
|
|
301
316
|
/**
|
|
302
317
|
* Maps a library `ProgressEvent` to a human-facing `ProgressLine`. The
|
|
303
318
|
* verbose arm keeps the historical `pass0:` / `pass1:` / `pass1b:` /
|
|
@@ -386,7 +401,10 @@ export async function runCli(options) {
|
|
|
386
401
|
// pass in) does — reading it defensively lets both real usage and
|
|
387
402
|
// unit-test doubles work without a separate `--no-progress` flag.
|
|
388
403
|
const useTty = options.stderr.isTTY === true;
|
|
389
|
-
|
|
404
|
+
// `using` により、この関数を抜けるすべての経路(下記の各 `return` は
|
|
405
|
+
// もちろん、想定外の例外を含む)で確実に lanes.close() が呼ばれ、
|
|
406
|
+
// Display の setTimeout タイマーが解放される。
|
|
407
|
+
using lanes = new Lanes({ stream: options.stderr, verbose: !useTty });
|
|
390
408
|
// Verbose Lanes prepends `#header` to every `update()` line. Without
|
|
391
409
|
// this seed call the header would be undefined and each progress line
|
|
392
410
|
// would begin with the literal string `undefined ` — bug caught by
|
|
@@ -397,86 +415,77 @@ export async function runCli(options) {
|
|
|
397
415
|
}
|
|
398
416
|
const startTime = Date.now();
|
|
399
417
|
const elapsed = () => Math.max(0, Math.round((Date.now() - startTime) / 1000));
|
|
400
|
-
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
418
|
+
renderProgress(lanes, useTty, READING_INPUT);
|
|
419
|
+
// Load every JSONL line into memory once so the ids array stays
|
|
420
|
+
// parallel to the pages array — the streaming driver reads its
|
|
421
|
+
// factory twice, and stdin is a one-shot pipe.
|
|
422
|
+
const ids = [];
|
|
423
|
+
const pages = [];
|
|
424
|
+
try {
|
|
425
|
+
for await (const { id, page } of readJsonlPages(options.stdin)) {
|
|
426
|
+
ids.push(id);
|
|
427
|
+
pages.push(page);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
renderProgress(lanes, useTty, errorLine(formatErrorMessage(error)));
|
|
432
|
+
return 1;
|
|
433
|
+
}
|
|
434
|
+
renderProgress(lanes, useTty, readingDoneLine(pages.length));
|
|
435
|
+
// Only worth collecting when the caller asked for the file — a
|
|
436
|
+
// ClusterReason Map costs bookkeeping proportional to cluster count,
|
|
437
|
+
// not page count, but there's no reason to pay even that when unused.
|
|
438
|
+
const reasonsByClusterKey = args.clusterReasonsFile
|
|
439
|
+
? new Map()
|
|
440
|
+
: undefined;
|
|
441
|
+
// Same opt-in gate as `reasonsByClusterKey` — `onPartitionReport`
|
|
442
|
+
// itself gates the underlying validation pass (see its own JSDoc).
|
|
443
|
+
let partitionReport;
|
|
444
|
+
const resolveOptions = {
|
|
445
|
+
contentBlockAttribute: args.contentBlockAttribute,
|
|
446
|
+
onProgress: (event) => {
|
|
447
|
+
renderProgress(lanes, useTty, formatProgressLine(event, elapsed()));
|
|
448
|
+
},
|
|
449
|
+
onClusterReason: reasonsByClusterKey
|
|
450
|
+
? (key, reason) => reasonsByClusterKey.set(key, reason)
|
|
451
|
+
: undefined,
|
|
452
|
+
onPartitionReport: args.validationFile
|
|
453
|
+
? (report) => (partitionReport = report)
|
|
454
|
+
: undefined,
|
|
455
|
+
};
|
|
456
|
+
let clusterKeys;
|
|
404
457
|
try {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
458
|
+
clusterKeys = await resolvePageClusterKeys(() => pages, resolveOptions);
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
renderProgress(lanes, useTty, errorLine(formatErrorMessage(error)));
|
|
462
|
+
return 1;
|
|
463
|
+
}
|
|
464
|
+
const clusterCount = new Set(clusterKeys).size;
|
|
465
|
+
renderProgress(lanes, useTty, doneLine(pages.length, clusterCount, elapsed()));
|
|
466
|
+
for (const [index, key] of clusterKeys.entries()) {
|
|
467
|
+
const row = { id: ids[index] ?? index, clusterKey: key };
|
|
468
|
+
options.stdout.write(`${JSON.stringify(row)}\n`);
|
|
469
|
+
}
|
|
470
|
+
if (args.clusterReasonsFile && reasonsByClusterKey) {
|
|
411
471
|
try {
|
|
412
|
-
|
|
413
|
-
ids.push(id);
|
|
414
|
-
pages.push(page);
|
|
415
|
-
}
|
|
472
|
+
await writeFile(args.clusterReasonsFile, JSON.stringify(Object.fromEntries(reasonsByClusterKey), null, 2));
|
|
416
473
|
}
|
|
417
474
|
catch (error) {
|
|
418
|
-
renderProgress(lanes, useTty, errorLine(error
|
|
475
|
+
renderProgress(lanes, useTty, errorLine(formatErrorMessage(error)));
|
|
419
476
|
return 1;
|
|
420
477
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
// ClusterReason Map costs bookkeeping proportional to cluster count,
|
|
424
|
-
// not page count, but there's no reason to pay even that when unused.
|
|
425
|
-
const reasonsByClusterKey = args.clusterReasonsFile
|
|
426
|
-
? new Map()
|
|
427
|
-
: undefined;
|
|
428
|
-
// Same opt-in gate as `reasonsByClusterKey` — `onPartitionReport`
|
|
429
|
-
// itself gates the underlying validation pass (see its own JSDoc).
|
|
430
|
-
let partitionReport;
|
|
431
|
-
const resolveOptions = {
|
|
432
|
-
contentBlockAttribute: args.contentBlockAttribute,
|
|
433
|
-
onProgress: (event) => {
|
|
434
|
-
renderProgress(lanes, useTty, formatProgressLine(event, elapsed()));
|
|
435
|
-
},
|
|
436
|
-
onClusterReason: reasonsByClusterKey
|
|
437
|
-
? (key, reason) => reasonsByClusterKey.set(key, reason)
|
|
438
|
-
: undefined,
|
|
439
|
-
onPartitionReport: args.validationFile
|
|
440
|
-
? (report) => (partitionReport = report)
|
|
441
|
-
: undefined,
|
|
442
|
-
};
|
|
443
|
-
let clusterKeys;
|
|
478
|
+
}
|
|
479
|
+
if (args.validationFile && partitionReport) {
|
|
444
480
|
try {
|
|
445
|
-
|
|
481
|
+
await writeFile(args.validationFile, JSON.stringify(toJsonSafePartitionReport(partitionReport), null, 2));
|
|
446
482
|
}
|
|
447
483
|
catch (error) {
|
|
448
|
-
renderProgress(lanes, useTty, errorLine(error
|
|
484
|
+
renderProgress(lanes, useTty, errorLine(formatErrorMessage(error)));
|
|
449
485
|
return 1;
|
|
450
486
|
}
|
|
451
|
-
const clusterCount = new Set(clusterKeys).size;
|
|
452
|
-
renderProgress(lanes, useTty, doneLine(pages.length, clusterCount, elapsed()));
|
|
453
|
-
for (const [index, key] of clusterKeys.entries()) {
|
|
454
|
-
const row = { id: ids[index] ?? index, clusterKey: key };
|
|
455
|
-
options.stdout.write(`${JSON.stringify(row)}\n`);
|
|
456
|
-
}
|
|
457
|
-
if (args.clusterReasonsFile && reasonsByClusterKey) {
|
|
458
|
-
try {
|
|
459
|
-
await writeFile(args.clusterReasonsFile, JSON.stringify(Object.fromEntries(reasonsByClusterKey), null, 2));
|
|
460
|
-
}
|
|
461
|
-
catch (error) {
|
|
462
|
-
renderProgress(lanes, useTty, errorLine(error.message));
|
|
463
|
-
return 1;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (args.validationFile && partitionReport) {
|
|
467
|
-
try {
|
|
468
|
-
await writeFile(args.validationFile, JSON.stringify(toJsonSafePartitionReport(partitionReport), null, 2));
|
|
469
|
-
}
|
|
470
|
-
catch (error) {
|
|
471
|
-
renderProgress(lanes, useTty, errorLine(error.message));
|
|
472
|
-
return 1;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
return 0;
|
|
476
|
-
}
|
|
477
|
-
finally {
|
|
478
|
-
lanes.close();
|
|
479
487
|
}
|
|
488
|
+
return 0;
|
|
480
489
|
}
|
|
481
490
|
/**
|
|
482
491
|
* Reads the package version out of `package.json` at runtime. Kept as a
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d-zero/page-cluster",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Clusters crawled HTML pages by DOM-structure similarity — assigns the same key to pages sharing a template, ignoring text content. CLI-first, with library APIs.",
|
|
5
5
|
"author": "D-ZERO",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=24.11.0"
|
|
12
|
+
},
|
|
10
13
|
"type": "module",
|
|
11
14
|
"exports": {
|
|
12
15
|
".": {
|
|
@@ -76,8 +79,9 @@
|
|
|
76
79
|
"clean": "tsc --build --clean"
|
|
77
80
|
},
|
|
78
81
|
"dependencies": {
|
|
79
|
-
"@d-zero/
|
|
80
|
-
"@d-zero/
|
|
82
|
+
"@d-zero/cli-core": "1.4.0",
|
|
83
|
+
"@d-zero/dealer": "1.11.0",
|
|
84
|
+
"@d-zero/shared": "0.23.0",
|
|
81
85
|
"htmlparser2": "12.0.0"
|
|
82
86
|
},
|
|
83
87
|
"repository": {
|
|
@@ -85,5 +89,5 @@
|
|
|
85
89
|
"url": "https://github.com/d-zero-dev/tools.git",
|
|
86
90
|
"directory": "packages/@d-zero/page-cluster"
|
|
87
91
|
},
|
|
88
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "d48a3ed9c01d32f0ca1a5b00e4abc36c376ce880"
|
|
89
93
|
}
|