feat: playwright extension (#1764)

* feat: playwright build extension

* feat: add playwright task

* full dep list & added some comments

* fix v3-catalog

* log image size for local builds

* detect playwright version

* add changesets

* add docs

* additional checks

* fix unit test workflow for forks

* use middleware and update docs

* update jsdoc

---------

Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
This commit is contained in:
Mendy Landa
2025-05-13 14:22:01 +03:00
committed by GitHub
parent da0ef73724
commit 21c0dc619e
15 changed files with 728 additions and 22 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Log images sizes for self-hosted deploys
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/build": patch
---
Add playwright extension
+6
View File
@@ -7,6 +7,8 @@ jobs:
unitTests:
name: "🧪 Unit Tests"
runs-on: ubuntu-latest
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
steps:
- name: 🔧 Disable IPv6
run: |
@@ -52,10 +54,14 @@ jobs:
# ..to avoid rate limits when pulling images
- name: 🐳 Login to DockerHub
if: ${{ env.DOCKERHUB_USERNAME }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 🐳 Skipping DockerHub login (no secrets available)
if: ${{ !env.DOCKERHUB_USERNAME }}
run: echo "DockerHub login skipped because secrets are not available."
- name: 📥 Download deps
run: pnpm install --frozen-lockfile
+1
View File
@@ -50,6 +50,7 @@ Trigger.dev provides a set of built-in extensions that you can use to customize
| :-------------------------------------------------------------------- | :----------------------------------------------------------------------------- |
| [prismaExtension](/config/extensions/prismaExtension) | Using prisma in your Trigger.dev tasks |
| [pythonExtension](/config/extensions/pythonExtension) | Execute Python scripts in your project |
| [playwright](/config/extensions/playwright) | Use Playwright in your Trigger.dev tasks |
| [puppeteer](/config/extensions/puppeteer) | Use Puppeteer in your Trigger.dev tasks |
| [ffmpeg](/config/extensions/ffmpeg) | Use FFmpeg in your Trigger.dev tasks |
| [aptGet](/config/extensions/aptGet) | Install system packages in your build image |
+169
View File
@@ -0,0 +1,169 @@
---
title: "Playwright"
sidebarTitle: "playwright"
description: "Use the playwright build extension to use Playwright with Trigger.dev"
---
If you are using [Playwright](https://playwright.dev/), you should use the Playwright build extension.
- Automatically installs Playwright and required browser dependencies
- Allows you to specify which browsers to install (chromium, firefox, webkit)
- Supports headless or non-headless mode
- Lets you specify the Playwright version, or auto-detects it
- Installs only the dependencies needed for the selected browsers to optimize build time and image size
<Note>
This extension only affects the build and deploy process, not the `dev` command.
</Note>
You can use it for a simple Playwright setup like this:
```ts
import { defineConfig } from "@trigger.dev/sdk/v3";
import { playwright } from "@trigger.dev/build/extensions/playwright";
export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
extensions: [
playwright(),
],
},
});
```
### Options
- `browsers`: Array of browsers to install. Valid values: `"chromium"`, `"firefox"`, `"webkit"`. Default: `["chromium"]`.
- `headless`: Run browsers in headless mode. Default: `true`. If set to `false`, a virtual display (Xvfb) will be set up automatically.
- `version`: Playwright version to install. If not provided, the version will be auto-detected from your dependencies (recommended).
<Warning>
Using a different version in your app than specified here will break things. We recommend not setting this option to automatically detect the version.
</Warning>
### Custom browsers and version
```ts
import { defineConfig } from "@trigger.dev/sdk/v3";
import { playwright } from "@trigger.dev/build/extensions/playwright";
export default defineConfig({
project: "<project ref>",
build: {
extensions: [
playwright({
browsers: ["chromium", "webkit"], // optional, will use ["chromium"] if not provided
version: "1.43.1", // optional, will automatically detect the version if not provided
}),
],
},
});
```
### Headless mode
By default, browsers are run in headless mode. If you need to run browsers with a UI (for example, for debugging), set `headless: false`. This will automatically set up a virtual display using Xvfb.
```ts
import { defineConfig } from "@trigger.dev/sdk/v3";
import { playwright } from "@trigger.dev/build/extensions/playwright";
export default defineConfig({
project: "<project ref>",
build: {
extensions: [
playwright({
headless: false,
}),
],
},
});
```
### Environment variables
The extension sets the following environment variables during the build:
- `PLAYWRIGHT_BROWSERS_PATH`: Set to `/ms-playwright` so Playwright finds the installed browsers
- `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD`: Set to `1` to skip browser download at runtime
- `PLAYWRIGHT_SKIP_BROWSER_VALIDATION`: Set to `1` to skip browser validation at runtime
- `DISPLAY`: Set to `:99` if `headless: false` (for Xvfb)
## Managing browser instances
To prevent issues with waits and resumes, you can use middleware and locals to manage the browser instance. This will ensure the browser is available for the whole run, and is properly cleaned up on waits, resumes, and after the run completes.
Here's an example using `chromium`, but you can adapt it for other browsers:
```ts
import { logger, tasks, locals } from "@trigger.dev/sdk";
import { chromium, type Browser } from "playwright";
// Create a locals key for the browser instance
const PlaywrightBrowserLocal = locals.create<{ browser: Browser }>("playwright-browser");
export function getBrowser() {
return locals.getOrThrow(PlaywrightBrowserLocal).browser;
}
export function setBrowser(browser: Browser) {
locals.set(PlaywrightBrowserLocal, { browser });
}
tasks.middleware("playwright-browser", async ({ next }) => {
// Launch the browser before the task runs
const browser = await chromium.launch();
setBrowser(browser);
logger.log("[chromium]: Browser launched (middleware)");
try {
await next();
} finally {
// Always close the browser after the task completes
await browser.close();
logger.log("[chromium]: Browser closed (middleware)");
}
});
tasks.onWait("playwright-browser", async () => {
// Close the browser when the run is waiting
const browser = getBrowser();
await browser.close();
logger.log("[chromium]: Browser closed (onWait)");
});
tasks.onResume("playwright-browser", async () => {
// Relaunch the browser when the run resumes
// Note: You will have to have to manually get a new browser instance in the run function
const browser = await chromium.launch();
setBrowser(browser);
logger.log("[chromium]: Browser launched (onResume)");
});
```
You can then use `getBrowser()` in your task's `run` function to access the browser instance:
```ts
export const playwrightTestTask = task({
id: "playwright-test",
run: async () => {
const browser = getBrowser();
const page = await browser.newPage();
await page.goto("https://google.com");
await page.screenshot({ path: "screenshot.png" });
await page.close();
// Waits will gracefully close the browser
await wait.for({ seconds: 10 });
// On resume, we will re-launch the browser but you will have to manually get the new instance
const newBrowser = getBrowser();
const newPage = await newBrowser.newPage();
await newPage.goto("https://playwright.dev");
await newPage.screenshot({ path: "screenshot2.png" });
await newPage.close();
},
});
```
+1
View File
@@ -73,6 +73,7 @@
"pages": [
"config/extensions/prismaExtension",
"config/extensions/pythonExtension",
"config/extensions/playwright",
"config/extensions/puppeteer",
"config/extensions/ffmpeg",
"config/extensions/aptGet",
+16 -1
View File
@@ -29,7 +29,8 @@
"./extensions/prisma": "./src/extensions/prisma.ts",
"./extensions/audioWaveform": "./src/extensions/audioWaveform.ts",
"./extensions/typescript": "./src/extensions/typescript.ts",
"./extensions/puppeteer": "./src/extensions/puppeteer.ts"
"./extensions/puppeteer": "./src/extensions/puppeteer.ts",
"./extensions/playwright": "./src/extensions/playwright.ts"
},
"sourceDialects": [
"@triggerdotdev/source"
@@ -57,6 +58,9 @@
],
"extensions/puppeteer": [
"dist/commonjs/extensions/puppeteer.d.ts"
],
"extensions/playwright": [
"dist/commonjs/extensions/playwright.d.ts"
]
}
},
@@ -173,6 +177,17 @@
"types": "./dist/commonjs/extensions/puppeteer.d.ts",
"default": "./dist/commonjs/extensions/puppeteer.js"
}
},
"./extensions/playwright": {
"import": {
"@triggerdotdev/source": "./src/extensions/playwright.ts",
"types": "./dist/esm/extensions/playwright.d.ts",
"default": "./dist/esm/extensions/playwright.js"
},
"require": {
"types": "./dist/commonjs/extensions/playwright.d.ts",
"default": "./dist/commonjs/extensions/playwright.js"
}
}
},
"main": "./dist/commonjs/index.js",
+353
View File
@@ -0,0 +1,353 @@
import type { BuildContext, BuildExtension } from "@trigger.dev/core/v3/build";
import type { BuildManifest, BuildTarget } from "@trigger.dev/core/v3";
type PlaywrightBrowser = "chromium" | "firefox" | "webkit";
interface PlaywrightExtensionOptions {
/**
* Browsers to install. Select only needed browsers to optimize build time and size.
* @default ["chromium"]
*/
browsers?: PlaywrightBrowser[];
/**
* Run the browsers in headless mode (Recommended)
* @default true
*/
headless?: boolean;
/**
* Playwright version override. If not provided, we will try to detect the version automatically.
*/
version?: string;
}
/**
* This list is from the official playwright registry.
*
* @see https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/registry/nativeDeps.ts
*/
const debian12Deps = {
tools: [
"xvfb",
"fonts-noto-color-emoji",
"fonts-unifont",
"libfontconfig1",
"libfreetype6",
"xfonts-scalable",
"fonts-liberation",
"fonts-ipafont-gothic",
"fonts-wqy-zenhei",
"fonts-tlwg-loma-otf",
"fonts-freefont-ttf",
],
chromium: [
"libasound2",
"libatk-bridge2.0-0",
"libatk1.0-0",
"libatspi2.0-0",
"libcairo2",
"libcups2",
"libdbus-1-3",
"libdrm2",
"libgbm1",
"libglib2.0-0",
"libnspr4",
"libnss3",
"libpango-1.0-0",
"libx11-6",
"libxcb1",
"libxcomposite1",
"libxdamage1",
"libxext6",
"libxfixes3",
"libxkbcommon0",
"libxrandr2",
],
firefox: [
"libasound2",
"libatk1.0-0",
"libcairo-gobject2",
"libcairo2",
"libdbus-1-3",
"libdbus-glib-1-2",
"libfontconfig1",
"libfreetype6",
"libgdk-pixbuf-2.0-0",
"libglib2.0-0",
"libgtk-3-0",
"libharfbuzz0b",
"libpango-1.0-0",
"libpangocairo-1.0-0",
"libx11-6",
"libx11-xcb1",
"libxcb-shm0",
"libxcb1",
"libxcomposite1",
"libxcursor1",
"libxdamage1",
"libxext6",
"libxfixes3",
"libxi6",
"libxrandr2",
"libxrender1",
"libxtst6",
],
webkit: [
"libsoup-3.0-0",
"gstreamer1.0-libav",
"gstreamer1.0-plugins-bad",
"gstreamer1.0-plugins-base",
"gstreamer1.0-plugins-good",
"libatk-bridge2.0-0",
"libatk1.0-0",
"libcairo2",
"libdbus-1-3",
"libdrm2",
"libegl1",
"libenchant-2-2",
"libepoxy0",
"libevdev2",
"libfontconfig1",
"libfreetype6",
"libgbm1",
"libgdk-pixbuf-2.0-0",
"libgles2",
"libglib2.0-0",
"libglx0",
"libgstreamer-gl1.0-0",
"libgstreamer-plugins-base1.0-0",
"libgstreamer1.0-0",
"libgtk-4-1",
"libgudev-1.0-0",
"libharfbuzz-icu0",
"libharfbuzz0b",
"libhyphen0",
"libicu72",
"libjpeg62-turbo",
"liblcms2-2",
"libmanette-0.2-0",
"libnotify4",
"libopengl0",
"libopenjp2-7",
"libopus0",
"libpango-1.0-0",
"libpng16-16",
"libproxy1v5",
"libsecret-1-0",
"libwayland-client0",
"libwayland-egl1",
"libwayland-server0",
"libwebp7",
"libwebpdemux2",
"libwoff1",
"libx11-6",
"libxcomposite1",
"libxdamage1",
"libxkbcommon0",
"libxml2",
"libxslt1.1",
"libatomic1",
"libevent-2.1-7",
"libavif15",
],
lib2package: {
"libavif.so.15": "libavif15",
"libsoup-3.0.so.0": "libsoup-3.0-0",
"libasound.so.2": "libasound2",
"libatk-1.0.so.0": "libatk1.0-0",
"libatk-bridge-2.0.so.0": "libatk-bridge2.0-0",
"libatspi.so.0": "libatspi2.0-0",
"libcairo.so.2": "libcairo2",
"libcups.so.2": "libcups2",
"libdbus-1.so.3": "libdbus-1-3",
"libdrm.so.2": "libdrm2",
"libgbm.so.1": "libgbm1",
"libgio-2.0.so.0": "libglib2.0-0",
"libglib-2.0.so.0": "libglib2.0-0",
"libgobject-2.0.so.0": "libglib2.0-0",
"libnspr4.so": "libnspr4",
"libnss3.so": "libnss3",
"libnssutil3.so": "libnss3",
"libpango-1.0.so.0": "libpango-1.0-0",
"libsmime3.so": "libnss3",
"libX11.so.6": "libx11-6",
"libxcb.so.1": "libxcb1",
"libXcomposite.so.1": "libxcomposite1",
"libXdamage.so.1": "libxdamage1",
"libXext.so.6": "libxext6",
"libXfixes.so.3": "libxfixes3",
"libxkbcommon.so.0": "libxkbcommon0",
"libXrandr.so.2": "libxrandr2",
"libgtk-4.so.1": "libgtk-4-1",
},
};
/**
* Installs Playwright browsers and dependencies for your Trigger.dev deployments.
*
* @param options - Configuration options for the Playwright extension.
* @param options.browsers Browsers to install. Accepts an array of strings. Default: `["chromium"]`
* @param options.headless Whether to run browsers in headless mode. Default: `true`
* @param options.version Playwright version to use for browser installation. When not set, will automatically detect the version (recommended). Default: `undefined`
*
*/
export function playwright(options: PlaywrightExtensionOptions = {}) {
return new PlaywrightExtension(options);
}
/**
* Background:
*
* Running `npx playwright install --with-deps` normally will install the browsers and the dependencies.
* However, this is not possible in a build context, because we don't have sudo access.
*
* So we need to install the dependencies manually and then download and install the browsers.
* This has a few challenges:
* 1. We don't want to download all browsers, only the ones we need with it's dependencies
* The less dependencies we have to install, the faster the build, and the smaller the image.
* 2. We need to know where to download the browsers from
* while we can hardcode the download url it might change over time (as it has in the past)
* so we need to download the browser info first and then parse the output to get the download url.
*
* Note: While this looks like we are downloading & installing a lot of stuff, it's actually not that bad
* since running `npx playwright install --with-deps` will result in the same amount of downloads.
*/
class PlaywrightExtension implements BuildExtension {
public readonly name = "PlaywrightExtension";
private moduleExternals: string[];
private readonly options: Required<Omit<PlaywrightExtensionOptions, "version">> & {
version?: string;
};
constructor({
browsers = ["chromium"],
headless = true,
version,
}: PlaywrightExtensionOptions = {}) {
if (browsers && browsers.length === 0) {
throw new Error("At least one browser must be specified");
}
this.options = { browsers, headless, version };
this.moduleExternals = ["playwright"];
}
externalsForTarget(target: BuildTarget) {
if (target === "dev") {
return [];
}
return this.moduleExternals;
}
onBuildComplete(context: BuildContext, manifest: BuildManifest) {
if (context.target === "dev") return;
// Detect Playwright version from manifest.externals or use override
const playwrightExternal = manifest.externals?.find(
(external: any) => external.name === "playwright" || external.name === "@playwright/test"
);
const version = playwrightExternal?.version ?? this.options.version;
if (!version) {
throw new Error(
"PlaywrightExtension could not determine the version of playwright. Please provide a version in the PlaywrightExtension options."
);
}
context.logger.debug(
`Adding ${this.name} to the build with browsers: ${this.options.browsers.join(
", "
)}, version: ${version}`
);
const instructions: string[] = [
// Base dependencies, we need these to download the browsers
`RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
unzip \
jq \
grep \
sed \
npm \
&& apt-get clean && rm -rf /var/lib/apt/lists/*`,
// Install Playwright globally with detected version
`RUN npm install -g playwright@${version}`,
];
const deps = [...debian12Deps.tools, ...Object.values(debian12Deps.lib2package)];
if (this.options.browsers.includes("chromium")) deps.push(...debian12Deps.chromium);
if (this.options.browsers.includes("firefox")) deps.push(...debian12Deps.firefox);
if (this.options.browsers.includes("webkit")) deps.push(...debian12Deps.webkit);
instructions.push(
`RUN apt-get update && apt-get install -y --no-install-recommends ${deps.join(" ")} \
&& apt-get clean && rm -rf /var/lib/apt/lists/*`
);
// Setup directory for playwright browsers
instructions.push(`RUN mkdir -p /ms-playwright`);
/**
* `npx playwright install --dry-run` prints the download urls for the browsers.
* We save this output to a file and then parse it to get the download urls for the browsers.
*/
instructions.push(`RUN npx playwright install --dry-run > /tmp/browser-info.txt`);
this.options.browsers.forEach((browser) => {
const browserType = browser === "chromium" ? "chromium-headless-shell" : browser;
instructions.push(
`RUN grep -A5 "browser: ${browserType}" /tmp/browser-info.txt > /tmp/${browser}-info.txt`,
`RUN INSTALL_DIR=$(grep "Install location:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs) && \
DIR_NAME=$(basename "$INSTALL_DIR") && \
if [ -z "$DIR_NAME" ]; then echo "Failed to extract installation directory for ${browser}"; exit 1; fi && \
MS_DIR="/ms-playwright/$DIR_NAME" && \
mkdir -p "$MS_DIR"`,
`RUN DOWNLOAD_URL=$(grep "Download url:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs | sed "s/mac-arm64/linux/g" | sed "s/mac-15-arm64/ubuntu-20.04/g") && \
if [ -z "$DOWNLOAD_URL" ]; then echo "Failed to extract download URL for ${browser}"; exit 1; fi && \
echo "Downloading ${browser} from $DOWNLOAD_URL" && \
curl -L -o /tmp/${browser}.zip "$DOWNLOAD_URL" && \
if [ $? -ne 0 ]; then echo "Failed to download ${browser}"; exit 1; fi && \
unzip -q /tmp/${browser}.zip -d "/ms-playwright/$(basename $(grep "Install location:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs))" && \
if [ $? -ne 0 ]; then echo "Failed to extract ${browser}"; exit 1; fi && \
chmod -R +x "/ms-playwright/$(basename $(grep "Install location:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs))" && \
rm /tmp/${browser}.zip`
);
});
// Environment variables
const envVars: Record<string, string> = {
PLAYWRIGHT_BROWSERS_PATH: "/ms-playwright", // where playwright will find the browsers
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1", // we already downloaded the browsers
PLAYWRIGHT_SKIP_BROWSER_VALIDATION: "1", // we already downloaded the browsers
};
if (!this.options.headless) {
instructions.push(
`RUN echo '#!/bin/sh' > /usr/local/bin/xvfb-exec`,
`RUN echo 'Xvfb :99 -screen 0 1024x768x24 &' >> /usr/local/bin/xvfb-exec`,
`RUN echo 'exec "$@"' >> /usr/local/bin/xvfb-exec`,
`RUN chmod +x /usr/local/bin/xvfb-exec`
);
envVars.DISPLAY = ":99"; // Virtual display for the browsers
}
context.addLayer({
id: "playwright",
image: {
instructions,
},
deploy: {
env: envVars,
override: true,
},
dependencies: {
playwright: version,
},
});
}
}
+23
View File
@@ -155,6 +155,7 @@ export interface DepotBuildImageOptions {
type BuildImageSuccess = {
ok: true;
image: string;
imageSizeBytes: number;
logs: string;
digest?: string;
};
@@ -264,6 +265,7 @@ async function depotBuildImage(options: DepotBuildImageOptions): Promise<BuildIm
return {
ok: true as const,
image: `registry.depot.dev/${options.buildProjectId}:${options.buildId}`,
imageSizeBytes: 0,
logs,
digest,
};
@@ -366,6 +368,26 @@ async function selfHostedBuildImage(
digest = extractImageDigest(errors);
// Get the image size
const sizeProcess = x("docker", ["image", "inspect", imageRef, "--format={{.Size}}"], {
nodeOptions: { cwd: options.cwd },
});
let imageSizeBytes = 0;
for await (const line of sizeProcess) {
if (line.trim() === "") {
continue;
}
imageSizeBytes = parseInt(line, 10);
break;
}
if (imageSizeBytes) {
// Convert to MB and log
options.onLog?.(`Image size: ${(imageSizeBytes / (1024 * 1024)).toFixed(2)} MB`);
}
if (options.selfHostedRegistry || options.pushImage) {
const pushArgs = ["push", imageRef].filter(Boolean) as string[];
@@ -393,6 +415,7 @@ async function selfHostedBuildImage(
return {
ok: true as const,
image: options.imageTag,
imageSizeBytes,
digest,
logs: extractLogs(errors),
};
+19
View File
@@ -2260,6 +2260,9 @@ importers:
pg:
specifier: ^8.11.5
version: 8.11.5
playwright:
specifier: ^1.50.1
version: 1.50.1
puppeteer:
specifier: ^23.4.0
version: 23.4.0(typescript@5.5.4)
@@ -29909,6 +29912,22 @@ packages:
engines: {node: '>=16'}
hasBin: true
/playwright-core@1.50.1:
resolution: {integrity: sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==}
engines: {node: '>=18'}
hasBin: true
dev: false
/playwright@1.50.1:
resolution: {integrity: sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==}
engines: {node: '>=18'}
hasBin: true
dependencies:
playwright-core: 1.50.1
optionalDependencies:
fsevents: 2.3.2
dev: false
/polite-json@5.0.0:
resolution: {integrity: sha512-OLS/0XeUAcE8a2fdwemNja+udKgXNnY6yKVIXqAD2zVRx1KvY6Ato/rZ2vdzbxqYwPW0u6SCNC/bAMPNzpzxbw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+1
View File
@@ -41,6 +41,7 @@
"msw": "^2.2.1",
"openai": "^4.47.0",
"pg": "^8.11.5",
"playwright": "^1.50.1",
"puppeteer": "^23.4.0",
"react": "19.0.0-rc.0",
"react-email": "^3.0.1",
-9
View File
@@ -1,9 +0,0 @@
import { logger, type HandleErrorFunction } from "@trigger.dev/sdk/v3";
export const handleError: HandleErrorFunction = async (
payload,
error,
{ ctx, retry, retryAt, retryDelayInMs }
) => {
logger.log("handling error", { error, retry, retryAt, retryDelayInMs });
};
+17
View File
@@ -0,0 +1,17 @@
import { tasks } from "@trigger.dev/sdk";
tasks.onStart(({ payload, ctx }) => {
console.log(`Task ${ctx.task.id} started ${ctx.run.id}`);
});
tasks.onFailure(({ payload, error, ctx }) => {
console.log(
`Task ${ctx.task.id} failed ${ctx.run.id}: ${
error instanceof Error ? error.message : String(error)
}`
);
});
tasks.catchError(({ payload, ctx, task, error, retry, retryAt, retryDelayInMs }) => {
console.log("handling error", { error, retry, retryAt, retryDelayInMs });
});
@@ -0,0 +1,110 @@
import { logger, task, locals, tasks, wait } from "@trigger.dev/sdk";
import { chromium, type Browser } from "playwright";
/**
* Example task demonstrating Playwright browser automation with Trigger.dev
*
* To use other browsers (firefox, webkit):
* 1. Import them from playwright: `import { chromium, firefox, webkit } from "playwright";`
* 2. Launch them in the middleware instead of chromium: `const browser = await firefox.launch();`
* 3. Configure the playwright extension in your project:
* ```
* // In your build configuration
* import { playwright } from "@trigger.dev/core/v3/build";
*
* extensions: [
* // Only add browsers your tasks will use
* playwright({ browsers: ["chromium", "firefox", "webkit"] })
* ]
* ```
*/
export const playwrightTestTask = task({
id: "playwright-test",
retry: {
maxAttempts: 1,
},
run: async () => {
const playwrightVersion = require("playwright/package.json").version;
logger.log("Starting Playwright automation task", { version: playwrightVersion });
// Use the browser from locals
const browser = getBrowser();
const prefix = getPrefixFn(browser);
logger.log(prefix("Browser acquired from locals"));
// The onWait lifecycle hook will automatically close the browser
// This ensures that checkpoint and restore will be successful
await wait.for({ seconds: 10 });
// We have to get a new browser instance because the existing one was closed
const newBrowser = getBrowser();
logger.log(prefix("New browser acquired from locals"));
const page = await newBrowser.newPage();
logger.log(prefix("New page created"));
await page.goto("https://google.com");
logger.log(prefix("Navigated to google.com"));
const screenshot = await page.screenshot({ path: "screenshot.png" });
logger.log(prefix("Screenshot taken"), { size: screenshot.byteLength });
await page.close();
logger.log(prefix("Page closed"));
},
});
const getPrefixFn = (browser: Browser) => {
const browserType = browser.browserType();
const browserName = browserType.name();
return (msg: string) => `[${browserName}]: ${msg}`;
};
// Locals key for Playwright browser
const PlaywrightBrowserLocal = locals.create<{ browser: Browser }>("playwright-browser");
export function getBrowser() {
return locals.getOrThrow(PlaywrightBrowserLocal).browser;
}
export function setBrowser(browser: Browser) {
locals.set(PlaywrightBrowserLocal, { browser });
}
tasks.middleware("playwright-browser", async ({ ctx, payload, task, next }) => {
// Only using chromium for now, can be extended for other browsers
const browser = await chromium.launch();
setBrowser(browser);
const prefix = getPrefixFn(browser);
logger.log(prefix("Browser launched (middleware)"));
try {
await next();
} finally {
await browser.close();
logger.log(prefix("Browser closed (middleware)"));
}
});
tasks.onWait("playwright-browser", async ({ ctx, payload, task }) => {
const browser = getBrowser();
const prefix = getPrefixFn(browser);
await browser.close();
logger.log(prefix("Browser closed (onWait)"));
});
tasks.onResume("playwright-browser", async ({ ctx, payload, task }) => {
// Only using chromium for now, can be extended for other browsers
// Make sure this is the same browser as the one used in the middleware
const browser = await chromium.launch();
setBrowser(browser);
const prefix = getPrefixFn(browser);
logger.log(prefix("Browser launched (onResume)"));
});
+2 -12
View File
@@ -5,12 +5,11 @@ import { esbuildPlugin } from "@trigger.dev/build";
import { audioWaveform } from "@trigger.dev/build/extensions/audioWaveform";
import { ffmpeg, syncEnvVars } from "@trigger.dev/build/extensions/core";
import { puppeteer } from "@trigger.dev/build/extensions/puppeteer";
import { playwright } from "@trigger.dev/build/extensions/playwright";
import { prismaExtension } from "@trigger.dev/build/extensions/prisma";
import { emitDecoratorMetadata } from "@trigger.dev/build/extensions/typescript";
import { defineConfig } from "@trigger.dev/sdk/v3";
export { handleError } from "./src/handleError.js";
export default defineConfig({
runtime: "node",
project: "yubjwjsfkxnylobaqvqz",
@@ -31,16 +30,6 @@ export default defineConfig({
},
enableConsoleLogging: false,
logLevel: "info",
onStart: async (payload, { ctx }) => {
console.log(`Task ${ctx.task.id} started ${ctx.run.id}`);
},
onFailure: async (payload, error, { ctx }) => {
console.log(
`Task ${ctx.task.id} failed ${ctx.run.id}: ${
error instanceof Error ? error.message : String(error)
}`
);
},
build: {
conditions: ["react-server"],
extensions: [
@@ -87,6 +76,7 @@ export default defineConfig({
}));
}),
puppeteer(),
playwright(),
],
external: ["re2"],
},