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, + }, + }); + } +} 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"], },