| 1 | import type * as cf from "@cloudflare/workers-types"; |
| 2 | import * as Data from "effect/Data"; |
| 3 | import type * as Effect from "effect/Effect"; |
| 4 | import type * as Stream from "effect/Stream"; |
| 5 | import type { RuntimeContext } from "../../RuntimeContext.ts"; |
| 6 | import * as Binding from "./Binding.ts"; |
| 7 | import type { BrowserBinding } from "./BrowserBinding.ts"; |
| 8 | |
| 9 | const TypeId = "Cloudflare.Browser" as const; |
| 10 | type TypeId = typeof TypeId; |
| 11 | |
| 12 | export class BrowserError extends Data.TaggedError("BrowserError")<{ |
| 13 | message: string; |
| 14 | cause: unknown; |
| 15 | }> {} |
| 16 | |
| 17 | /** The `Response` type returned by the Cloudflare Browser Rendering binding. */ |
| 18 | export type BrowserResponse = Awaited<ReturnType<cf.BrowserRun["fetch"]>>; |
| 19 | |
| 20 | /** An Effect produced by a {@link BrowserClient} operation. */ |
| 21 | type BrowserEffect<A> = Effect.Effect<A, BrowserError, RuntimeContext>; |
| 22 | |
| 23 | /** A byte stream produced by a binary {@link BrowserClient} action. */ |
| 24 | type BrowserByteStream = Stream.Stream< |
| 25 | Uint8Array, |
| 26 | BrowserError, |
| 27 | RuntimeContext |
| 28 | >; |
| 29 | |
| 30 | // Quick action option types, re-exported so callers don't reach into the |
| 31 | // `@cloudflare/workers-types` namespace directly. |
| 32 | export type BrowserScreenshotOptions = cf.BrowserRunScreenshotOptions; |
| 33 | export type BrowserPDFOptions = cf.BrowserRunPDFOptions; |
| 34 | export type BrowserContentOptions = cf.BrowserRunContentOptions; |
| 35 | export type BrowserScrapeOptions = cf.BrowserRunScrapeOptions; |
| 36 | export type BrowserLinksOptions = cf.BrowserRunLinksOptions; |
| 37 | export type BrowserSnapshotOptions = cf.BrowserRunSnapshotOptions; |
| 38 | export type BrowserJsonOptions = cf.BrowserRunJsonOptions; |
| 39 | export type BrowserMarkdownOptions = cf.BrowserRunMarkdownOptions; |
| 40 | |
| 41 | // Quick action success payloads. |
| 42 | export type BrowserContentResult = cf.BrowserRunContentSuccessResponse; |
| 43 | export type BrowserScrapeResult = cf.BrowserRunScrapeSuccessResponse; |
| 44 | export type BrowserLinksResult = cf.BrowserRunLinksSuccessResponse; |
| 45 | export type BrowserSnapshotResult = cf.BrowserRunSnapshotSuccessResponse; |
| 46 | export type BrowserJsonResult = cf.BrowserRunJsonSuccessResponse; |
| 47 | export type BrowserMarkdownResult = cf.BrowserRunMarkdownSuccessResponse; |
| 48 | export type BrowserErrorResponse = cf.BrowserRunErrorResponse; |
| 49 | |
| 50 | /** |
| 51 | * Effect-native client for a Cloudflare Browser Rendering binding. |
| 52 | * |
| 53 | * Mirrors the runtime {@link cf.BrowserRun} binding, translating its shapes into |
| 54 | * Effect-native ones: JSON quick actions resolve to their parsed success |
| 55 | * payload, and binary actions (`screenshot`, `pdf`) resolve to a `Stream` of the |
| 56 | * response bytes. Non-success responses fail with {@link BrowserError}. The |
| 57 | * {@link raw} accessor and {@link fetch} are the promise-shaped escape hatches |
| 58 | * for libraries like `@cloudflare/puppeteer`. |
| 59 | */ |
| 60 | export interface BrowserClient { |
| 61 | /** Effect resolving to the raw Cloudflare Browser Rendering runtime binding. */ |
| 62 | raw: Effect.Effect<cf.BrowserRun, never, RuntimeContext>; |
| 63 | /** Send a raw HTTP request to the Browser Run API. */ |
| 64 | fetch( |
| 65 | ...args: Parameters<cf.BrowserRun["fetch"]> |
| 66 | ): BrowserEffect<BrowserResponse>; |
| 67 | /** Run a Browser Run quick action, resolving to the parsed payload. */ |
| 68 | quickAction( |
| 69 | action: "screenshot", |
| 70 | options: BrowserScreenshotOptions, |
| 71 | ): BrowserByteStream; |
| 72 | quickAction(action: "pdf", options: BrowserPDFOptions): BrowserByteStream; |
| 73 | quickAction( |
| 74 | action: "content", |
| 75 | options: BrowserContentOptions, |
| 76 | ): BrowserEffect<BrowserContentResult>; |
| 77 | quickAction( |
| 78 | action: "scrape", |
| 79 | options: BrowserScrapeOptions, |
| 80 | ): BrowserEffect<BrowserScrapeResult>; |
| 81 | quickAction( |
| 82 | action: "links", |
| 83 | options: BrowserLinksOptions, |
| 84 | ): BrowserEffect<BrowserLinksResult>; |
| 85 | quickAction( |
| 86 | action: "snapshot", |
| 87 | options: BrowserSnapshotOptions, |
| 88 | ): BrowserEffect<BrowserSnapshotResult>; |
| 89 | quickAction( |
| 90 | action: "json", |
| 91 | options: BrowserJsonOptions, |
| 92 | ): BrowserEffect<BrowserJsonResult>; |
| 93 | quickAction( |
| 94 | action: "markdown", |
| 95 | options: BrowserMarkdownOptions, |
| 96 | ): BrowserEffect<BrowserMarkdownResult>; |
| 97 | /** Take a screenshot of a web page, streaming the raw image bytes. */ |
| 98 | screenshot(options: BrowserScreenshotOptions): BrowserByteStream; |
| 99 | /** Generate a PDF of a web page, streaming the raw PDF bytes. */ |
| 100 | pdf(options: BrowserPDFOptions): BrowserByteStream; |
| 101 | /** Get the HTML content of a web page. */ |
| 102 | content(options: BrowserContentOptions): BrowserEffect<BrowserContentResult>; |
| 103 | /** Scrape elements from a web page by CSS selector. */ |
| 104 | scrape(options: BrowserScrapeOptions): BrowserEffect<BrowserScrapeResult>; |
| 105 | /** Extract all links from a web page. */ |
| 106 | links(options: BrowserLinksOptions): BrowserEffect<BrowserLinksResult>; |
| 107 | /** Get both the HTML content and a base64-encoded screenshot of a web page. */ |
| 108 | snapshot( |
| 109 | options: BrowserSnapshotOptions, |
| 110 | ): BrowserEffect<BrowserSnapshotResult>; |
| 111 | /** Extract structured JSON data from a web page using AI. */ |
| 112 | json(options: BrowserJsonOptions): BrowserEffect<BrowserJsonResult>; |
| 113 | /** Convert a web page to Markdown. */ |
| 114 | markdown( |
| 115 | options: BrowserMarkdownOptions, |
| 116 | ): BrowserEffect<BrowserMarkdownResult>; |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * A Cloudflare Browser Rendering binding for launching headless browser sessions |
| 121 | * from Workers — a Worker-only binding with no backing cloud resource. |
| 122 | * |
| 123 | * `Browser` is a single value that is at once the `Binding.Service` tag, the |
| 124 | * callable that produces a {@link BrowserBinding}, and the type. Declare it on a |
| 125 | * Worker's `env` (it flows through `InferEnv` → `cf.BrowserRun`) or `yield*` it |
| 126 | * inside an Effect-native Worker to attach the binding and obtain the |
| 127 | * {@link BrowserClient}. |
| 128 | * |
| 129 | * ### Effect-style Worker (recommended) |
| 130 | * **Example:** Bind the runtime client and convert a page to Markdown |
| 131 | * ```typescript |
| 132 | * import * as Effect from "effect/Effect"; |
| 133 | * |
| 134 | * Cloudflare.Worker( |
| 135 | * "BrowserWorker", |
| 136 | * { main: import.meta.url }, |
| 137 | * Effect.gen(function* () { |
| 138 | * const browser = yield* Cloudflare.Browser("BROWSER"); |
| 139 | * |
| 140 | * return { |
| 141 | * fetch: Effect.gen(function* () { |
| 142 | * return yield* browser.markdown({ url: "https://example.com" }); |
| 143 | * }), |
| 144 | * }; |
| 145 | * }).pipe(Effect.provide(Cloudflare.Workers.BrowserBinding)), |
| 146 | * ); |
| 147 | * ``` |
| 148 | * |
| 149 | * ### Worker binding metadata |
| 150 | * **Example:** Declare the binding on `env` |
| 151 | * ```typescript |
| 152 | * export const Worker = Cloudflare.Worker("Worker", { |
| 153 | * main: "./src/worker.ts", |
| 154 | * env: { BROWSER: Cloudflare.Browser() }, |
| 155 | * }); |
| 156 | * |
| 157 | * export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>; |
| 158 | * // { BROWSER: BrowserRun } |
| 159 | * ``` |
| 160 | * |
| 161 | * ### Local development |
| 162 | * **Example:** Proxy to the real Browser Rendering service in dev |
| 163 | * ```typescript |
| 164 | * // Default: a real headless Chrome is launched locally and driven over |
| 165 | * // CDP under `alchemy dev`. Alchemy.remote() opts the binding into the |
| 166 | * // real Browser Rendering service instead — in an Effect-native Worker: |
| 167 | * const browser = yield* Cloudflare.Browser("BROWSER").pipe(Alchemy.remote()); |
| 168 | * |
| 169 | * // or declared on an async Worker's env: |
| 170 | * env: { BROWSER: Cloudflare.Browser("BROWSER").pipe(Alchemy.remote()) } |
| 171 | * ``` |
| 172 | * |
| 173 | * @see https://developers.cloudflare.com/browser-rendering/workers-binding-api/ |
| 174 | * |
| 175 | * @binding |
| 176 | * @product Browser Rendering |
| 177 | * @category Developer Platform |
| 178 | */ |
| 179 | export interface Browser extends Binding.Service< |
| 180 | Browser, |
| 181 | TypeId, |
| 182 | BrowserClient |
| 183 | > { |
| 184 | /** |
| 185 | * @param name Binding name (logical id) — the `env` key it resolves to. |
| 186 | * @default "BROWSER" |
| 187 | */ |
| 188 | (name?: string): BrowserBinding; |
| 189 | } |
| 190 | |
| 191 | export const Browser = Binding.Service<Browser>({ |
| 192 | id: TypeId, |
| 193 | defaultName: "BROWSER", |
| 194 | toWorkerBinding: (binding) => ({ |
| 195 | type: "browser", |
| 196 | name: binding.name, |
| 197 | }), |
| 198 | }); |
| 199 | |
| 200 | export const isBrowser = (value: unknown): value is BrowserBinding => |
| 201 | Binding.isBinding(value) && value.kind === TypeId; |