@opencraw/core 0.1.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/README.md +67 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +5302 -0
- package/dist/src/access/access-broker.use-case.d.ts +43 -0
- package/dist/src/access/access-config.error.d.ts +5 -0
- package/dist/src/access/access-config.repository.d.ts +11 -0
- package/dist/src/access/access-json-schema.mapper.d.ts +8 -0
- package/dist/src/access/access-plugin.contract.d.ts +48 -0
- package/dist/src/access/access-preset.store.d.ts +19 -0
- package/dist/src/access/access-profile.contract.d.ts +91 -0
- package/dist/src/access/index.d.ts +11 -0
- package/dist/src/access/render-access.mapper.d.ts +41 -0
- package/dist/src/access/session-id.algorithm.d.ts +8 -0
- package/dist/src/api-steps/extract-from-document.use-case.d.ts +27 -0
- package/dist/src/api-steps/index.d.ts +4 -0
- package/dist/src/api-steps/run-api-step.use-case.d.ts +20 -0
- package/dist/src/api-steps/send-request.use-case.d.ts +22 -0
- package/dist/src/browser-session/browser-session.config.d.ts +21 -0
- package/dist/src/browser-session/browser.client.d.ts +67 -0
- package/dist/src/browser-session/index.d.ts +5 -0
- package/dist/src/crawl-events/crawl-event.contract.d.ts +120 -0
- package/dist/src/crawl-events/event-bus.store.d.ts +10 -0
- package/dist/src/crawl-events/index.d.ts +4 -0
- package/dist/src/crawl-events/trace-line.mapper.d.ts +14 -0
- package/dist/src/crawl-execution/bootstrap-session.use-case.d.ts +55 -0
- package/dist/src/crawl-execution/crawl-options.config.d.ts +37 -0
- package/dist/src/crawl-execution/crawl-report.model.d.ts +25 -0
- package/dist/src/crawl-execution/create-crawler.use-case.d.ts +19 -0
- package/dist/src/crawl-execution/index.d.ts +8 -0
- package/dist/src/crawl-execution/rotating-runner.use-case.d.ts +57 -0
- package/dist/src/crawl-execution/run-crawl.use-case.d.ts +13 -0
- package/dist/src/crawl-execution/run-input-recipe.use-case.d.ts +40 -0
- package/dist/src/extraction-scope/extraction-scope.model.d.ts +73 -0
- package/dist/src/extraction-scope/index.d.ts +5 -0
- package/dist/src/extraction-scope/live-element.model.d.ts +28 -0
- package/dist/src/hooks/hook-registry.store.d.ts +16 -0
- package/dist/src/hooks/hook.contract.d.ts +17 -0
- package/dist/src/hooks/hook.error.d.ts +7 -0
- package/dist/src/hooks/index.d.ts +4 -0
- package/dist/src/http-session/http-response.contract.d.ts +44 -0
- package/dist/src/http-session/http.client.d.ts +42 -0
- package/dist/src/http-session/index.d.ts +5 -0
- package/dist/src/index.d.ts +24 -0
- package/dist/src/output-mapping/coerce-field.mapper.d.ts +19 -0
- package/dist/src/output-mapping/generated-field.mapper.d.ts +16 -0
- package/dist/src/output-mapping/index.d.ts +9 -0
- package/dist/src/output-mapping/map-record.use-case.d.ts +27 -0
- package/dist/src/output-mapping/mapping.error.d.ts +17 -0
- package/dist/src/output-mapping/missing-value.policy.d.ts +14 -0
- package/dist/src/output-mapping/output-field.validator.d.ts +10 -0
- package/dist/src/output-mapping/output-record.model.d.ts +20 -0
- package/dist/src/recipe-loading/index.d.ts +9 -0
- package/dist/src/recipe-loading/load-recipe-set.use-case.d.ts +45 -0
- package/dist/src/recipe-loading/read-recipe-source.use-case.d.ts +14 -0
- package/dist/src/recipe-loading/recipe-binding.error.d.ts +14 -0
- package/dist/src/recipe-loading/recipe-binding.validator.d.ts +32 -0
- package/dist/src/recipe-loading/recipe-file.repository.d.ts +11 -0
- package/dist/src/recipe-loading/recipe-set.model.d.ts +12 -0
- package/dist/src/recipe-loading/recipe-source.contract.d.ts +28 -0
- package/dist/src/recipe-loading/recipe-text.mapper.d.ts +14 -0
- package/dist/src/recipe-schema/index.d.ts +15 -0
- package/dist/src/recipe-schema/input-recipe.contract.d.ts +103 -0
- package/dist/src/recipe-schema/json-schema.mapper.d.ts +15 -0
- package/dist/src/recipe-schema/output-recipe.contract.d.ts +47 -0
- package/dist/src/recipe-schema/recipe-kind.enum.d.ts +31 -0
- package/dist/src/recipe-schema/recipe-validation.error.d.ts +17 -0
- package/dist/src/recipe-schema/recipe.validator.d.ts +28 -0
- package/dist/src/recipe-schema/step.contract.d.ts +152 -0
- package/dist/src/recipe-schema/transform-rule.contract.d.ts +120 -0
- package/dist/src/record-sink/dedupe.policy.d.ts +17 -0
- package/dist/src/record-sink/index.d.ts +8 -0
- package/dist/src/record-sink/json-lines-sink.repository.d.ts +19 -0
- package/dist/src/record-sink/memory-sink.repository.d.ts +9 -0
- package/dist/src/record-sink/record-sink.contract.d.ts +17 -0
- package/dist/src/selection/html-selector.algorithm.d.ts +18 -0
- package/dist/src/selection/index.d.ts +8 -0
- package/dist/src/selection/json-path.algorithm.d.ts +9 -0
- package/dist/src/selection/json-text.algorithm.d.ts +31 -0
- package/dist/src/selection/regex.algorithm.d.ts +12 -0
- package/dist/src/selection/take-value.mapper.d.ts +27 -0
- package/dist/src/step-flow/block-rule.policy.d.ts +21 -0
- package/dist/src/step-flow/blocked.error.d.ts +9 -0
- package/dist/src/step-flow/for-each.use-case.d.ts +18 -0
- package/dist/src/step-flow/index.d.ts +10 -0
- package/dist/src/step-flow/paginate.use-case.d.ts +17 -0
- package/dist/src/step-flow/retry.policy.d.ts +21 -0
- package/dist/src/step-flow/run-gate.policy.d.ts +39 -0
- package/dist/src/step-flow/run-steps.use-case.d.ts +39 -0
- package/dist/src/step-flow/step-failure.error.d.ts +14 -0
- package/dist/src/step-flow/step-runner.contract.d.ts +39 -0
- package/dist/src/template/expression.algorithm.d.ts +53 -0
- package/dist/src/template/index.d.ts +7 -0
- package/dist/src/template/template.algorithm.d.ts +53 -0
- package/dist/src/template/value-path.algorithm.d.ts +29 -0
- package/dist/src/template/value-text.algorithm.d.ts +18 -0
- package/dist/src/transformation/apply-transform-chain.use-case.d.ts +17 -0
- package/dist/src/transformation/collection.algorithm.d.ts +15 -0
- package/dist/src/transformation/currency.algorithm.d.ts +15 -0
- package/dist/src/transformation/date.algorithm.d.ts +13 -0
- package/dist/src/transformation/index.d.ts +10 -0
- package/dist/src/transformation/lookup.algorithm.d.ts +24 -0
- package/dist/src/transformation/number.algorithm.d.ts +30 -0
- package/dist/src/transformation/string.algorithm.d.ts +18 -0
- package/dist/src/transformation/transform-registry.store.d.ts +36 -0
- package/dist/src/transformation/transform.error.d.ts +8 -0
- package/dist/src/transformation/url.algorithm.d.ts +20 -0
- package/dist/src/web-steps/evaluate-script.use-case.d.ts +10 -0
- package/dist/src/web-steps/extract-from-page.use-case.d.ts +10 -0
- package/dist/src/web-steps/index.d.ts +2 -0
- package/dist/src/web-steps/interact.use-case.d.ts +36 -0
- package/dist/src/web-steps/navigate.use-case.d.ts +14 -0
- package/dist/src/web-steps/run-web-step.use-case.d.ts +22 -0
- package/dist/src/web-steps/snapshot-elements.use-case.d.ts +13 -0
- package/package.json +75 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AccessLease } from '../access/index.js';
|
|
2
|
+
import type { BrowserClient, BrowserSession, SessionOptions, StorageState } from '../browser-session/index.js';
|
|
3
|
+
import type { EventBus } from '../crawl-events/index.js';
|
|
4
|
+
import type { HookRegistry } from '../hooks/index.js';
|
|
5
|
+
import type { InputRecipe } from '../recipe-schema/index.js';
|
|
6
|
+
export interface BootstrapDependencies {
|
|
7
|
+
/** Launches (or returns) the shared browser; only called when a browser is needed. */
|
|
8
|
+
browser: () => Promise<BrowserClient>;
|
|
9
|
+
hooks: HookRegistry;
|
|
10
|
+
events: EventBus;
|
|
11
|
+
storageStateDir?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The session options an access lease contributes: its proxy, TLS leniency,
|
|
15
|
+
* blocked resources, and its headers under the recipe's own.
|
|
16
|
+
*
|
|
17
|
+
* @param lease - The lease, if any.
|
|
18
|
+
* @param headers - The recipe's `session.headers`.
|
|
19
|
+
* @returns Options for `BrowserClient.newSession`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function accessOptions(lease: AccessLease | undefined, headers: Record<string, string> | undefined): Pick<SessionOptions, 'proxy' | 'ignoreHTTPSErrors' | 'blockResources' | 'headers'>;
|
|
22
|
+
/**
|
|
23
|
+
* The storage state a recipe starts from: a saved file, else what its
|
|
24
|
+
* `session.bootstrap` steps produce in a browser (filtered by `keep`, saved
|
|
25
|
+
* when asked), else nothing.
|
|
26
|
+
*
|
|
27
|
+
* The bootstrap runs through the same access lease as the crawl that follows,
|
|
28
|
+
* so a login and the requests that use its cookies come from one IP.
|
|
29
|
+
*
|
|
30
|
+
* @param recipe - The input recipe.
|
|
31
|
+
* @param deps - Browser, hooks, events.
|
|
32
|
+
* @param lease - The recipe run's access; direct when omitted.
|
|
33
|
+
* @returns The state, or `undefined` when the recipe declares none.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveStorageState(recipe: InputRecipe, deps: BootstrapDependencies, lease?: AccessLease): Promise<StorageState | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* The storage state saved by an earlier bootstrap (`session.storageStatePath`), if the recipe names one.
|
|
38
|
+
*
|
|
39
|
+
* @param recipe - The input recipe.
|
|
40
|
+
* @param deps - For `storageStateDir`.
|
|
41
|
+
* @returns The state, or `undefined`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readSavedState(recipe: InputRecipe, deps: Pick<BootstrapDependencies, 'storageStateDir'>): Promise<StorageState | undefined>;
|
|
44
|
+
/**
|
|
45
|
+
* Runs a recipe's bootstrap steps in a browser session, then captures what
|
|
46
|
+
* `keep` lists and saves it when `saveTo` asks. The session stays open: a
|
|
47
|
+
* remote browser keeps it for the crawl, a local one is closed by the caller.
|
|
48
|
+
*
|
|
49
|
+
* @param recipe - An input recipe with `session.bootstrap`.
|
|
50
|
+
* @param browserSession - Where the steps run.
|
|
51
|
+
* @param deps - Hooks, events, `storageStateDir`.
|
|
52
|
+
* @returns The kept state.
|
|
53
|
+
*/
|
|
54
|
+
export declare function runBootstrap(recipe: InputRecipe, browserSession: BrowserSession, deps: Omit<BootstrapDependencies, 'browser'>): Promise<StorageState>;
|
|
55
|
+
//# sourceMappingURL=bootstrap-session.use-case.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AccessConfig, AccessPlugin } from '../access/index.js';
|
|
2
|
+
import type { BrowserSessionConfig } from '../browser-session/index.js';
|
|
3
|
+
import type { CrawlListener } from '../crawl-events/index.js';
|
|
4
|
+
import type { HookMap } from '../hooks/index.js';
|
|
5
|
+
import type { DedupeScope, RecordSink } from '../record-sink/index.js';
|
|
6
|
+
/** How a crawler is created. Everything is optional. */
|
|
7
|
+
export interface CrawlOptions {
|
|
8
|
+
/** Browser launch settings for web recipes and bootstraps. */
|
|
9
|
+
browser?: BrowserSessionConfig;
|
|
10
|
+
/** Handlers recipes reference by name. */
|
|
11
|
+
hooks?: HookMap;
|
|
12
|
+
/** Where records go; default: kept in memory and returned in the report. */
|
|
13
|
+
sink?: RecordSink;
|
|
14
|
+
onEvent?: CrawlListener;
|
|
15
|
+
/** Default `run`: a key seen once is dropped for the rest of the run. */
|
|
16
|
+
dedupe?: DedupeScope;
|
|
17
|
+
/** Whether a failed input recipe stops the run; default `continue`. */
|
|
18
|
+
onRecipeError?: 'continue' | 'stop';
|
|
19
|
+
/** Base directory for relative `storageStatePath` and `saveTo` values. */
|
|
20
|
+
storageStateDir?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Skip records whose key the sink already has (`sink.has`), reporting them as
|
|
23
|
+
* `skipped`. Needs a sink that can answer, such as `jsonLinesSink(path, { append: true })`.
|
|
24
|
+
*/
|
|
25
|
+
resume?: boolean;
|
|
26
|
+
/** Attach the scope snapshot to `record:emit` and `record:reject` events, for inspecting what a mapping saw. */
|
|
27
|
+
debug?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Where traffic goes: named proxy profiles (presets for common providers,
|
|
30
|
+
* credentials as `{{env.NAME}}`) and the default one. Recipes pick a profile
|
|
31
|
+
* with `session.access.profile`. Without it every recipe goes direct.
|
|
32
|
+
*/
|
|
33
|
+
access?: AccessConfig;
|
|
34
|
+
/** Plugins `{ kind: 'plugin', name }` profiles refer to. */
|
|
35
|
+
accessPlugins?: AccessPlugin[];
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=crawl-options.config.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SinkSummary } from '../record-sink/index.js';
|
|
2
|
+
/** What one input recipe did. */
|
|
3
|
+
export interface RecipeReport {
|
|
4
|
+
recipeId: string;
|
|
5
|
+
mode: 'web' | 'api';
|
|
6
|
+
emitted: number;
|
|
7
|
+
rejected: number;
|
|
8
|
+
duplicates: number;
|
|
9
|
+
/** Records a resumed run found in the sink already. */
|
|
10
|
+
skipped: number;
|
|
11
|
+
pages: number;
|
|
12
|
+
durationMs: number;
|
|
13
|
+
/** Set when the recipe stopped on a failure. */
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
/** What a whole run did. */
|
|
17
|
+
export interface CrawlReport {
|
|
18
|
+
outputId: string;
|
|
19
|
+
recipes: RecipeReport[];
|
|
20
|
+
/** Records written to the sink across every recipe. */
|
|
21
|
+
records: number;
|
|
22
|
+
sink: SinkSummary;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=crawl-report.model.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RecipeSet } from '../recipe-loading/index.js';
|
|
2
|
+
import type { CrawlOptions } from './crawl-options.config.js';
|
|
3
|
+
import type { CrawlReport } from './crawl-report.model.js';
|
|
4
|
+
/** A configured engine: run recipe sets, then close it to release the browser. */
|
|
5
|
+
export interface Crawler {
|
|
6
|
+
run: (set: RecipeSet) => Promise<CrawlReport>;
|
|
7
|
+
/** Closes the browser if one was launched. Safe to call more than once. */
|
|
8
|
+
close: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Creates a crawler. The browser is launched lazily, on the first recipe or
|
|
12
|
+
* bootstrap that needs it, and shared by every run until `close`.
|
|
13
|
+
*
|
|
14
|
+
* @param options - Hooks, sink, events, browser settings, access, policies.
|
|
15
|
+
* @returns The crawler.
|
|
16
|
+
* @throws AccessConfigError when the access config cannot work.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createCrawler(options?: CrawlOptions): Crawler;
|
|
19
|
+
//# sourceMappingURL=create-crawler.use-case.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createCrawler } from './create-crawler.use-case.js';
|
|
2
|
+
export type { Crawler } from './create-crawler.use-case.js';
|
|
3
|
+
export type { CrawlOptions } from './crawl-options.config.js';
|
|
4
|
+
export type { CrawlReport, RecipeReport } from './crawl-report.model.js';
|
|
5
|
+
export { runInputRecipe } from './run-input-recipe.use-case.js';
|
|
6
|
+
export { runCrawl } from './run-crawl.use-case.js';
|
|
7
|
+
export { resolveStorageState, readSavedState, runBootstrap } from './bootstrap-session.use-case.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AccessLease } from '../access/index.js';
|
|
2
|
+
import type { EventBus } from '../crawl-events/index.js';
|
|
3
|
+
import type { ExtractionScope, LiveElement } from '../extraction-scope/index.js';
|
|
4
|
+
import type { InputRecipe, PaginateNext, Step } from '../recipe-schema/index.js';
|
|
5
|
+
import { BlockedError } from '../step-flow/index.js';
|
|
6
|
+
import type { NextPageResult, StepRunner } from '../step-flow/index.js';
|
|
7
|
+
/** A runner and the access lease it was opened with. */
|
|
8
|
+
export interface LeasedRunner {
|
|
9
|
+
runner: StepRunner;
|
|
10
|
+
lease: AccessLease;
|
|
11
|
+
}
|
|
12
|
+
export interface RotatingRunnerOptions {
|
|
13
|
+
recipe: InputRecipe;
|
|
14
|
+
events: EventBus;
|
|
15
|
+
/** How many rotations the run may use; 0 disables rotation. */
|
|
16
|
+
maxRotations: number;
|
|
17
|
+
/** Opens a runner on a fresh lease: attempt 1 first, then 2, 3... */
|
|
18
|
+
open: (attempt: number) => Promise<LeasedRunner>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The runner a recipe run uses. It delegates to a runner opened on an access
|
|
22
|
+
* lease, reports every block as an `access:blocked` event and, when the recipe
|
|
23
|
+
* allows it, rotates: takes a new lease, opens a new runner on it (re-running
|
|
24
|
+
* the bootstrap) and tells the step walk to retry the blocked step.
|
|
25
|
+
*
|
|
26
|
+
* Under concurrency several iterations can be blocked by the same lease at
|
|
27
|
+
* once. Each block remembers the generation of runner that produced it, so
|
|
28
|
+
* only the first one rotates; the rest just retry on the new runner. Replaced
|
|
29
|
+
* runners are kept until the run ends, because other iterations may still have
|
|
30
|
+
* requests in flight on them.
|
|
31
|
+
*/
|
|
32
|
+
export declare class RotatingRunner implements StepRunner {
|
|
33
|
+
private readonly options;
|
|
34
|
+
/**
|
|
35
|
+
* Opens the first runner.
|
|
36
|
+
*
|
|
37
|
+
* @param options - The recipe, events, rotation budget and how to open a runner.
|
|
38
|
+
* @returns The rotating runner.
|
|
39
|
+
*/
|
|
40
|
+
static open(options: RotatingRunnerOptions): Promise<RotatingRunner>;
|
|
41
|
+
private current;
|
|
42
|
+
private readonly retired;
|
|
43
|
+
private readonly blockedAt;
|
|
44
|
+
private generation;
|
|
45
|
+
private rotations;
|
|
46
|
+
private rotating;
|
|
47
|
+
private constructor();
|
|
48
|
+
private get inner();
|
|
49
|
+
private swap;
|
|
50
|
+
private note;
|
|
51
|
+
runLeaf(step: Step, scope: ExtractionScope): Promise<void>;
|
|
52
|
+
nextPage(next: PaginateNext, scope: ExtractionScope): Promise<NextPageResult>;
|
|
53
|
+
elements(selector: string, scope: ExtractionScope): Promise<LiveElement[]>;
|
|
54
|
+
rotate(error: BlockedError): Promise<boolean>;
|
|
55
|
+
dispose(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=rotating-runner.use-case.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RecipeSet } from '../recipe-loading/index.js';
|
|
2
|
+
import type { CrawlReport } from './crawl-report.model.js';
|
|
3
|
+
import type { RecipeRunDependencies } from './run-input-recipe.use-case.js';
|
|
4
|
+
/**
|
|
5
|
+
* Runs every input recipe of a set, one after another, into one sink.
|
|
6
|
+
*
|
|
7
|
+
* @param set - The bound recipes.
|
|
8
|
+
* @param deps - Shared browser, hooks, events, sink and de-duplication.
|
|
9
|
+
* @param onRecipeError - Whether a failed recipe stops the run.
|
|
10
|
+
* @returns The report.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runCrawl(set: RecipeSet, deps: RecipeRunDependencies, onRecipeError: 'continue' | 'stop'): Promise<CrawlReport>;
|
|
13
|
+
//# sourceMappingURL=run-crawl.use-case.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AccessBroker } from '../access/index.js';
|
|
2
|
+
import { BrowserClient } from '../browser-session/index.js';
|
|
3
|
+
import type { EventBus } from '../crawl-events/index.js';
|
|
4
|
+
import type { HookRegistry } from '../hooks/index.js';
|
|
5
|
+
import type { InputRecipe, OutputRecipe } from '../recipe-schema/index.js';
|
|
6
|
+
import type { DedupePolicy, RecordSink } from '../record-sink/index.js';
|
|
7
|
+
import type { RecipeReport } from './crawl-report.model.js';
|
|
8
|
+
export interface RecipeRunDependencies {
|
|
9
|
+
browser: () => Promise<BrowserClient>;
|
|
10
|
+
hooks: HookRegistry;
|
|
11
|
+
events: EventBus;
|
|
12
|
+
sink: RecordSink;
|
|
13
|
+
dedupe: DedupePolicy;
|
|
14
|
+
storageStateDir?: string;
|
|
15
|
+
/** Accept invalid TLS certificates in api mode too (sandbox proxies); mirrors `browser.ignoreHTTPSErrors`. */
|
|
16
|
+
ignoreHTTPSErrors?: boolean;
|
|
17
|
+
/** Skip records the sink already has (`sink.has`). */
|
|
18
|
+
resume?: boolean;
|
|
19
|
+
/** Attach the scope snapshot to record events. */
|
|
20
|
+
debug?: boolean;
|
|
21
|
+
/** Leases each recipe run its network access. */
|
|
22
|
+
access: AccessBroker;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Runs one input recipe end to end: session, runner, the step walk, and for
|
|
26
|
+
* every emitted scope the mapping, de-duplication and the sink. A step or
|
|
27
|
+
* mapping failure under the `fail` policy ends the recipe and is reported,
|
|
28
|
+
* never thrown: the caller decides whether the run goes on.
|
|
29
|
+
*
|
|
30
|
+
* Emits are serialised through one promise chain whatever the concurrency, so
|
|
31
|
+
* the sink sees one record at a time and `maxRecords` is exact: once reached,
|
|
32
|
+
* every later emit returns `stop` before mapping.
|
|
33
|
+
*
|
|
34
|
+
* @param input - The input recipe.
|
|
35
|
+
* @param output - The output recipe it feeds.
|
|
36
|
+
* @param deps - Shared browser, hooks, events, sink and de-duplication.
|
|
37
|
+
* @returns What happened.
|
|
38
|
+
*/
|
|
39
|
+
export declare function runInputRecipe(input: InputRecipe, output: OutputRecipe, deps: RecipeRunDependencies): Promise<RecipeReport>;
|
|
40
|
+
//# sourceMappingURL=run-input-recipe.use-case.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values a recipe has extracted so far, by id, with the lexical nesting the
|
|
3
|
+
* steps create: `forEach` and `paginate` open a child scope per iteration or page
|
|
4
|
+
* and drop it afterwards, so nothing from one page leaks into the next. A read
|
|
5
|
+
* walks up the chain; a write lands in the scope it is made on.
|
|
6
|
+
*
|
|
7
|
+
* Page state (the current URL, page number and the document `extract` reads by
|
|
8
|
+
* default) is scope state too, bound in the innermost scope that navigated.
|
|
9
|
+
*/
|
|
10
|
+
/** A fetched or rendered document a later `extract` can read. */
|
|
11
|
+
export type ScopeDocument = {
|
|
12
|
+
kind: 'json';
|
|
13
|
+
data: unknown;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'html';
|
|
16
|
+
html: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'text';
|
|
19
|
+
text: string;
|
|
20
|
+
};
|
|
21
|
+
/** Where the crawl is: bound by whichever scope last navigated. */
|
|
22
|
+
export interface PageState {
|
|
23
|
+
url: string;
|
|
24
|
+
number: number;
|
|
25
|
+
document?: ScopeDocument;
|
|
26
|
+
}
|
|
27
|
+
/** Paths a template can read besides ids: `page.url`, `page.number`, `start.url`, `vars.*`. */
|
|
28
|
+
export declare const RESERVED_ROOTS: readonly ["page", "start", "vars"];
|
|
29
|
+
export declare class ExtractionScope {
|
|
30
|
+
private readonly parent?;
|
|
31
|
+
private readonly values;
|
|
32
|
+
private page;
|
|
33
|
+
constructor(parent?: ExtractionScope | undefined);
|
|
34
|
+
/** Opens a nested scope; reads fall through to this one. */
|
|
35
|
+
child(): ExtractionScope;
|
|
36
|
+
/** Binds a value in this scope, shadowing any parent binding of the same id. */
|
|
37
|
+
set(id: string, value: unknown): void;
|
|
38
|
+
/**
|
|
39
|
+
* Appends to the list the id is bound to, in whichever scope binds it (this
|
|
40
|
+
* one or a parent), as a new list: a snapshot emitted earlier keeps what it saw.
|
|
41
|
+
*
|
|
42
|
+
* @param id - The id of a list bound in this scope or a parent.
|
|
43
|
+
* @param items - What to add, in order.
|
|
44
|
+
* @throws Error when no scope binds the id, or it holds something other than a list.
|
|
45
|
+
*/
|
|
46
|
+
append(id: string, items: readonly unknown[]): void;
|
|
47
|
+
/** Whether the id is bound here or in a parent. */
|
|
48
|
+
has(id: string): boolean;
|
|
49
|
+
/** The nearest binding of the id; `undefined` when unbound. */
|
|
50
|
+
get(id: string): unknown;
|
|
51
|
+
/** Records a navigation in this scope. Missing fields inherit from the nearest page state. */
|
|
52
|
+
setPage(state: Partial<PageState>): void;
|
|
53
|
+
/** Replaces only the current document, keeping URL and number. */
|
|
54
|
+
setDocument(document: ScopeDocument): void;
|
|
55
|
+
/** The nearest page state up the chain, if any scope navigated. */
|
|
56
|
+
get pageState(): PageState | undefined;
|
|
57
|
+
/** The document `extract` reads when no `from` is given. */
|
|
58
|
+
get document(): ScopeDocument | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Every binding visible from here, child shadowing parent, plus `page` as
|
|
61
|
+
* `{ url, number }`. This is what `emit` hands to the mapping.
|
|
62
|
+
*/
|
|
63
|
+
snapshot(): Record<string, unknown>;
|
|
64
|
+
/**
|
|
65
|
+
* Resolves a dotted path the way templates do: the first segment is an id (or
|
|
66
|
+
* `page`), the rest walks into the value.
|
|
67
|
+
*
|
|
68
|
+
* @param path - A dotted path such as `item.href` or `page.url`.
|
|
69
|
+
* @returns The value, or `undefined`.
|
|
70
|
+
*/
|
|
71
|
+
lookup(path: string): unknown;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=extraction-scope.model.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ExtractionScope, RESERVED_ROOTS } from './extraction-scope.model.js';
|
|
2
|
+
export type { PageState, ScopeDocument } from './extraction-scope.model.js';
|
|
3
|
+
export { isLiveElement } from './live-element.model.js';
|
|
4
|
+
export type { LiveElement } from './live-element.model.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A snapshot of one element a `forEach` over `selector` iterates: what it held
|
|
3
|
+
* when the loop started, plus where to find it again. The engine never keeps a
|
|
4
|
+
* handle to the element itself: `selector` and `index` re-resolve it on every
|
|
5
|
+
* use, so a re-render between iterations does not break the loop.
|
|
6
|
+
*/
|
|
7
|
+
export interface LiveElement {
|
|
8
|
+
/** The selector the loop matched, already rendered. */
|
|
9
|
+
selector: string;
|
|
10
|
+
/** Position among the matches, from 0. */
|
|
11
|
+
index: number;
|
|
12
|
+
/** Text content, whitespace collapsed. */
|
|
13
|
+
text: string;
|
|
14
|
+
/** Inner HTML. */
|
|
15
|
+
html: string;
|
|
16
|
+
/** Every attribute, by name. */
|
|
17
|
+
attrs: Record<string, string>;
|
|
18
|
+
/** The `value` of an input, option or select; absent otherwise. */
|
|
19
|
+
value?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Whether a value is a live element snapshot (a `target` template rendered to one).
|
|
23
|
+
*
|
|
24
|
+
* @param value - Anything a template rendered.
|
|
25
|
+
* @returns `true` for an object with a `selector` string and an `index` number.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isLiveElement(value: unknown): value is LiveElement;
|
|
28
|
+
//# sourceMappingURL=live-element.model.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Hook, HookMap } from './hook.contract.js';
|
|
2
|
+
/** The hooks a crawler was created with, resolved by name at run time. */
|
|
3
|
+
export declare class HookRegistry {
|
|
4
|
+
private readonly hooks;
|
|
5
|
+
constructor(hooks?: HookMap);
|
|
6
|
+
register(name: string, hook: Hook): void;
|
|
7
|
+
has(name: string): boolean;
|
|
8
|
+
names(): string[];
|
|
9
|
+
/**
|
|
10
|
+
* @param name - The name a recipe used.
|
|
11
|
+
* @returns The hook.
|
|
12
|
+
* @throws UnknownHookError when nothing was registered under that name.
|
|
13
|
+
*/
|
|
14
|
+
resolve(name: string): Hook;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=hook-registry.store.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Severity of a message a hook logs. Typed here so hooks stay a leaf slice. */
|
|
2
|
+
export type HookLogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
3
|
+
/** What a hook knows about where it runs. */
|
|
4
|
+
export interface HookContext {
|
|
5
|
+
recipeId: string;
|
|
6
|
+
/** The extracted values visible at the call site, child scopes shadowing parents. */
|
|
7
|
+
scope: Record<string, unknown>;
|
|
8
|
+
log: (level: HookLogLevel, message: string, meta?: Record<string, unknown>) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A programmatic handler a recipe references by name, from a `hook` step
|
|
12
|
+
* (`input` is `undefined`) or a `hook` transform (`input` is the value so far).
|
|
13
|
+
*/
|
|
14
|
+
export type Hook = (input: unknown, args: Record<string, unknown>, context: HookContext) => unknown;
|
|
15
|
+
/** Hooks by the name recipes use. */
|
|
16
|
+
export type HookMap = Record<string, Hook>;
|
|
17
|
+
//# sourceMappingURL=hook.contract.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** A recipe named a hook nothing registered. */
|
|
2
|
+
export declare class UnknownHookError extends Error {
|
|
3
|
+
readonly hookName: string;
|
|
4
|
+
readonly name = "UnknownHookError";
|
|
5
|
+
constructor(hookName: string, known: readonly string[]);
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=hook.error.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { BodyKind, HttpMethod } from '../recipe-schema/index.js';
|
|
2
|
+
/** One HTTP request as the api runner sends it, templates already rendered. */
|
|
3
|
+
export interface HttpRequest {
|
|
4
|
+
method?: HttpMethod;
|
|
5
|
+
url: string;
|
|
6
|
+
query?: Record<string, string>;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
body?: unknown;
|
|
9
|
+
/** How to read the body; default: from the response content type. */
|
|
10
|
+
as?: BodyKind;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/** A parsed response body. Structurally the same as a scope document, on purpose. */
|
|
14
|
+
export type HttpBody = {
|
|
15
|
+
kind: 'json';
|
|
16
|
+
data: unknown;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'html';
|
|
19
|
+
html: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'text';
|
|
22
|
+
text: string;
|
|
23
|
+
};
|
|
24
|
+
export interface HttpResponse {
|
|
25
|
+
status: number;
|
|
26
|
+
/** The final URL after redirects. */
|
|
27
|
+
url: string;
|
|
28
|
+
headers: Record<string, string>;
|
|
29
|
+
body: HttpBody;
|
|
30
|
+
}
|
|
31
|
+
/** The part of the client the api runner needs; tests fake it. */
|
|
32
|
+
export interface HttpSender {
|
|
33
|
+
send: (request: HttpRequest) => Promise<HttpResponse>;
|
|
34
|
+
}
|
|
35
|
+
/** A response with a 4xx or 5xx status. */
|
|
36
|
+
export declare class HttpError extends Error {
|
|
37
|
+
readonly status: number;
|
|
38
|
+
readonly url: string;
|
|
39
|
+
readonly body: HttpBody;
|
|
40
|
+
readonly headers: Record<string, string>;
|
|
41
|
+
readonly name = "HttpError";
|
|
42
|
+
constructor(status: number, url: string, body: HttpBody, headers?: Record<string, string>);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=http-response.contract.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { APIRequestContext } from 'playwright';
|
|
2
|
+
import type { HttpRequest, HttpResponse, HttpSender } from './http-response.contract.js';
|
|
3
|
+
/** Playwright's storage state: cookies plus per-origin local storage. */
|
|
4
|
+
export type StorageState = Awaited<ReturnType<APIRequestContext['storageState']>>;
|
|
5
|
+
export interface HttpClientOptions {
|
|
6
|
+
/** Cookies and storage captured by a browser session, or loaded from a file. */
|
|
7
|
+
storageState?: StorageState;
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
userAgent?: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
ignoreHTTPSErrors?: boolean;
|
|
12
|
+
/** Send every request through this proxy. */
|
|
13
|
+
proxy?: {
|
|
14
|
+
server: string;
|
|
15
|
+
username?: string;
|
|
16
|
+
password?: string;
|
|
17
|
+
bypass?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* HTTP through Playwright's request context: cookies, redirects and storage
|
|
22
|
+
* state behave exactly as they do in the browser, so a session captured by a
|
|
23
|
+
* browser bootstrap can be reused without translation. No browser is launched.
|
|
24
|
+
*/
|
|
25
|
+
export declare class HttpClient implements HttpSender {
|
|
26
|
+
private readonly context;
|
|
27
|
+
private readonly timeoutMs;
|
|
28
|
+
static open(options?: HttpClientOptions): Promise<HttpClient>;
|
|
29
|
+
private constructor();
|
|
30
|
+
/**
|
|
31
|
+
* Sends a request and parses the body.
|
|
32
|
+
*
|
|
33
|
+
* @param httpRequest - What to send.
|
|
34
|
+
* @returns The response.
|
|
35
|
+
* @throws HttpError for a 4xx or 5xx status.
|
|
36
|
+
*/
|
|
37
|
+
send(httpRequest: HttpRequest): Promise<HttpResponse>;
|
|
38
|
+
/** The cookies and storage this context holds now. */
|
|
39
|
+
storageState(): Promise<StorageState>;
|
|
40
|
+
dispose(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=http.client.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { HttpClient } from './http.client.js';
|
|
2
|
+
export type { HttpClientOptions, StorageState } from './http.client.js';
|
|
3
|
+
export { HttpError } from './http-response.contract.js';
|
|
4
|
+
export type { HttpRequest, HttpResponse, HttpBody, HttpSender } from './http-response.contract.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { createCrawler } from './crawl-execution/index.js';
|
|
2
|
+
export { AccessBroker, AccessConfigError, ACCESS_PRESETS, loadAccessConfig, accessConfigSchema, accessConfigJsonSchema } from './access/index.js';
|
|
3
|
+
export type { AccessConfig, AccessProfile, AccessPlugin, AccessLease, LeaseRequest, PluginLeaseRequest, ProxySettings, AccessPreset } from './access/index.js';
|
|
4
|
+
export type { Crawler, CrawlOptions, CrawlReport, RecipeReport } from './crawl-execution/index.js';
|
|
5
|
+
export { loadRecipeSet, loadRecipes, readRecipeSource, bindRecipeSet, RecipeSet, RecipeBindingError, validateBinding } from './recipe-loading/index.js';
|
|
6
|
+
export type { RecipeSetSource, RecipeSource, RecipeBytes, RecipeDocument, BindingIssue } from './recipe-loading/index.js';
|
|
7
|
+
export { parseInputRecipe, parseOutputRecipe, RecipeValidationError, inputRecipeJsonSchema, outputRecipeJsonSchema, inputRecipeSchema, outputRecipeSchema } from './recipe-schema/index.js';
|
|
8
|
+
export type { InputRecipe, OutputRecipe, FieldSpec, Step, StepType, MappingRule, TransformRule, ErrorPolicy, PaginateNext, SessionSpec, SessionAccess, RecipeIssue } from './recipe-schema/index.js';
|
|
9
|
+
export type { Hook, HookMap, HookContext } from './hooks/index.js';
|
|
10
|
+
export { UnknownHookError } from './hooks/index.js';
|
|
11
|
+
export type { OutputRecord } from './output-mapping/index.js';
|
|
12
|
+
export { RecordRejectedError, MappingFailedError } from './output-mapping/index.js';
|
|
13
|
+
export type { RecordSink, SinkSummary, MemorySink, DedupeScope } from './record-sink/index.js';
|
|
14
|
+
export { memorySink, jsonLinesSink } from './record-sink/index.js';
|
|
15
|
+
export type { CrawlEvent, CrawlListener } from './crawl-events/index.js';
|
|
16
|
+
export { traceLine } from './crawl-events/index.js';
|
|
17
|
+
export type { BrowserSessionConfig, SessionOptions } from './browser-session/index.js';
|
|
18
|
+
export { BrowserClient, BrowserSession } from './browser-session/index.js';
|
|
19
|
+
export { HttpClient, HttpError } from './http-session/index.js';
|
|
20
|
+
export type { HttpClientOptions, HttpRequest, HttpResponse, HttpBody } from './http-session/index.js';
|
|
21
|
+
export { tryParseJson } from './selection/index.js';
|
|
22
|
+
export { StepFailure } from './step-flow/index.js';
|
|
23
|
+
export { TransformError } from './transformation/index.js';
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FieldSpec } from '../recipe-schema/index.js';
|
|
2
|
+
/** A value that cannot become the field's type. */
|
|
3
|
+
export declare class CoercionError extends Error {
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly name = "CoercionError";
|
|
6
|
+
constructor(path: string, reason: string);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Converts a transformed value into the field's declared type.
|
|
10
|
+
* `null` and `undefined` pass through: missing-value policies decide about them.
|
|
11
|
+
*
|
|
12
|
+
* @param value - The value after its transform chain.
|
|
13
|
+
* @param field - The output field.
|
|
14
|
+
* @param path - Where the field is, for messages.
|
|
15
|
+
* @returns The coerced value.
|
|
16
|
+
* @throws CoercionError when the conversion is impossible.
|
|
17
|
+
*/
|
|
18
|
+
export declare function coerceValue(value: unknown, field: FieldSpec, path: string): unknown;
|
|
19
|
+
//# sourceMappingURL=coerce-field.mapper.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { GeneratedValue } from '../recipe-schema/index.js';
|
|
2
|
+
/** What generated fields are produced from. */
|
|
3
|
+
export interface GenerationSource {
|
|
4
|
+
recipeId: string;
|
|
5
|
+
url: string;
|
|
6
|
+
emittedAt: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The engine-supplied value of a generated field.
|
|
10
|
+
*
|
|
11
|
+
* @param kind - Which value.
|
|
12
|
+
* @param source - Where the record came from.
|
|
13
|
+
* @returns The value.
|
|
14
|
+
*/
|
|
15
|
+
export declare function generatedValue(kind: GeneratedValue, source: GenerationSource): unknown;
|
|
16
|
+
//# sourceMappingURL=generated-field.mapper.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { mapRecord } from './map-record.use-case.js';
|
|
2
|
+
export type { MapRecordRequest } from './map-record.use-case.js';
|
|
3
|
+
export type { OutputRecord } from './output-record.model.js';
|
|
4
|
+
export { recordKey } from './output-record.model.js';
|
|
5
|
+
export { RecordRejectedError, MappingFailedError } from './mapping.error.js';
|
|
6
|
+
export { coerceValue, CoercionError } from './coerce-field.mapper.js';
|
|
7
|
+
export { validateField } from './output-field.validator.js';
|
|
8
|
+
export { resolveMissingPolicy, isMissing } from './missing-value.policy.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { HookRegistry } from '../hooks/index.js';
|
|
2
|
+
import type { InputRecipe, OutputRecipe } from '../recipe-schema/index.js';
|
|
3
|
+
import type { TransformContext } from '../transformation/index.js';
|
|
4
|
+
import type { OutputRecord } from './output-record.model.js';
|
|
5
|
+
/** Everything needed to turn one emitted scope snapshot into a record. */
|
|
6
|
+
export interface MapRecordRequest {
|
|
7
|
+
snapshot: Record<string, unknown>;
|
|
8
|
+
input: InputRecipe;
|
|
9
|
+
output: OutputRecipe;
|
|
10
|
+
hooks: HookRegistry;
|
|
11
|
+
/** The page URL at emit time. */
|
|
12
|
+
url: string;
|
|
13
|
+
emittedAt?: string;
|
|
14
|
+
log?: TransformContext['log'];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Maps an emitted scope snapshot to a validated output record: resolves every
|
|
18
|
+
* mapping rule, runs its transform chain, fills generated fields, then coerces
|
|
19
|
+
* and validates each output field and applies the missing-value policy.
|
|
20
|
+
*
|
|
21
|
+
* @param request - The snapshot and the recipes.
|
|
22
|
+
* @returns The record.
|
|
23
|
+
* @throws RecordRejectedError when a field's policy is `skip-record`.
|
|
24
|
+
* @throws MappingFailedError when a field's policy is `fail`, or a transform throws.
|
|
25
|
+
*/
|
|
26
|
+
export declare function mapRecord(request: MapRecordRequest): Promise<OutputRecord>;
|
|
27
|
+
//# sourceMappingURL=map-record.use-case.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** A record that does not satisfy its output recipe and is dropped (policy `skip-record`). */
|
|
2
|
+
export declare class RecordRejectedError extends Error {
|
|
3
|
+
readonly field: string;
|
|
4
|
+
readonly reason: string;
|
|
5
|
+
readonly name = "RecordRejectedError";
|
|
6
|
+
constructor(field: string, reason: string);
|
|
7
|
+
}
|
|
8
|
+
/** A record that does not satisfy its output recipe under policy `fail`: the recipe stops. */
|
|
9
|
+
export declare class MappingFailedError extends Error {
|
|
10
|
+
readonly field: string;
|
|
11
|
+
readonly reason: string;
|
|
12
|
+
readonly name = "MappingFailedError";
|
|
13
|
+
constructor(field: string, reason: string, options?: {
|
|
14
|
+
cause?: unknown;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=mapping.error.d.ts.map
|