-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathplaywright.ts
More file actions
372 lines (342 loc) · 11.7 KB
/
playwright.ts
File metadata and controls
372 lines (342 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
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`);
// Determine which browsers to actually install
const browsersToInstall = new Set<PlaywrightBrowser | "chromium-headless-shell">();
this.options.browsers.forEach((browser) => {
if (browser === "chromium") {
if (this.options.headless) {
// For headless mode, only install chromium-headless-shell
browsersToInstall.add("chromium-headless-shell");
} else {
// For non-headless mode, install both chromium and chromium-headless-shell
// This allows users to easily switch between headless and non-headless mode
browsersToInstall.add("chromium");
browsersToInstall.add("chromium-headless-shell");
}
} else {
browsersToInstall.add(browser);
}
});
Array.from(browsersToInstall).forEach((browser) => {
instructions.push(
`RUN grep -A5 -m1 "browser: ${browser}" /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,
},
});
}
}