@nitpicker/crawler 0.11.0 → 0.12.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.
Files changed (75) hide show
  1. package/lib/archive/archive.d.ts +117 -2
  2. package/lib/archive/archive.js +147 -2
  3. package/lib/archive/cache/compute-archive-cache-key.d.ts +39 -0
  4. package/lib/archive/cache/compute-archive-cache-key.js +95 -0
  5. package/lib/archive/cache/extract-archive-to-cache.d.ts +43 -0
  6. package/lib/archive/cache/extract-archive-to-cache.js +309 -0
  7. package/lib/archive/cache/get-archive-cache-root.d.ts +20 -0
  8. package/lib/archive/cache/get-archive-cache-root.js +53 -0
  9. package/lib/archive/cache/is-archive-cache-disabled.d.ts +24 -0
  10. package/lib/archive/cache/is-archive-cache-disabled.js +34 -0
  11. package/lib/archive/cache/resolve-archive-cache-dir.d.ts +26 -0
  12. package/lib/archive/cache/resolve-archive-cache-dir.js +32 -0
  13. package/lib/archive/database.d.ts +216 -15
  14. package/lib/archive/database.js +1459 -938
  15. package/lib/archive/derive-lineage-from-parent.d.ts +37 -0
  16. package/lib/archive/derive-lineage-from-parent.js +42 -0
  17. package/lib/archive/get-failed-page-messages.d.ts +43 -0
  18. package/lib/archive/get-failed-page-messages.js +131 -0
  19. package/lib/archive/init-schema.js +153 -1
  20. package/lib/archive/is-inventory-source.d.ts +21 -0
  21. package/lib/archive/is-inventory-source.js +22 -0
  22. package/lib/archive/migrate-inventory-runs.d.ts +29 -0
  23. package/lib/archive/migrate-inventory-runs.js +52 -0
  24. package/lib/archive/types.d.ts +33 -0
  25. package/lib/classify-error-kind.d.ts +19 -0
  26. package/lib/classify-error-kind.js +122 -0
  27. package/lib/crawler/build-js-redirect-edge.d.ts +68 -0
  28. package/lib/crawler/build-js-redirect-edge.js +57 -0
  29. package/lib/crawler/build-redirect-event.d.ts +24 -0
  30. package/lib/crawler/build-redirect-event.js +28 -0
  31. package/lib/crawler/clear-dns-burned-host-cache.d.ts +6 -0
  32. package/lib/crawler/clear-dns-burned-host-cache.js +11 -0
  33. package/lib/crawler/crawler.d.ts +3 -1
  34. package/lib/crawler/crawler.js +655 -107
  35. package/lib/crawler/derive-js-redirect-target.d.ts +68 -0
  36. package/lib/crawler/derive-js-redirect-target.js +129 -0
  37. package/lib/crawler/derive-resource-source.d.ts +25 -15
  38. package/lib/crawler/derive-resource-source.js +28 -17
  39. package/lib/crawler/dns-burned-host-cache.d.ts +26 -0
  40. package/lib/crawler/dns-burned-host-cache.js +25 -0
  41. package/lib/crawler/dns-burned-host-short-circuit-counter.d.ts +13 -0
  42. package/lib/crawler/dns-burned-host-short-circuit-counter.js +11 -0
  43. package/lib/crawler/fetch-destination.d.ts +12 -4
  44. package/lib/crawler/fetch-destination.js +94 -16
  45. package/lib/crawler/is-js-redirect-error-shape.d.ts +40 -0
  46. package/lib/crawler/is-js-redirect-error-shape.js +53 -0
  47. package/lib/crawler/is-puppeteer-fallback-candidate.d.ts +16 -0
  48. package/lib/crawler/is-puppeteer-fallback-candidate.js +63 -0
  49. package/lib/crawler/link-list.d.ts +21 -1
  50. package/lib/crawler/link-list.js +23 -3
  51. package/lib/crawler/plan-sub-resource-emits.d.ts +63 -0
  52. package/lib/crawler/plan-sub-resource-emits.js +44 -0
  53. package/lib/crawler/preload-short-circuit-error.d.ts +22 -0
  54. package/lib/crawler/preload-short-circuit-error.js +25 -0
  55. package/lib/crawler/should-burn-host.d.ts +78 -0
  56. package/lib/crawler/should-burn-host.js +61 -0
  57. package/lib/crawler/should-get-fallback-on-head-failure.d.ts +38 -0
  58. package/lib/crawler/should-get-fallback-on-head-failure.js +46 -0
  59. package/lib/crawler/types.d.ts +107 -0
  60. package/lib/crawler-orchestrator.d.ts +13 -3
  61. package/lib/crawler-orchestrator.js +292 -69
  62. package/lib/crawler.d.ts +3 -2
  63. package/lib/crawler.js +3 -1
  64. package/lib/permanent-error-kinds.d.ts +43 -0
  65. package/lib/permanent-error-kinds.js +48 -0
  66. package/lib/types.d.ts +84 -0
  67. package/lib/utils/compute-file-sha256.d.ts +23 -0
  68. package/lib/utils/compute-file-sha256.js +55 -0
  69. package/lib/utils/error/emit-error-with-retry.d.ts +40 -0
  70. package/lib/utils/error/emit-error-with-retry.js +44 -0
  71. package/lib/utils/error/emit-error.d.ts +39 -0
  72. package/lib/utils/error/emit-error.js +41 -0
  73. package/package.json +11 -11
  74. package/lib/utils/error/error-emitter.d.ts +0 -18
  75. package/lib/utils/error/error-emitter.js +0 -29
@@ -12,6 +12,29 @@ export interface RedirectEdgeResult {
12
12
  type: 'redirect-edge';
13
13
  /** HEAD-resolved page data carrying the redirect chain (source → destination). */
14
14
  pageData: PageData;
15
+ /**
16
+ * Where the chain came from. The caller needs this to decide whether the
17
+ * URLs in `pageData.redirectPaths` are already-known (HTTP chain — every
18
+ * hop was followed by the browser/HEAD pre-flight and the destination is
19
+ * already rendered) or brand-new (JS redirect — only the source was
20
+ * processed, the destination came out of `page.url()` and has never been
21
+ * touched).
22
+ *
23
+ * - `'http-chain'` — Returned when the HEAD pre-flight resolved a real
24
+ * 3xx chain and the destination has already been claimed via
25
+ * `#scrapedDestinations`. The crawler folds every URL in
26
+ * `redirectPaths` into the link-list's done-set; the destination is not
27
+ * re-enqueued because it is already in the archive.
28
+ * - `'js-redirect'` — Returned when `scraper.scrapeStart` threw because
29
+ * `page.goto()` resolved to `null` (client-side
30
+ * `window.location.replace()` / `<meta http-equiv="refresh">`) and
31
+ * `page.url()` exposed a different destination. `redirectPaths`
32
+ * contains exactly one URL: the JS-redirect target. The crawler MUST
33
+ * enqueue that destination so it reaches the browser, and MUST NOT
34
+ * fold it into the done-set (otherwise the dealer's `seen` rejects
35
+ * the push and the destination is silently lost from the archive).
36
+ */
37
+ source: 'http-chain' | 'js-redirect';
15
38
  }
16
39
  /**
17
40
  * The outcome of {@link Crawler.#scrapePage}: either a normal scrape result from
@@ -19,6 +42,52 @@ export interface RedirectEdgeResult {
19
42
  * redirect destination was already rendered and only the edge needs recording.
20
43
  */
21
44
  export type ScrapeOutcome = ScrapeResult | RedirectEdgeResult;
45
+ /**
46
+ * Internal envelope returned by {@link Crawler.#launchBrowserAndScrape} that
47
+ * augments beholder's {@link ScrapeResult} with the puppeteer-side
48
+ * post-navigation URL.
49
+ *
50
+ * **Why:** when `scraper.scrapeStart` throws because `page.goto()` resolved to
51
+ * `null` (the classic puppeteer symptom of a client-side
52
+ * `window.location.replace()` / meta-refresh firing mid-navigation), the only
53
+ * authoritative source for the URL the browser actually landed on is
54
+ * `page.url()` — neither the HEAD pre-flight nor the thrown error carries it.
55
+ * Capturing it here lets `#scrapePage` fold the source into a redirect edge
56
+ * instead of recording a hard `status = -1` that `--retry-failed` would chase
57
+ * forever (`Page.goto returned null` classifies as `protocol`, which is neither
58
+ * permanent nor a puppeteer-fallback kind — so the SQL filter resets it every
59
+ * pass and the next pass replays the same failure).
60
+ *
61
+ * `postNavigationUrl` is optional because:
62
+ * - successful / skipped outcomes do not need it (the success path already
63
+ * exposes the final URL via `pageData.url` + `redirectPaths`);
64
+ * - capturing can itself fail when the underlying browser context is already
65
+ * torn down (target closed, session killed) — we treat that as "no extra
66
+ * information" and fall through to the existing error path.
67
+ */
68
+ export type BrowserScrapeResult = ScrapeResult & {
69
+ /**
70
+ * URL puppeteer reports via `page.url()` *after* a thrown navigation.
71
+ *
72
+ * Semantically only meaningful when the parent result is `type: 'error'`
73
+ * — `#launchBrowserAndScrape` sets it from inside its catch arm, and the
74
+ * success / skipped paths never write to it. The field is typed as
75
+ * optional on the whole envelope rather than narrowed to the error
76
+ * variant because beholder's `ScrapeResult` is not a discriminated
77
+ * union (all variants share the same shape and disambiguate via
78
+ * `type`), so narrowing here would force a parallel ad-hoc union with
79
+ * no compile-time payoff. Consumers MUST therefore check
80
+ * `result.type === 'error'` before reading `postNavigationUrl` — and in
81
+ * practice the only consumer is the JS-redirect rescue, which does
82
+ * exactly that.
83
+ *
84
+ * Consumers should also confirm the URL is meaningful via
85
+ * {@link deriveJsRedirectTarget} — `about:blank`, identity values,
86
+ * case-only or trailing-slash variants are all filtered there, not
87
+ * here.
88
+ */
89
+ postNavigationUrl?: string;
90
+ };
22
91
  /**
23
92
  * Configuration options that control crawler behavior.
24
93
  *
@@ -65,6 +134,19 @@ export interface CrawlerOptions extends Required<Pick<ParseURLOptions, 'disableQ
65
134
  * resource-reuse optimization. See {@link ResourceLookup}.
66
135
  */
67
136
  lookupResource: ResourceLookup | null;
137
+ /**
138
+ * Lookup for an already-persisted page's `source` column, or `null` when
139
+ * lineage propagation across sessions is not required. See
140
+ * {@link PageSourceLookup}.
141
+ *
142
+ * Injected by the orchestrator so that `#scrapePage` can resolve the
143
+ * parent's source on `--resume` / `--retry-failed` paths, where
144
+ * `inventoryMode` is not persisted but the page's `source` column is.
145
+ * Without this, sub-resources captured during a re-render of an
146
+ * inventory-labelled page would fall back to the DB DEFAULT `'crawled'`
147
+ * and lose their `'inventory-discovered'` provenance.
148
+ */
149
+ lookupPageSource: PageSourceLookup | null;
68
150
  /**
69
151
  * When non-null, the crawler is running in `--inventory` mode. New page
70
152
  * rows whose URL matches `seedUrls` are labelled `'inventory-seed'`;
@@ -99,6 +181,22 @@ export interface InventoryMode {
99
181
  * @returns The recorded resource data, or `null` when no row matches.
100
182
  */
101
183
  export type ResourceLookup = (urls: readonly string[]) => Promise<ResourceLookupResult | null>;
184
+ /**
185
+ * Looks up the `source` column of a previously persisted page by URL.
186
+ *
187
+ * Returns `undefined` when no row matches (e.g. a freshly-discovered URL
188
+ * that has not been INSERTed yet) so the caller can fall through to its
189
+ * default behaviour.
190
+ *
191
+ * Used by {@link Crawler} during sub-resource lineage propagation:
192
+ * `#scrapePage` consults this once per page to resolve the parent's
193
+ * lineage when the in-memory `inventoryMode` is unavailable (i.e. on
194
+ * `--resume` / `--retry-failed` sessions where inventory state lives only
195
+ * in the DB).
196
+ * @param urlWithoutHashAndAuth - The URL key (`url.withoutHashAndAuth` form) to look up.
197
+ * @returns The recorded `source`, or `undefined` when no matching row exists.
198
+ */
199
+ export type PageSourceLookup = (urlWithoutHashAndAuth: string) => Promise<PageSource | undefined>;
102
200
  /**
103
201
  * Minimal sub-resource data needed to synthesize {@link PageData}
104
202
  * without performing a network fetch.
@@ -236,5 +334,14 @@ export interface CrawlerEventTypes {
236
334
  redirect: {
237
335
  /** HEAD-resolved page data carrying the redirect chain (source → destination). */
238
336
  result: PageData;
337
+ /**
338
+ * Inventory provenance for the redirect-edge call. Forwarded by the
339
+ * orchestrator to `Archive.setRedirect` → `Database.recordRedirect`
340
+ * so brand-new destination rows INSERTed by the edge-only path pick
341
+ * up the inventory label (and propagate it to intermediates) when
342
+ * the originating chain is in the inventory chain. `undefined` keeps
343
+ * the DB DEFAULT `'crawled'`.
344
+ */
345
+ source: PageSource | undefined;
239
346
  };
240
347
  }
@@ -89,13 +89,13 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
89
89
  * Sets up event listeners on the crawler, starts crawling, and resolves
90
90
  * when the crawl completes. Discovered pages, external pages, skipped pages,
91
91
  * and resources are forwarded to the archive for storage.
92
- * @param list - The list of parsed URLs to crawl. The first URL is used as the root.
92
+ * @param list - The list of parsed URLs to crawl. May be empty when a resumed
93
+ * session already has pending pages queued (for example `--retry-failed`).
93
94
  * @param opts - Optional crawl overrides.
94
95
  * @param opts.recursive - Whether discovered URLs are followed. Defaults to
95
96
  * `!fromList` (recursive unless the archive was created from a URL list), so
96
97
  * existing callers keep their behaviour; the retry flow passes it explicitly.
97
98
  * @returns A promise that resolves when crawling is complete.
98
- * @throws {Error} If the URL list is empty.
99
99
  */
100
100
  crawling(list: ExURL[], opts?: {
101
101
  recursive?: boolean;
@@ -200,10 +200,20 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
200
200
  * @param inventoryUrls - Pre-read URL list (one URL per element).
201
201
  * @param options - Optional config overrides — most callers leave this blank and let the archived config flow through.
202
202
  * @param initializedCallback - Hook invoked once the orchestrator is constructed but before `crawling` runs (the CLI uses it to attach progress reporting).
203
+ * @param sourceFileSha256 - **Pre-computed** SHA-256 hex digest of the
204
+ * source URL list. The orchestrator deliberately does NOT receive
205
+ * the file path: the path is privacy-sensitive (leaks user-home /
206
+ * OS structure when archives are shared) and we want it lifted off
207
+ * this boundary so no future log line / breadcrumb / error message
208
+ * inside the orchestrator can accidentally re-leak it. The CLI
209
+ * computes the digest via `computeFileSha256(resolvedListFile)`
210
+ * and passes it through here. Pass `null` for programmatic
211
+ * callers that built `inventoryUrls` in-memory; the audit row's
212
+ * `source_file_sha256` column will be `NULL`.
203
213
  * @returns The orchestrator instance after a successful inventory pass.
204
214
  * @throws {Error} When `inventoryUrls` is empty, the archive is in list mode, or pending URLs from a previous crawl remain unresolved.
205
215
  */
206
- static inventory(archivePath: string, inventoryUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback): Promise<CrawlerOrchestrator>;
216
+ static inventory(archivePath: string, inventoryUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback, sourceFileSha256?: string | null): Promise<CrawlerOrchestrator>;
207
217
  /**
208
218
  * Re-fetch previously-failed pages in an existing `.nitpicker` archive.
209
219
  *