@trawlme/cli 3.8.0 → 3.8.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/dist/commands/scraps.js +55 -2
- package/package.json +1 -1
package/dist/commands/scraps.js
CHANGED
|
@@ -47,6 +47,58 @@ function tryValidateUrl(value, opts) {
|
|
|
47
47
|
throw err;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* One-line rendering of an emptyContext blob for the run-info table. Two
|
|
52
|
+
* buckets are actionable and must stay distinct: selectors that matched zero
|
|
53
|
+
* nodes (the page changed) and selectors the browser rejected outright
|
|
54
|
+
* (`-1`, a bug in the scrap script). Folding the second into "all matched"
|
|
55
|
+
* would report a broken selector as healthy. `--json` still emits the full
|
|
56
|
+
* object, `page.topClasses` / `topAnchorPaths` included.
|
|
57
|
+
*/
|
|
58
|
+
function summarizeEmptyContext(ctx) {
|
|
59
|
+
if (!ctx || typeof ctx !== 'object')
|
|
60
|
+
return null;
|
|
61
|
+
const parts = [];
|
|
62
|
+
const selectors = ctx.selectors && typeof ctx.selectors === 'object' ? ctx.selectors : {};
|
|
63
|
+
const names = Object.keys(selectors);
|
|
64
|
+
// Selectors may themselves contain a comma (`div, span`), so quote each one
|
|
65
|
+
// — a raw join renders one grouping selector as if it were two.
|
|
66
|
+
const list = (ns) => `${ns.slice(0, 3).map((s) => `\`${s}\``).join(', ')}${ns.length > 3 ? ` (+${ns.length - 3})` : ''}`;
|
|
67
|
+
// Four disjoint, exhaustive buckets — every selector lands in exactly one, so
|
|
68
|
+
// no count is ever dropped and none is summarised as something it is not.
|
|
69
|
+
// `selectors` comes from a Mongoose Mixed field, so a count may be `null` or
|
|
70
|
+
// a string: that is `unreadable`, which is neither healthy nor zero-match and
|
|
71
|
+
// must be named as its own thing next to the others, not collapsed into them.
|
|
72
|
+
const isCount = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
73
|
+
const zero = names.filter((s) => selectors[s] === 0);
|
|
74
|
+
const invalid = names.filter((s) => selectors[s] === -1);
|
|
75
|
+
const matched = names.filter((s) => isCount(selectors[s]) && selectors[s] > 0);
|
|
76
|
+
const unreadable = names.filter((s) => !zero.includes(s) && !invalid.includes(s) && !matched.includes(s));
|
|
77
|
+
const mixed = zero.length > 0 || invalid.length > 0 || unreadable.length > 0;
|
|
78
|
+
if (zero.length)
|
|
79
|
+
parts.push(`0-match: ${list(zero)}`);
|
|
80
|
+
if (invalid.length)
|
|
81
|
+
parts.push(`invalid: ${list(invalid)}`);
|
|
82
|
+
if (unreadable.length)
|
|
83
|
+
parts.push(`unreadable: ${list(unreadable)}`);
|
|
84
|
+
if (matched.length) {
|
|
85
|
+
// Named even in a mixed set: "3 of the selectors still work" is the
|
|
86
|
+
// difference between a page that moved and a page that vanished.
|
|
87
|
+
parts.push(mixed ? `${matched.length} matched` : `${matched.length} selector(s), all matched`);
|
|
88
|
+
}
|
|
89
|
+
// Same Mixed-field caution on the title: coerce before calling a string
|
|
90
|
+
// method (a numeric title would otherwise crash `run-info` on the very run
|
|
91
|
+
// it exists to explain), and test against null/undefined rather than
|
|
92
|
+
// truthiness so a falsy-but-real title like `0` still shows. Inner double
|
|
93
|
+
// quotes would close the wrapper early and corrupt the line.
|
|
94
|
+
const title = ctx.page?.title;
|
|
95
|
+
if (title !== undefined && title !== null && String(title) !== '') {
|
|
96
|
+
parts.push(`title="${String(title).replace(/"/g, "'")}"`);
|
|
97
|
+
}
|
|
98
|
+
if (typeof ctx.page?.totalAnchors === 'number')
|
|
99
|
+
parts.push(`${ctx.page.totalAnchors} anchors`);
|
|
100
|
+
return parts.length ? parts.join(' | ') : null;
|
|
101
|
+
}
|
|
50
102
|
function lastStatus(scrap) {
|
|
51
103
|
const last = scrap.history?.[0];
|
|
52
104
|
if (!last)
|
|
@@ -1118,14 +1170,15 @@ export function attachRunInfoCommand(parent, attachOpts = {}) {
|
|
|
1118
1170
|
blockType: h.blockType ?? null,
|
|
1119
1171
|
errorMessage: h.errorSnapshot?.errorMessage ?? null,
|
|
1120
1172
|
selector: h.errorSnapshot?.selector ?? null,
|
|
1121
|
-
emptyContext: h.
|
|
1173
|
+
emptyContext: h.emptyContext ?? null,
|
|
1122
1174
|
createdAt: h.createdAt ?? null,
|
|
1123
1175
|
};
|
|
1124
1176
|
if (opts.json) {
|
|
1125
1177
|
json(info);
|
|
1126
1178
|
return;
|
|
1127
1179
|
}
|
|
1128
|
-
|
|
1180
|
+
// The blob is an object; the table needs one line. --json keeps it whole.
|
|
1181
|
+
table([{ ...info, emptyContext: summarizeEmptyContext(info.emptyContext) }], ['hid', 'status', 'time', 'tier', 'failureKind', 'blockType', 'errorMessage', 'selector', 'emptyContext', 'createdAt']);
|
|
1129
1182
|
});
|
|
1130
1183
|
}
|
|
1131
1184
|
attachRunInfoCommand(scraps, { hidden: true });
|