@nitpicker/crawler 0.9.0 → 0.11.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 (92) hide show
  1. package/lib/archive/archive-accessor.d.ts +87 -14
  2. package/lib/archive/archive-accessor.js +162 -36
  3. package/lib/archive/archive.d.ts +147 -24
  4. package/lib/archive/archive.js +252 -86
  5. package/lib/archive/database.d.ts +209 -25
  6. package/lib/archive/database.js +928 -108
  7. package/lib/archive/filesystem/peek-tar-top-dir.d.ts +28 -0
  8. package/lib/archive/filesystem/peek-tar-top-dir.js +65 -0
  9. package/lib/archive/init-schema.d.ts +53 -2
  10. package/lib/archive/init-schema.js +247 -15
  11. package/lib/archive/meta/assert-compatible-version.d.ts +39 -0
  12. package/lib/archive/meta/assert-compatible-version.js +72 -0
  13. package/lib/archive/meta/classify-jsonld-type.d.ts +23 -0
  14. package/lib/archive/meta/classify-jsonld-type.js +43 -0
  15. package/lib/archive/meta/compare-semver.d.ts +23 -0
  16. package/lib/archive/meta/compare-semver.js +51 -0
  17. package/lib/archive/meta/compute-page-denormalized.d.ts +21 -0
  18. package/lib/archive/meta/compute-page-denormalized.js +35 -0
  19. package/lib/archive/meta/derive-flat-from-meta.d.ts +35 -0
  20. package/lib/archive/meta/derive-flat-from-meta.js +158 -0
  21. package/lib/archive/meta/derive-meta-extras.d.ts +20 -0
  22. package/lib/archive/meta/derive-meta-extras.js +23 -0
  23. package/lib/archive/meta/extract-tags-for-archive.d.ts +18 -0
  24. package/lib/archive/meta/extract-tags-for-archive.js +36 -0
  25. package/lib/archive/meta/summarize-jsonld.d.ts +17 -0
  26. package/lib/archive/meta/summarize-jsonld.js +29 -0
  27. package/lib/archive/meta/summarize-tags.d.ts +16 -0
  28. package/lib/archive/meta/summarize-tags.js +33 -0
  29. package/lib/archive/meta/types.d.ts +207 -0
  30. package/lib/archive/meta/types.js +33 -0
  31. package/lib/archive/migrate-crawl-errors.d.ts +20 -0
  32. package/lib/archive/migrate-crawl-errors.js +38 -0
  33. package/lib/archive/migrate-html-blob-tables.d.ts +24 -0
  34. package/lib/archive/migrate-html-blob-tables.js +53 -0
  35. package/lib/archive/migrate-page-errors.d.ts +16 -0
  36. package/lib/archive/migrate-page-errors.js +35 -0
  37. package/lib/archive/migrate-pages-resources-source.d.ts +16 -0
  38. package/lib/archive/migrate-pages-resources-source.js +46 -0
  39. package/lib/archive/page.d.ts +187 -49
  40. package/lib/archive/page.js +258 -63
  41. package/lib/archive/peek-archive-lock.d.ts +40 -0
  42. package/lib/archive/peek-archive-lock.js +62 -0
  43. package/lib/archive/resolve-redirect-chain.d.ts +33 -0
  44. package/lib/archive/resolve-redirect-chain.js +27 -0
  45. package/lib/archive/types.d.ts +135 -26
  46. package/lib/crawler/close-browser-safely.d.ts +64 -0
  47. package/lib/crawler/close-browser-safely.js +73 -0
  48. package/lib/crawler/crawler.d.ts +4 -1
  49. package/lib/crawler/crawler.js +290 -32
  50. package/lib/crawler/create-change-phase-handler.d.ts +54 -0
  51. package/lib/crawler/create-change-phase-handler.js +44 -0
  52. package/lib/crawler/derive-page-source.d.ts +23 -0
  53. package/lib/crawler/derive-page-source.js +28 -0
  54. package/lib/crawler/derive-resource-source.d.ts +23 -0
  55. package/lib/crawler/derive-resource-source.js +26 -0
  56. package/lib/crawler/drain-phase-errors.d.ts +48 -0
  57. package/lib/crawler/drain-phase-errors.js +35 -0
  58. package/lib/crawler/fetch-destination.js +38 -2
  59. package/lib/crawler/format-crawl-progress.d.ts +12 -3
  60. package/lib/crawler/format-crawl-progress.js +14 -6
  61. package/lib/crawler/handle-browser-close.d.ts +29 -0
  62. package/lib/crawler/handle-browser-close.js +28 -0
  63. package/lib/crawler/is-html-content-type.d.ts +17 -0
  64. package/lib/crawler/is-html-content-type.js +19 -0
  65. package/lib/crawler/is-likely-html-url.d.ts +22 -0
  66. package/lib/crawler/is-likely-html-url.js +65 -0
  67. package/lib/crawler/kill-process-tree.d.ts +94 -0
  68. package/lib/crawler/kill-process-tree.js +178 -0
  69. package/lib/crawler/link-list.js +2 -1
  70. package/lib/crawler/link-to-page-data.d.ts +13 -5
  71. package/lib/crawler/link-to-page-data.js +26 -5
  72. package/lib/crawler/log-undrained-phase-errors.d.ts +37 -0
  73. package/lib/crawler/log-undrained-phase-errors.js +34 -0
  74. package/lib/crawler/normalize-content-type.d.ts +14 -0
  75. package/lib/crawler/normalize-content-type.js +20 -0
  76. package/lib/crawler/partition-urls-by-html.d.ts +16 -0
  77. package/lib/crawler/partition-urls-by-html.js +23 -0
  78. package/lib/crawler/redirect-dest-key.d.ts +19 -0
  79. package/lib/crawler/redirect-dest-key.js +27 -0
  80. package/lib/crawler/resource-to-page-data.d.ts +28 -0
  81. package/lib/crawler/resource-to-page-data.js +59 -0
  82. package/lib/crawler/types.d.ts +122 -1
  83. package/lib/crawler-orchestrator.d.ts +93 -1
  84. package/lib/crawler-orchestrator.js +389 -12
  85. package/lib/crawler.d.ts +5 -0
  86. package/lib/crawler.js +3 -0
  87. package/lib/resource-row-to-lookup-result.d.ts +13 -0
  88. package/lib/resource-row-to-lookup-result.js +20 -0
  89. package/lib/types.d.ts +11 -1
  90. package/lib/utils/object/parse-response-headers.d.ts +12 -0
  91. package/lib/utils/object/parse-response-headers.js +26 -0
  92. package/package.json +4 -4
@@ -1,6 +1,24 @@
1
+ import type { PageSource } from '../archive/types.js';
1
2
  import type { PageData, CrawlerError, Resource } from '../utils/types/types.js';
2
- import type { ChangePhaseEvent } from '@d-zero/beholder';
3
+ import type { ChangePhaseEvent, ScrapeResult } from '@d-zero/beholder';
3
4
  import type { ParseURLOptions } from '@d-zero/shared/parse-url';
5
+ /**
6
+ * Result of resolving a URL that redirects to a destination already rendered
7
+ * during this crawl (#73). The crawler records the redirect edge only and skips
8
+ * launching the browser, so the destination is never re-rendered.
9
+ */
10
+ export interface RedirectEdgeResult {
11
+ /** Discriminant marking this as a redirect-edge-only outcome. */
12
+ type: 'redirect-edge';
13
+ /** HEAD-resolved page data carrying the redirect chain (source → destination). */
14
+ pageData: PageData;
15
+ }
16
+ /**
17
+ * The outcome of {@link Crawler.#scrapePage}: either a normal scrape result from
18
+ * the browser/HEAD pipeline, or a {@link RedirectEdgeResult} when the URL's
19
+ * redirect destination was already rendered and only the edge needs recording.
20
+ */
21
+ export type ScrapeOutcome = ScrapeResult | RedirectEdgeResult;
4
22
  /**
5
23
  * Configuration options that control crawler behavior.
6
24
  *
@@ -42,6 +60,60 @@ export interface CrawlerOptions extends Required<Pick<ParseURLOptions, 'disableQ
42
60
  userAgent: string;
43
61
  /** Whether to ignore robots.txt restrictions. */
44
62
  ignoreRobots: boolean;
63
+ /**
64
+ * Lookup for previously captured sub-resources, or `null` to disable the
65
+ * resource-reuse optimization. See {@link ResourceLookup}.
66
+ */
67
+ lookupResource: ResourceLookup | null;
68
+ /**
69
+ * When non-null, the crawler is running in `--inventory` mode. New page
70
+ * rows whose URL matches `seedUrls` are labelled `'inventory-seed'`;
71
+ * every other newly-inserted page or sub-resource is labelled
72
+ * `'inventory-discovered'`. When `null`, no source label is emitted —
73
+ * the DB DEFAULT `'crawled'` applies.
74
+ */
75
+ inventoryMode: InventoryMode | null;
76
+ }
77
+ /**
78
+ * Inventory-mode runtime configuration. Passed from
79
+ * `CrawlerOrchestrator.inventory` into the Crawler so the emit pipeline can
80
+ * label new rows with the correct {@link PageSource}.
81
+ */
82
+ export interface InventoryMode {
83
+ /**
84
+ * URLs explicitly listed in the user-supplied URL file, keyed by their
85
+ * `withoutHashAndAuth` form (so credentials in the URL don't break the
86
+ * match). Membership decides `inventory-seed` vs `inventory-discovered`
87
+ * for HTML pages.
88
+ */
89
+ seedUrls: ReadonlySet<string>;
90
+ }
91
+ /**
92
+ * Looks up a previously captured sub-resource by URL.
93
+ *
94
+ * Injected by the orchestrator so that the crawler can reuse network data
95
+ * recorded during page rendering instead of issuing a redundant HEAD
96
+ * pre-flight request. Implementations must serialize the read against any
97
+ * pending resource writes (e.g., via the orchestrator's WriteQueue).
98
+ * @param urls - URL candidates to match (e.g., with and without auth credentials).
99
+ * @returns The recorded resource data, or `null` when no row matches.
100
+ */
101
+ export type ResourceLookup = (urls: readonly string[]) => Promise<ResourceLookupResult | null>;
102
+ /**
103
+ * Minimal sub-resource data needed to synthesize {@link PageData}
104
+ * without performing a network fetch.
105
+ */
106
+ export interface ResourceLookupResult {
107
+ /** HTTP status code of the recorded response, or `null` if unknown. */
108
+ status: number | null;
109
+ /** HTTP status text of the recorded response, or `null` if unknown. */
110
+ statusText: string | null;
111
+ /** The Content-Type header value (media type only), or `null` if unknown. */
112
+ contentType: string | null;
113
+ /** The Content-Length header value in bytes, or `null` if unknown. */
114
+ contentLength: number | null;
115
+ /** Raw HTTP response headers, or `null` if unavailable. */
116
+ responseHeaders: Record<string, string | string[] | undefined> | null;
45
117
  }
46
118
  /**
47
119
  * Describes a detected pagination pattern between two consecutive URLs.
@@ -67,6 +139,12 @@ export interface CrawlerEventTypes {
67
139
  page: {
68
140
  /** The scraped page data including HTML, metadata, anchors, and images. */
69
141
  result: PageData;
142
+ /**
143
+ * Inventory provenance to write to `pages.source` when this row is new.
144
+ * `undefined` means the DB default (`'crawled'`) applies, which is the
145
+ * common case outside `crawl --inventory`. See {@link PageSource}.
146
+ */
147
+ source?: PageSource;
70
148
  };
71
149
  /**
72
150
  * Emitted when an external page (outside the crawl scope) has been scraped.
@@ -74,6 +152,8 @@ export interface CrawlerEventTypes {
74
152
  externalPage: {
75
153
  /** The scraped page data for the external page. */
76
154
  result: PageData;
155
+ /** Inventory provenance for new rows — see {@link CrawlerEventTypes.page.source}. */
156
+ source?: PageSource;
77
157
  };
78
158
  /**
79
159
  * Emitted when a URL is skipped due to exclusion rules, robots.txt restrictions,
@@ -93,6 +173,14 @@ export interface CrawlerEventTypes {
93
173
  response: {
94
174
  /** The captured resource data including URL, status, content type, and headers. */
95
175
  resource: Resource;
176
+ /**
177
+ * Inventory provenance to write to `resources.source` when this row is new.
178
+ * Sub-resources discovered while puppeteer renders an inventory-seed
179
+ * page are always `'inventory-discovered'` (a sub-resource is never
180
+ * itself a seed). `undefined` means the DB default (`'crawled'`)
181
+ * applies. See {@link PageSource}.
182
+ */
183
+ source?: PageSource;
96
184
  };
97
185
  /**
98
186
  * Emitted to record the relationship between a page and a resource it references.
@@ -116,4 +204,37 @@ export interface CrawlerEventTypes {
116
204
  * (e.g., scrapeStart, headRequest, openPage, success).
117
205
  */
118
206
  changePhase: ChangePhaseEvent;
207
+ /**
208
+ * Emitted when a secondary scrape step fails for a URL but the page itself
209
+ * is otherwise scraped successfully (e.g. a viewport switch in
210
+ * `#fetchImages` detaches the frame and `@retryable` gives up). The
211
+ * orchestrator persists these as `page_errors` rows so the failure is
212
+ * visible in the archive instead of being lost to stdout logs.
213
+ *
214
+ * For ordering, this event is always emitted AFTER `page` / `externalPage`
215
+ * for the same URL, so the orchestrator's WriteQueue serialises the
216
+ * `pages` upsert before the `page_errors` insert and the FK resolution
217
+ * via URL succeeds.
218
+ */
219
+ pageError: {
220
+ /** URL of the affected page. */
221
+ url: string;
222
+ /** Scrape phase name (typically `'retryExhausted'`). */
223
+ phase: string;
224
+ /** Human-readable failure message. */
225
+ message: string;
226
+ /** Whether the URL is external to the crawl scope. */
227
+ isExternal: boolean;
228
+ };
229
+ /**
230
+ * Emitted when a URL redirects to a destination that has already been
231
+ * rendered during this crawl, so only the redirect edge is recorded and the
232
+ * destination is not re-rendered (#73). The orchestrator persists this via
233
+ * `Archive.setRedirect`, which writes the edge without overwriting the
234
+ * destination's content.
235
+ */
236
+ redirect: {
237
+ /** HEAD-resolved page data carrying the redirect chain (source → destination). */
238
+ result: PageData;
239
+ };
119
240
  }
@@ -1,4 +1,5 @@
1
1
  import type { Config } from './archive/types.js';
2
+ import type { InventoryMode } from './crawler/types.js';
2
3
  import type { CrawlEvent } from './types.js';
3
4
  import type { ExURL } from '@d-zero/shared/parse-url';
4
5
  import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
@@ -38,6 +39,12 @@ interface CrawlConfig extends Config {
38
39
  userAgent: string;
39
40
  /** Whether to ignore robots.txt restrictions. */
40
41
  ignoreRobots: boolean;
42
+ /**
43
+ * Inventory-mode runtime configuration (see {@link InventoryMode}). Set
44
+ * by {@link CrawlerOrchestrator.inventory}; the default crawl path leaves
45
+ * this `null` so new rows are labelled `'crawled'` by the DB DEFAULT.
46
+ */
47
+ inventoryMode: InventoryMode | null;
41
48
  }
42
49
  /**
43
50
  * Callback invoked after the CrawlerOrchestrator instance is fully initialized
@@ -83,10 +90,16 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
83
90
  * when the crawl completes. Discovered pages, external pages, skipped pages,
84
91
  * and resources are forwarded to the archive for storage.
85
92
  * @param list - The list of parsed URLs to crawl. The first URL is used as the root.
93
+ * @param opts - Optional crawl overrides.
94
+ * @param opts.recursive - Whether discovered URLs are followed. Defaults to
95
+ * `!fromList` (recursive unless the archive was created from a URL list), so
96
+ * existing callers keep their behaviour; the retry flow passes it explicitly.
86
97
  * @returns A promise that resolves when crawling is complete.
87
98
  * @throws {Error} If the URL list is empty.
88
99
  */
89
- crawling(list: ExURL[]): Promise<void>;
100
+ crawling(list: ExURL[], opts?: {
101
+ recursive?: boolean;
102
+ }): Promise<void>;
90
103
  /**
91
104
  * Kill any zombie Chromium processes that were not properly cleaned up.
92
105
  *
@@ -146,6 +159,85 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
146
159
  * @throws {Error} When `newUrls` is empty, the archive is in list mode, or it cannot be parsed.
147
160
  */
148
161
  static append(archivePath: string, newUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback): Promise<CrawlerOrchestrator>;
162
+ /**
163
+ * Inventory mode: cross-reference a user-supplied URL list against an
164
+ * existing `.nitpicker` archive and import ONLY the URLs that are not yet
165
+ * tracked there. Designed to surface "orphan" landing pages that link
166
+ * graph traversal could not reach, and "unused" server-side files that
167
+ * no crawled page references — both of which the
168
+ * `listIsolatedPages` / `listUnusedResources` queries can then list.
169
+ *
170
+ * Flow:
171
+ *
172
+ * 1. Open the archive (writer mode, takes the archive lock).
173
+ * 2. Reject list-mode archives — they hold metadata-only rows that
174
+ * inventory has no business touching.
175
+ * 3. Reject archives with unfinished `pending` URLs — those would inherit
176
+ * the inventory `source` label by mistake. Operator must resume /
177
+ * retry-failed first.
178
+ * 4. Parse the URL list. Anything outside the archived scope is warned
179
+ * and skipped (inventory is per-server by design).
180
+ * 5. Subtract URLs that already exist in `pages` or `resources` so the
181
+ * second (and N-th) inventory pass is a no-op for known rows — keeps
182
+ * `'inventory-seed'` rows from being silently demoted.
183
+ * 6. Make `<archive>.bak`. Anything thrown beyond this point restores
184
+ * from the backup.
185
+ * 7. HEAD-probe each novel URL. Responses classified as HTML are queued
186
+ * as Crawler seeds (`'inventory-seed'`); everything else is recorded
187
+ * in `resources` directly as `'inventory-seed'` (no browser launch).
188
+ * 8. If any HTML seeds exist, start a Crawler with
189
+ * `inventoryMode = { seedUrls }` so the rendered page and every newly
190
+ * discovered downstream link is labelled correctly. `resume` is fed
191
+ * the existing `scraped` / `resources` sets so links into already-
192
+ * crawled pages stop at the seen-gate without re-rendering.
193
+ * 9. Drop the backup on success; restore it on any throw.
194
+ *
195
+ * Mutually exclusive with `--append` / `--retry-failed` / `--resume` /
196
+ * `--diff` / `--list` / `--list-file` / `--single` / `--output` — the
197
+ * CLI dispatch enforces this; this method assumes the caller honoured
198
+ * the contract.
199
+ * @param archivePath - Absolute or cwd-relative path to the `.nitpicker` archive.
200
+ * @param inventoryUrls - Pre-read URL list (one URL per element).
201
+ * @param options - Optional config overrides — most callers leave this blank and let the archived config flow through.
202
+ * @param initializedCallback - Hook invoked once the orchestrator is constructed but before `crawling` runs (the CLI uses it to attach progress reporting).
203
+ * @returns The orchestrator instance after a successful inventory pass.
204
+ * @throws {Error} When `inventoryUrls` is empty, the archive is in list mode, or pending URLs from a previous crawl remain unresolved.
205
+ */
206
+ static inventory(archivePath: string, inventoryUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback): Promise<CrawlerOrchestrator>;
207
+ /**
208
+ * Re-fetch previously-failed pages in an existing `.nitpicker` archive.
209
+ *
210
+ * Opens the archive, resets every page whose previous attempt ended in a
211
+ * recoverable failure (missing status / content type, or a 5xx status — see
212
+ * {@link Archive.resetFailedPages}) back to pending, and resumes crawling.
213
+ * The archived crawl configuration is reused — scopes, excludes, keywords,
214
+ * user agent, etc. — so the retry honours the original crawl boundaries
215
+ * unless a field is explicitly overridden via `options`. The exception is
216
+ * `recursive`: it is taken from `options` (the CLI flag defaults it to
217
+ * `true`) rather than inherited from the archive, so a retry decides afresh
218
+ * whether to follow newly-discovered URLs regardless of how the original
219
+ * crawl was run.
220
+ *
221
+ * When `recursive` is enabled (the default), newly-discovered URLs from the
222
+ * re-fetched pages are followed and crawled from scratch; when disabled, only
223
+ * the failed pages themselves are re-fetched. The archived roots seed the
224
+ * crawl scope while the reset pages are picked up through the resumed pending
225
+ * set, so failed external pages stay external (metadata-only) instead of being
226
+ * promoted into scope, and a failed root is re-fetched in place.
227
+ *
228
+ * A `<archive>.bak` is created before any DB mutation and removed on success;
229
+ * if the crawl throws, the backup is restored to keep the original archive
230
+ * intact.
231
+ *
232
+ * List-mode archives (`info.fromList === true`) are rejected for the same
233
+ * reason as {@link CrawlerOrchestrator.append}: their pages are metadata-only.
234
+ * @param archivePath - Absolute or relative path to the existing `.nitpicker`.
235
+ * @param options - Optional config overrides applied on top of the archived config.
236
+ * @param initializedCallback - Optional callback invoked after initialization but before crawling resumes.
237
+ * @returns The orchestrator instance after the retry crawl completes.
238
+ * @throws {Error} When the archive is in list mode or has no parseable roots.
239
+ */
240
+ static retryFailed(archivePath: string, options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback): Promise<CrawlerOrchestrator>;
149
241
  /**
150
242
  * Resume a previously interrupted crawl from an existing archive file.
151
243
  *