From 63b3e1081d0e7627a511aac31eb70f5e9861f303 Mon Sep 17 00:00:00 2001 From: Mendy Landa Date: Thu, 6 Mar 2025 12:05:59 +0200 Subject: [PATCH 01/12] feat: playwright build extension --- packages/build/package.json | 17 +- packages/build/src/extensions/playwright.ts | 190 ++++++++++++++++++++ 2 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 packages/build/src/extensions/playwright.ts diff --git a/packages/build/package.json b/packages/build/package.json index 45e69db347..af2683084a 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -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", diff --git a/packages/build/src/extensions/playwright.ts b/packages/build/src/extensions/playwright.ts new file mode 100644 index 0000000000..bad52a1e39 --- /dev/null +++ b/packages/build/src/extensions/playwright.ts @@ -0,0 +1,190 @@ +import type { BuildContext, BuildExtension } from "@trigger.dev/core/v3/build"; + +type PlaywrightBrowser = "chromium" | "firefox" | "webkit"; + +interface PlaywrightExtensionOptions { + /** + * Browsers to install. Select only needed browsers to optimize build time and size. + * @default ["chromium"] + */ + browsers?: PlaywrightBrowser[]; + + /** + * Whether to support non-headless mode. + * @default true + */ + headless?: boolean; +} + +/** + * Creates a Playwright extension for trigger.dev + * @param options Configuration options + */ +export function playwright(options: PlaywrightExtensionOptions = {}) { + return new PlaywrightExtension(options); +} + +class PlaywrightExtension implements BuildExtension { + public readonly name = "PlaywrightExtension"; + private readonly options: Required; + + constructor({ browsers = ["chromium"], headless = true }: PlaywrightExtensionOptions = {}) { + if (browsers && browsers.length === 0) { + throw new Error("At least one browser must be specified"); + } + this.options = { browsers, headless }; + } + + onBuildComplete(context: BuildContext) { + if (context.target === "dev") return; + + context.logger.debug( + `Adding ${this.name} to the build with browsers: ${this.options.browsers.join(", ")}` + ); + + const instructions: string[] = [ + // Base dependencies + `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 + `RUN npm install -g playwright`, + ]; + + // Browser-specific dependencies + const chromiumDeps = [ + "libnspr4", + "libatk1.0-0", + "libatk-bridge2.0-0", + "libatspi2.0-0", + "libasound2", + "libnss3", + "libxcomposite1", + "libxdamage1", + "libxfixes3", + "libxrandr2", + "libgbm1", + "libxkbcommon0", + ]; + + const firefoxDeps = [ + "libgtk-3.0", + "libgtk-4-1", + "libgtk-4-common", + "libgtk-4-dev", + "libgtk-4-doc", + "libasound2", + ]; + + const webkitDeps = [ + "libenchant-2-2", + "libgl1", + "libgles2", + "libgstreamer-gl1.0-0", + "libgstreamer-plugins-base1.0-0", + "libgstreamer-plugins-bad1.0-0", + "libharfbuzz-icu0", + "libhyphen0", + "libicu72", + "libjpeg-dev", + "libopenjp2-7", + "libopus0", + "libpng-dev", + "libsecret-1-0", + "libvpx7", + "libwebp7", + "libwoff1", + "libx11-6", + "libxcomposite1", + "libxdamage1", + "libxrender1", + "libxt6", + "libgtk-4-1", + "libgraphene-1.0-0", + "libxslt1.1", + "libevent-2.1-7", + "libmanette-0.2-0", + "libwebpdemux2", + "libwebpmux3", + "libatomic1", + "libavif15", + "libx264-dev", + "flite", + "libatk1.0-0", + "libatk-bridge2.0-0", + ]; + + const deps = []; + if (this.options.browsers.includes("chromium")) deps.push(...chromiumDeps); + if (this.options.browsers.includes("firefox")) deps.push(...firefoxDeps); + if (this.options.browsers.includes("webkit")) deps.push(...webkitDeps); + + const uniqueDeps = [...new Set(deps)]; + + if (uniqueDeps.length > 0) { + instructions.push( + `RUN apt-get update && apt-get install -y --no-install-recommends ${uniqueDeps.join(" ")} \ + && apt-get clean && rm -rf /var/lib/apt/lists/*` + ); + } + + // Setup Playwright browsers + instructions.push(`RUN mkdir -p /ms-playwright`); + 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") && \ + 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") && \ + echo "Downloading ${browser} from $DOWNLOAD_URL" && \ + curl -L -o /tmp/${browser}.zip "$DOWNLOAD_URL" && \ + unzip -q /tmp/${browser}.zip -d "/ms-playwright/$(basename $(grep "Install location:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs))" && \ + 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 = { + PLAYWRIGHT_BROWSERS_PATH: "/ms-playwright", + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1", + PLAYWRIGHT_SKIP_BROWSER_VALIDATION: "1", + }; + + 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"; + } + + context.addLayer({ + id: "playwright", + image: { + instructions, + }, + deploy: { + env: envVars, + override: true, + }, + }); + } +} From a367708c930aa75c80daa8f8cf7b3bcc3a0a11d9 Mon Sep 17 00:00:00 2001 From: Mendy Landa Date: Thu, 6 Mar 2025 12:06:10 +0200 Subject: [PATCH 02/12] feat: add playwright task --- pnpm-lock.yaml | 19 ++++++++ references/v3-catalog/package.json | 1 + .../v3-catalog/src/trigger/playwrightTask.ts | 47 +++++++++++++++++++ references/v3-catalog/trigger.config.ts | 2 + 4 files changed, 69 insertions(+) create mode 100644 references/v3-catalog/src/trigger/playwrightTask.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3e027be59..66face8fa7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1898,6 +1898,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) @@ -27003,6 +27006,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} diff --git a/references/v3-catalog/package.json b/references/v3-catalog/package.json index 52732b334e..0c59dbb86b 100644 --- a/references/v3-catalog/package.json +++ b/references/v3-catalog/package.json @@ -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", diff --git a/references/v3-catalog/src/trigger/playwrightTask.ts b/references/v3-catalog/src/trigger/playwrightTask.ts new file mode 100644 index 0000000000..2f6dd8192e --- /dev/null +++ b/references/v3-catalog/src/trigger/playwrightTask.ts @@ -0,0 +1,47 @@ +import { logger, task } from "@trigger.dev/sdk/v3"; +import { chromium } 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. Add them to the browserType array: `for (const browserType of [chromium, firefox, webkit])` + * 3. Configure the playwright extension in your project: + * ``` + * // In your build configuration + * import { playwright } from "@trigger.dev/core/v3/build"; + * + * extensions: [ + * playwright({ browsers: ["chromium", "firefox", "webkit"] }) + * ] + * ``` + */ +export const playwrightTestTask = task({ + id: "playwright-test", + retry: { + maxAttempts: 1, + }, + run: async () => { + logger.log("Starting Playwright automation task"); + + for (const browserType of [chromium]) { + const prefix = (msg: string) => `[${browserType.name()}]: ${msg}`; + + const browser = await browserType.launch(); + logger.log(prefix("Browser launched")); + + const page = await browser.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 browser.close(); + logger.log(prefix("Browser closed")); + } + }, +}); diff --git a/references/v3-catalog/trigger.config.ts b/references/v3-catalog/trigger.config.ts index 9a24160445..ff82ad1a72 100644 --- a/references/v3-catalog/trigger.config.ts +++ b/references/v3-catalog/trigger.config.ts @@ -5,6 +5,7 @@ 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"; @@ -87,6 +88,7 @@ export default defineConfig({ })); }), puppeteer(), + playwright(), ], external: ["re2"], }, From b0bf62441bdf1e334ede6c67d12c6ee4ec7c5586 Mon Sep 17 00:00:00 2001 From: Mendy Landa Date: Fri, 9 May 2025 07:53:32 +0300 Subject: [PATCH 03/12] full dep list & added some comments --- packages/build/src/extensions/playwright.ts | 280 ++++++++++++++------ 1 file changed, 197 insertions(+), 83 deletions(-) diff --git a/packages/build/src/extensions/playwright.ts b/packages/build/src/extensions/playwright.ts index bad52a1e39..be89bdf582 100644 --- a/packages/build/src/extensions/playwright.ts +++ b/packages/build/src/extensions/playwright.ts @@ -10,12 +10,173 @@ interface PlaywrightExtensionOptions { browsers?: PlaywrightBrowser[]; /** - * Whether to support non-headless mode. + * Run the browsers in headless mode (Recommended) * @default true */ headless?: boolean; } +/** + * 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", + }, +}; + /** * Creates a Playwright extension for trigger.dev * @param options Configuration options @@ -24,6 +185,23 @@ 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 readonly options: Required; @@ -43,7 +221,7 @@ class PlaywrightExtension implements BuildExtension { ); const instructions: string[] = [ - // Base dependencies + // Base dependencies, we need these to download the browsers `RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ unzip \ @@ -57,90 +235,26 @@ class PlaywrightExtension implements BuildExtension { `RUN npm install -g playwright`, ]; - // Browser-specific dependencies - const chromiumDeps = [ - "libnspr4", - "libatk1.0-0", - "libatk-bridge2.0-0", - "libatspi2.0-0", - "libasound2", - "libnss3", - "libxcomposite1", - "libxdamage1", - "libxfixes3", - "libxrandr2", - "libgbm1", - "libxkbcommon0", - ]; - - const firefoxDeps = [ - "libgtk-3.0", - "libgtk-4-1", - "libgtk-4-common", - "libgtk-4-dev", - "libgtk-4-doc", - "libasound2", - ]; - - const webkitDeps = [ - "libenchant-2-2", - "libgl1", - "libgles2", - "libgstreamer-gl1.0-0", - "libgstreamer-plugins-base1.0-0", - "libgstreamer-plugins-bad1.0-0", - "libharfbuzz-icu0", - "libhyphen0", - "libicu72", - "libjpeg-dev", - "libopenjp2-7", - "libopus0", - "libpng-dev", - "libsecret-1-0", - "libvpx7", - "libwebp7", - "libwoff1", - "libx11-6", - "libxcomposite1", - "libxdamage1", - "libxrender1", - "libxt6", - "libgtk-4-1", - "libgraphene-1.0-0", - "libxslt1.1", - "libevent-2.1-7", - "libmanette-0.2-0", - "libwebpdemux2", - "libwebpmux3", - "libatomic1", - "libavif15", - "libx264-dev", - "flite", - "libatk1.0-0", - "libatk-bridge2.0-0", - ]; + 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); - const deps = []; - if (this.options.browsers.includes("chromium")) deps.push(...chromiumDeps); - if (this.options.browsers.includes("firefox")) deps.push(...firefoxDeps); - if (this.options.browsers.includes("webkit")) deps.push(...webkitDeps); - - const uniqueDeps = [...new Set(deps)]; - - if (uniqueDeps.length > 0) { - instructions.push( - `RUN apt-get update && apt-get install -y --no-install-recommends ${uniqueDeps.join(" ")} \ + 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 Playwright browsers + // Setup directory for playwright browsers instructions.push(`RUN mkdir -p /ms-playwright`); - instructions.push(`RUN npx playwright install --dry-run > /tmp/browser-info.txt`); + /** + * `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`, @@ -160,9 +274,9 @@ class PlaywrightExtension implements BuildExtension { // Environment variables const envVars: Record = { - PLAYWRIGHT_BROWSERS_PATH: "/ms-playwright", - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1", - PLAYWRIGHT_SKIP_BROWSER_VALIDATION: "1", + 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) { @@ -173,7 +287,7 @@ class PlaywrightExtension implements BuildExtension { `RUN chmod +x /usr/local/bin/xvfb-exec` ); - envVars.DISPLAY = ":99"; + envVars.DISPLAY = ":99"; // Virtual display for the browsers } context.addLayer({ From cb93119a228895f6750deff9ddbae2aa17f80a89 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 12 May 2025 15:25:16 +0100 Subject: [PATCH 04/12] fix v3-catalog --- references/v3-catalog/src/handleError.ts | 9 --------- references/v3-catalog/src/trigger/init.ts | 17 +++++++++++++++++ .../v3-catalog/src/trigger/playwrightTask.ts | 5 +++-- references/v3-catalog/trigger.config.ts | 12 ------------ 4 files changed, 20 insertions(+), 23 deletions(-) delete mode 100644 references/v3-catalog/src/handleError.ts create mode 100644 references/v3-catalog/src/trigger/init.ts diff --git a/references/v3-catalog/src/handleError.ts b/references/v3-catalog/src/handleError.ts deleted file mode 100644 index c73b7dbf58..0000000000 --- a/references/v3-catalog/src/handleError.ts +++ /dev/null @@ -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 }); -}; diff --git a/references/v3-catalog/src/trigger/init.ts b/references/v3-catalog/src/trigger/init.ts new file mode 100644 index 0000000000..982261ab23 --- /dev/null +++ b/references/v3-catalog/src/trigger/init.ts @@ -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 }); +}); diff --git a/references/v3-catalog/src/trigger/playwrightTask.ts b/references/v3-catalog/src/trigger/playwrightTask.ts index 2f6dd8192e..ab06d3de53 100644 --- a/references/v3-catalog/src/trigger/playwrightTask.ts +++ b/references/v3-catalog/src/trigger/playwrightTask.ts @@ -1,6 +1,5 @@ import { logger, task } from "@trigger.dev/sdk/v3"; import { chromium } from "playwright"; - /** * Example task demonstrating Playwright browser automation with Trigger.dev * @@ -23,7 +22,9 @@ export const playwrightTestTask = task({ maxAttempts: 1, }, run: async () => { - logger.log("Starting Playwright automation task"); + const playwrightVersion = require("playwright/package.json").version; + + logger.log("Starting Playwright automation task", { version: playwrightVersion }); for (const browserType of [chromium]) { const prefix = (msg: string) => `[${browserType.name()}]: ${msg}`; diff --git a/references/v3-catalog/trigger.config.ts b/references/v3-catalog/trigger.config.ts index ff82ad1a72..c27ce98dda 100644 --- a/references/v3-catalog/trigger.config.ts +++ b/references/v3-catalog/trigger.config.ts @@ -10,8 +10,6 @@ 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", @@ -32,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: [ From 2e46fe741c9a618b51f045dca06d5b83effe51d0 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 12 May 2025 15:27:21 +0100 Subject: [PATCH 05/12] log image size for local builds --- packages/cli-v3/src/deploy/buildImage.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/cli-v3/src/deploy/buildImage.ts b/packages/cli-v3/src/deploy/buildImage.ts index 42ff998af3..73e377796a 100644 --- a/packages/cli-v3/src/deploy/buildImage.ts +++ b/packages/cli-v3/src/deploy/buildImage.ts @@ -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 Date: Mon, 12 May 2025 15:27:43 +0100 Subject: [PATCH 06/12] detect playwright version --- packages/build/src/extensions/playwright.ts | 54 ++++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/packages/build/src/extensions/playwright.ts b/packages/build/src/extensions/playwright.ts index be89bdf582..467a016565 100644 --- a/packages/build/src/extensions/playwright.ts +++ b/packages/build/src/extensions/playwright.ts @@ -1,4 +1,5 @@ import type { BuildContext, BuildExtension } from "@trigger.dev/core/v3/build"; +import type { BuildManifest, BuildTarget } from "@trigger.dev/core/v3"; type PlaywrightBrowser = "chromium" | "firefox" | "webkit"; @@ -14,6 +15,11 @@ interface PlaywrightExtensionOptions { * @default true */ headless?: boolean; + + /** + * Playwright version override. If not provided, we will try to detect the version automatically. + */ + version?: string; } /** @@ -204,20 +210,51 @@ export function playwright(options: PlaywrightExtensionOptions = {}) { */ class PlaywrightExtension implements BuildExtension { public readonly name = "PlaywrightExtension"; - private readonly options: Required; + private moduleExternals: string[]; + + private readonly options: Required> & { + version?: string; + }; - constructor({ browsers = ["chromium"], headless = true }: PlaywrightExtensionOptions = {}) { + 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 }; + this.options = { browsers, headless, version }; + this.moduleExternals = ["playwright"]; + } + + externalsForTarget(target: BuildTarget) { + if (target === "dev") { + return []; + } + + return this.moduleExternals; } - onBuildComplete(context: BuildContext) { + 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(", ")}` + `Adding ${this.name} to the build with browsers: ${this.options.browsers.join( + ", " + )}, version: ${version}` ); const instructions: string[] = [ @@ -231,8 +268,8 @@ class PlaywrightExtension implements BuildExtension { npm \ && apt-get clean && rm -rf /var/lib/apt/lists/*`, - // Install Playwright globally - `RUN npm install -g playwright`, + // Install Playwright globally with detected version + `RUN npm install -g playwright@${version}`, ]; const deps = [...debian12Deps.tools, ...Object.values(debian12Deps.lib2package)]; @@ -299,6 +336,9 @@ class PlaywrightExtension implements BuildExtension { env: envVars, override: true, }, + dependencies: { + playwright: version, + }, }); } } From 0f3e618e86fba98d91b9cc58434d766e56d5b771 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 12 May 2025 18:52:53 +0100 Subject: [PATCH 07/12] add changesets --- .changeset/itchy-frogs-care.md | 5 +++++ .changeset/witty-donkeys-unite.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/itchy-frogs-care.md create mode 100644 .changeset/witty-donkeys-unite.md diff --git a/.changeset/itchy-frogs-care.md b/.changeset/itchy-frogs-care.md new file mode 100644 index 0000000000..72cb57c867 --- /dev/null +++ b/.changeset/itchy-frogs-care.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +Log images sizes for self-hosted deploys diff --git a/.changeset/witty-donkeys-unite.md b/.changeset/witty-donkeys-unite.md new file mode 100644 index 0000000000..f1a17eb7ff --- /dev/null +++ b/.changeset/witty-donkeys-unite.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/build": patch +--- + +Add playwright extension From 20714e1ec99c191dc020184344382f238ddfe8be Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 12 May 2025 22:35:14 +0100 Subject: [PATCH 08/12] add docs --- docs/config/extensions/overview.mdx | 1 + docs/config/extensions/playwright.mdx | 103 ++++++++++++++++++++++++++ docs/docs.json | 1 + 3 files changed, 105 insertions(+) create mode 100644 docs/config/extensions/playwright.mdx diff --git a/docs/config/extensions/overview.mdx b/docs/config/extensions/overview.mdx index 412a11062b..abba56694e 100644 --- a/docs/config/extensions/overview.mdx +++ b/docs/config/extensions/overview.mdx @@ -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 | diff --git a/docs/config/extensions/playwright.mdx b/docs/config/extensions/playwright.mdx new file mode 100644 index 0000000000..ab315880fe --- /dev/null +++ b/docs/config/extensions/playwright.mdx @@ -0,0 +1,103 @@ +--- +title: "Playwright" +sidebarTitle: "playwright" +description: "Use the playwright build extension to use Playwright with Trigger.dev" +--- + +If you are using Playwright, 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 + +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: "", + // Your other config settings... + build: { + extensions: [ + playwright(), + ], + }, +}); +``` + + + This extension only affects the build and deploy process, not the `dev` command. + + +### 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). + + + Using a different version in your app than specified here will break things. + + +### Custom browsers and version + +```ts +import { defineConfig } from "@trigger.dev/sdk/v3"; +import { playwright } from "@trigger.dev/build/extensions/playwright"; + +export default defineConfig({ + project: "", + 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: "", + 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) + +### Notes + +- The extension installs only the dependencies required for the browsers you select, reducing build time and image size. +- Playwright is installed globally in the build image. +- The extension does not affect local development (`dev` command), only the build and deploy process. +- Only specify the Playwright version in the options if automatic detection is not working. + + + For more information about Playwright, see the [official Playwright documentation](https://playwright.dev/). + diff --git a/docs/docs.json b/docs/docs.json index cf7d0a3949..e9352be310 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -73,6 +73,7 @@ "pages": [ "config/extensions/prismaExtension", "config/extensions/pythonExtension", + "config/extensions/playwright", "config/extensions/puppeteer", "config/extensions/ffmpeg", "config/extensions/aptGet", From 18255574583abb2666aca3c15080264b9d7200a3 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 12 May 2025 22:38:51 +0100 Subject: [PATCH 09/12] additional checks --- packages/build/src/extensions/playwright.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/build/src/extensions/playwright.ts b/packages/build/src/extensions/playwright.ts index 467a016565..988305274e 100644 --- a/packages/build/src/extensions/playwright.ts +++ b/packages/build/src/extensions/playwright.ts @@ -297,13 +297,17 @@ class PlaywrightExtension implements BuildExtension { `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` ); From 51f41e7a0a3cc5258767be36dc15ac4de8f34096 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 13 May 2025 00:36:05 +0100 Subject: [PATCH 10/12] fix unit test workflow for forks --- .github/workflows/unit-tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 382ee5617f..79d280ce57 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -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 From 76ac0f03a65c008389580b066c3b22071f5c2844 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 13 May 2025 11:44:19 +0100 Subject: [PATCH 11/12] use middleware and update docs --- docs/config/extensions/playwright.mdx | 94 ++++++++++++++++--- .../v3-catalog/src/trigger/playwrightTask.ts | 94 +++++++++++++++---- 2 files changed, 158 insertions(+), 30 deletions(-) diff --git a/docs/config/extensions/playwright.mdx b/docs/config/extensions/playwright.mdx index ab315880fe..6924f22b56 100644 --- a/docs/config/extensions/playwright.mdx +++ b/docs/config/extensions/playwright.mdx @@ -4,7 +4,7 @@ sidebarTitle: "playwright" description: "Use the playwright build extension to use Playwright with Trigger.dev" --- -If you are using Playwright, you should use the Playwright build extension. +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) @@ -12,6 +12,10 @@ If you are using Playwright, you should use the Playwright build extension. - 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 + + This extension only affects the build and deploy process, not the `dev` command. + + You can use it for a simple Playwright setup like this: ```ts @@ -29,10 +33,6 @@ export default defineConfig({ }); ``` - - This extension only affects the build and deploy process, not the `dev` command. - - ### Options - `browsers`: Array of browsers to install. Valid values: `"chromium"`, `"firefox"`, `"webkit"`. Default: `["chromium"]`. @@ -40,7 +40,7 @@ export default defineConfig({ - `version`: Playwright version to install. If not provided, the version will be auto-detected from your dependencies (recommended). - Using a different version in your app than specified here will break things. + Using a different version in your app than specified here will break things. We recommend not setting this option to automatically detect the version. ### Custom browsers and version @@ -91,13 +91,79 @@ The extension sets the following environment variables during the build: - `PLAYWRIGHT_SKIP_BROWSER_VALIDATION`: Set to `1` to skip browser validation at runtime - `DISPLAY`: Set to `:99` if `headless: false` (for Xvfb) -### Notes +## Managing browser instances -- The extension installs only the dependencies required for the browsers you select, reducing build time and image size. -- Playwright is installed globally in the build image. -- The extension does not affect local development (`dev` command), only the build and deploy process. -- Only specify the Playwright version in the options if automatic detection is not working. +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. - - For more information about Playwright, see the [official Playwright documentation](https://playwright.dev/). - +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(); + }, +}); +``` diff --git a/references/v3-catalog/src/trigger/playwrightTask.ts b/references/v3-catalog/src/trigger/playwrightTask.ts index ab06d3de53..95f342dc0c 100644 --- a/references/v3-catalog/src/trigger/playwrightTask.ts +++ b/references/v3-catalog/src/trigger/playwrightTask.ts @@ -1,21 +1,24 @@ -import { logger, task } from "@trigger.dev/sdk/v3"; -import { chromium } from "playwright"; +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. Add them to the browserType array: `for (const browserType of [chromium, firefox, webkit])` + * 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: { @@ -26,23 +29,82 @@ export const playwrightTestTask = task({ logger.log("Starting Playwright automation task", { version: playwrightVersion }); - for (const browserType of [chromium]) { - const prefix = (msg: string) => `[${browserType.name()}]: ${msg}`; + // 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(); - const browser = await browserType.launch(); - logger.log(prefix("Browser launched")); + logger.log(prefix("New browser acquired from locals")); - const page = await browser.newPage(); - logger.log(prefix("New page created")); + const page = await newBrowser.newPage(); + logger.log(prefix("New page created")); - await page.goto("https://google.com"); - logger.log(prefix("Navigated to google.com")); + 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 }); + const screenshot = await page.screenshot({ path: "screenshot.png" }); + logger.log(prefix("Screenshot taken"), { size: screenshot.byteLength }); - await browser.close(); - logger.log(prefix("Browser closed")); - } + 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)")); +}); From a7038944c8a829032e3886a6ea05f7be7aec62d8 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 13 May 2025 12:15:55 +0100 Subject: [PATCH 12/12] update jsdoc --- packages/build/src/extensions/playwright.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/build/src/extensions/playwright.ts b/packages/build/src/extensions/playwright.ts index 988305274e..0f675d85ed 100644 --- a/packages/build/src/extensions/playwright.ts +++ b/packages/build/src/extensions/playwright.ts @@ -184,8 +184,13 @@ const debian12Deps = { }; /** - * Creates a Playwright extension for trigger.dev - * @param options Configuration options + * 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);