Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Astral is the browser automation library for Deno
Astral
Astral is a high-level puppeteer/playwright-like library that allows for control over a web browser (primarily for automation and testing). It is written from scratch with Deno in mind.
Usage
Take a screenshot of a website.
// Import Astral import { launch } from "jsr:@astral/astral"; // Launch the browser const browser = await launch(); // Open a new page const page = await browser.newPage("https://deno.land"); // Take a screenshot of the page and save that to disk const screenshot = await page.screenshot(); Deno.writeFileSync("screenshot.png", screenshot); // Close the browser await browser.close();
You can use the evaluate function to run code in the context of the browser.
// Import Astral import { launch } from "jsr:@astral/astral"; // Launch the browser const browser = await launch(); // Open a new page const page = await browser.newPage("https://deno.land"); // Run code in the context of the browser const value = await page.evaluate(() => { return document.body.innerHTML; }); console.log(value); // Run code with args const result = await page.evaluate((x, y) => { return `The result of adding ${x}+${y} = ${x + y}`; }, { args: [10, 15], }); console.log(result); // Close the browser await browser.close();
You can navigate to a page and interact with it.
// Import Astral import { launch } from "jsr:@astral/astral"; // Launch browser in headfull mode const browser = await launch({ headless: false }); // Open the webpage const page = await browser.newPage("https://deno.land"); // Click the search button const button = await page.$("button"); await button!.click(); // Type in the search input const input = await page.$("#search-input"); await input!.type("pyro", { delay: 1000 }); // Wait for the search results to come back await page.waitForNetworkIdle({ idleConnections: 0, idleTime: 1000 }); // Click the 'pyro' link const xLink = await page.$("a.justify-between:nth-child(1)"); await Promise.all([ page.waitForNavigation(), xLink!.click(), ]); // Click the link to 'pyro.deno.dev' const dLink = await page.$( ".markdown-body > p:nth-child(8) > a:nth-child(1)", ); await Promise.all([ page.waitForNavigation(), dLink!.click(), ]); // Close browser await browser.close();
TODO: Document the locator API.
Advanced Usage
If you already have a browser process running somewhere else or you're using a service that provides remote browsers for automation (such as browserless.io), it is possible to directly connect to its endpoint rather than spawning a new process.
// Import Astral import { connect } from "jsr:@astral/astral"; // Connect to remote endpoint const browser = await connect({ wsEndpoint: "wss://remote-browser-endpoint.example.com", }); // Do stuff const page = await browser.newPage("http://example.com"); console.log(await page.evaluate(() => document.title)); // Close connection await browser.close();
If you'd like to instead re-use a browser that you already launched, astral
exposes the WebSocket endpoint through browser.wsEndpoint()
.
// Spawn a browser process const browser = await launch(); // Connect to first browser instead const anotherBrowser = await connect({ wsEndpoint: browser.wsEndpoint() });
Page authenticate
// Open a new page const page = await browser.newPage(); // Provide credentials for HTTP authentication. const url = "https://postman-echo.com/basic-auth"; await page.authenticate({ username: "postman", password: "password" }); await page.goto(url, { waitUntil: "networkidle2" });
BYOB - Bring Your Own Browser
Essentially the process is as simple as running a chromium-like binary with the following flags:
chromium --remote-debugging-port=1337 \ --headless=new \ --no-first-run \ --password-store=basic \ --use-mock-keychain \ --hide-scrollbars
Technically, only the first flag is necessary, though I've found that these flags generally get the best result. Once your browser process is running, connecting to it is as simple as
// Import Astral import { connect } from "jsr:@astral/astral"; // Connect to remote endpoint const browser = await connect({ wsEndpoint: "<WS-ENDPOINT>", headless: false, }); console.log(browser.wsEndpoint()); // Do stuff const page = await browser.newPage("http://example.com"); console.log(await page.evaluate(() => document.title)); // Close connection await browser.close();
FAQ
Launch FAQ
"No usable sandbox!" with user namespace cloning enabled
Ubuntu 23.10+ (or possibly other Linux distros in the future) ship an AppArmor profile that applies to Chrome stable binaries installed at /opt/google/chrome/chrome (the default installation path). This policy is stored at /etc/apparmor.d/chrome. This AppArmor policy prevents Chrome for Testing binaries downloaded by Puppeteer from using user namespaces resulting in the No usable sandbox! error when trying to launch the browser. For workarounds, see https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md
The following command removes AppArmor restrictions on user namespaces, allowing Puppeteer to launch Chrome without the "No usable sandbox!" error (see puppeteer#13196):
echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
Add Package
deno add jsr:@astral/astral
Import symbol
import * as astral from "@astral/astral";
---- OR ----
Import directly with a jsr specifier
import * as astral from "jsr:@astral/astral";