Compare commits

..

12 Commits

Author SHA1 Message Date
Sparticuz cd113882ea 130.0.0 2024-10-17 12:45:36 -04:00
Kyle McNally f664837067
Merge pull request #310 from Sparticuz/chromium/130 2024-10-17 12:40:34 -04:00
Kyle McNally 1dbbfc7ac4 Add chromium 130 2024-10-17 12:36:02 -04:00
Sparticuz dfd47f4102 129.0.0 2024-10-04 12:09:12 -04:00
Kyle McNally 540387d11a
Merge pull request #306 from Sparticuz/chromium/129 2024-10-04 12:04:40 -04:00
Kyle McNally a64b8612f6 Update chromium to 129 2024-10-04 11:53:55 -04:00
Kyle McNally c031905fbf Update package deps 2024-10-04 11:53:33 -04:00
Sparticuz 517b6c43a4 127.0.0 2024-08-19 16:11:13 -04:00
Kyle McNally f9f32ca64c
Merge pull request #290 from Sparticuz/chromium/127 2024-08-19 16:09:26 -04:00
Kyle McNally 2f7267da42 Update deps 2024-08-19 16:01:01 -04:00
Kyle McNally b580ec7aae Chromium 127 2024-08-19 16:00:50 -04:00
Kyle McNally 58613272ad Puppeteer updates for tests 2024-08-19 15:59:14 -04:00
16 changed files with 391 additions and 270 deletions

View File

@ -20,7 +20,7 @@ test18:
npm install --fund=false --package-lock=false
npm run build
mkdir -p nodejs
npm install --prefix nodejs/ tar-fs@3.0.5 follow-redirects@1.15.6 --bin-links=false --fund=false --omit=optional --omit=dev --package-lock=false --save=false
npm install --prefix nodejs/ tar-fs@3.0.6 follow-redirects@1.15.9 --bin-links=false --fund=false --omit=optional --omit=dev --package-lock=false --save=false
npm pack
mkdir -p nodejs/node_modules/@sparticuz/chromium/
tar --directory nodejs/node_modules/@sparticuz/chromium/ --extract --file sparticuz-chromium-*.tgz --strip-components=1

View File

@ -11,7 +11,7 @@
[sparticuz/chrome-aws-lambda](https://github.com/sparticuz/chrome-aws-lambda) was originally forked from [alixaxel/chrome-aws-lambda#264](https://github.com/alixaxel/chrome-aws-lambda/pull/264).
The biggest difference, besides the chromium version, is the inclusion of some code from https://github.com/alixaxel/lambdafs, as well as dropping that as a dependency. Due to some changes in WebGL, the files in bin/swiftshader.tar.br need to be extracted to `/tmp` instead of `/tmp/swiftshader`. This necessitated changes in lambdafs.
However, it quickly became difficult to maintain because of the pace of `puppeteer` updates. This package, `@sparticuz/chromium`, is not chained to `puppeteer` versions, but also does not include the overrides and hooks that the original package contained. It is only `chromium`, as well as the special code needed to decompress the brotli package, and an additional set of predefined arguments tailored to serverless usage.
However, it quickly became difficult to maintain because of the pace of `puppeteer` updates. This package, `@sparticuz/chromium`, is not chained to `puppeteer` versions, but also does not include the overrides and hooks that the original package contained. It is only `chromium`, as well as the special code needed to decompress the brotli package, and a set of predefined arguments tailored to serverless usage.
## Install
@ -50,6 +50,14 @@ const test = require("node:test");
const puppeteer = require("puppeteer-core");
const chromium = require("@sparticuz/chromium");
// Optional: If you'd like to use the new headless mode. "shell" is the default.
// NOTE: Because we build the shell binary, this option does not work.
// However, this option will stay so when we migrate to full chromium it will work.
chromium.setHeadlessMode = true;
// Optional: If you'd like to disable webgl, true is the default.
chromium.setGraphicsMode = false;
// Optional: Load any fonts you need. Open Sans is included by default in AWS Lambda instances
await chromium.font(
"https://raw.githack.com/googlei18n/noto-emoji/master/fonts/NotoColorEmoji.ttf"
@ -57,20 +65,10 @@ await chromium.font(
test("Check the page title of example.com", async (t) => {
const browser = await puppeteer.launch({
args: puppeteer.defaultArgs({
args: chromium.args,
headless: "shell",
}),
defaultViewport: {
deviceScaleFactor: 1,
hasTouch: false,
height: 1080,
isLandscape: true,
isMobile: false,
width: 1920,
},
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: "shell",
headless: chromium.headless,
});
const page = await browser.newPage();
@ -94,7 +92,7 @@ test("Check the page title of example.com", async (t) => {
const browser = await playwright.launch({
args: chromium.args,
executablePath: await chromium.executablePath(),
headless: true,
headless: chromium.headless,
});
const context = await browser.newContext();
@ -129,13 +127,10 @@ In this example, /opt/chromium contains all the brotli files
```javascript
const browser = await puppeteer.launch({
args: puppeteer.defaultArgs({
args: chromium.args,
headless: "shell",
}),
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath("/opt/chromium"),
headless: "shell",
headless: chromium.headless,
});
```
@ -147,15 +142,12 @@ The latest chromium-pack.tar file will be on the latest [release](https://github
```javascript
const browser = await puppeteer.launch({
args: puppeteer.defaultArgs({
args: chromium.args,
headless: "shell",
}),
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(
"https://www.example.com/chromiumPack.tar"
),
headless: "shell",
headless: chromium.headless,
});
```
@ -185,12 +177,12 @@ For example, you can set your code to use an ENV variable such as `IS_LOCAL`, th
```javascript
const browser = await puppeteer.launch({
args: process.env.IS_LOCAL ? puppeteer.defaultArgs() : puppeteer.defaultArgs({args:chromium.args, process.env.IS_LOCAL ? false : "shell"}),
args: process.env.IS_LOCAL ? puppeteer.defaultArgs() : chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: process.env.IS_LOCAL
? "/tmp/localChromium/chromium/linux-1122391/chrome-linux/chrome"
: await chromium.executablePath(),
headless: process.env.IS_LOCAL ? false : "shell",
headless: process.env.IS_LOCAL ? false : chromium.headless,
});
```
@ -302,20 +294,20 @@ zip -9 --filesync --move --recurse-paths fonts.zip fonts/
## Graphics
NOTE: Disabling the Graphics stack is currently broken, causing Chromium to crash repeadly. For now, even if you `setGraphicsMode=false`, this package will set it to `true`.
By default, this package uses `swiftshader`/`angle` to do CPU acceleration for WebGL. This is the only known way to enable WebGL on a serverless platform. You can disable WebGL by setting `chromium.setGraphiceMode = false;` _before_ launching Chromium. Disabling this will also skip the extract of the `bin/swiftshader.tar.br` file, which saves about a second of initial execution time. Disabling graphics is recommended if you know you are not using any WebGL.
## API
| Method / Property | Returns | Description |
| ----------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `font(url)` | `Promise<string>` | Provisions a custom font and returns its basename. |
| `args` | `Array<string>` | Provides a list of recommended additional [Chromium flags](https://github.com/GoogleChrome/chrome-launcher/blob/master/docs/chrome-flags-for-tools.md) that are specific to running in Lambda. |
| `defaultViewport` | `Object` | Returns a sensible default viewport for serverless. |
| `executablePath(location?: string)` | `Promise<string>` | Returns the path the Chromium binary was extracted to. |
| `setGraphicsMode` | `void` | Sets the graphics mode to either `true` or `false` |
| `graphics` | `boolean` | Returns a boolean depending on whether webgl is enabled or disabled |
| Method / Property | Returns | Description |
| ----------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `font(url)` | `Promise<string>` | Provisions a custom font and returns its basename. |
| `args` | `Array<string>` | Provides a list of recommended additional [Chromium flags](https://github.com/GoogleChrome/chrome-launcher/blob/master/docs/chrome-flags-for-tools.md). |
| `defaultViewport` | `Object` | Returns a sensible default viewport for serverless. |
| `executablePath(location?: string)` | `Promise<string>` | Returns the path the Chromium binary was extracted to. |
| `setHeadlessMode` | `void` | Sets the headless mode to either `true` or `"shell"` |
| `headless` | `true \| "shell"` | Returns `true` or `"shell"` depending on what version of chrome's headless you are running |
| `setGraphicsMode` | `void` | Sets the graphics mode to either `true` or `false` |
| `graphics` | `boolean` | Returns a boolean depending on whether webgl is enabled or disabled |
## Compiling
@ -365,13 +357,11 @@ exports.handler = async (event, context, callback) => {
try {
- browser = await chromium.puppeteer.launch({
+ browser = await puppeteer.launch({
- args: chromium.args,
+ args: puppeteer.defaultArgs({ args: chromium.args, headless: "shell" }),
args: chromium.args,
defaultViewport: chromium.defaultViewport,
- executablePath: await chromium.executablePath,
+ executablePath: await chromium.executablePath(),
- headless: chromium.headless,
+ headless: "shell",
headless: chromium.headless,
ignoreHTTPSErrors: true,
});

View File

@ -3,14 +3,14 @@
"url": "https://example.com",
"expected": {
"title": "Example Domain",
"screenshot": "6b6bfde6d0cd0035255ac81eb4c6a8823fc74f02"
"screenshot": "e610a8be5568f23c453b08928460aae3ae0b4b0a"
}
},
{
"url": "https://get.webgl.org",
"expected": {
"remove": "logo-container",
"screenshot": "1e4d1ce70b48ca14c1cee2a8e5f458f6ea1b987d"
"screenshot": "ec6c79a571b4cb5727c6fc23f9da30de3868138c"
}
}
]

View File

@ -3,19 +3,17 @@ const { createHash } = require("node:crypto");
const puppeteer = require("puppeteer-core");
const chromium = require("@sparticuz/chromium");
exports.handler = async (event) => {
exports.handler = async (event, context) => {
let browser = null;
try {
browser = await puppeteer.launch({
args: puppeteer.defaultArgs({
args: chromium.args,
headless: "shell",
}),
args: chromium.args,
defaultViewport: chromium.defaultViewport,
dumpio: true,
executablePath: await chromium.executablePath(),
headless: "shell",
ignoreHTTPSErrors: true,
headless: chromium.headless,
acceptInsecureCerts: true,
});
console.log("Chromium version", await browser.version());
@ -40,7 +38,7 @@ exports.handler = async (event) => {
document.getElementById(selector).remove();
}, job.expected.remove);
}
const screenshot = await page.screenshot();
const screenshot = Buffer.from(await page.screenshot());
/*
console.log(
`data:image/png;base64,${screenshot.toString("base64")}`,

View File

@ -14,4 +14,4 @@ instance_size=c7i.12xlarge
ansible_connection=ssh
ansible_python_interpreter=auto_silent
ansible_ssh_private_key_file=ansible.pem
chromium_revision=1300313
chromium_revision=1343869

Binary file not shown.

Binary file not shown.

View File

@ -1,14 +1,12 @@
import chromium from "@sparticuz/chromium";
import puppeteer from "puppeteer-core";
import chromium from '@sparticuz/chromium';
import puppeteer from 'puppeteer-core';
export const lambdaHandler = async (event, context) => {
const browser = await puppeteer.launch({
args: puppeteer.defaultArgs({
args: chromium.args,
headless: "shell",
}),
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: "shell",
headless: chromium.headless,
});
const page = await browser.newPage();
@ -22,5 +20,5 @@ export const lambdaHandler = async (event, context) => {
await browser.close();
return { result: "success", browserVersion, pageTitle };
};
return { result: 'success', browserVersion, pageTitle };
}

View File

@ -4,12 +4,10 @@ const chromium = require("@sparticuz/chromium");
const handler = async () => {
try {
const browser = await puppeteer.launch({
args: puppeteer.defaultArgs({
args: chromium.args,
headless: "shell",
}),
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: "shell",
headless: chromium.headless,
ignoreHTTPSErrors: true,
});

View File

@ -4,10 +4,8 @@ const chromium = require("@sparticuz/chromium-min");
const handler = async () => {
try {
const browser = await puppeteer.launch({
args: puppeteer.defaultArgs({
args: chromium.args,
headless: "shell",
}),
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(
"https://github.com/Sparticuz/chromium/releases/download/v110.0.1/chromium-v110.0.1-pack.tar"
),

View File

@ -5,12 +5,10 @@ module.exports = {
handler: async () => {
try {
const browser = await puppeteer.launch({
args: puppeteer.defaultArgs({
args: chromium.args,
headless: "shell",
}),
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: "shell",
headless: chromium.headless,
ignoreHTTPSErrors: true,
});

51
package-lock.json generated
View File

@ -1,25 +1,25 @@
{
"name": "@sparticuz/chromium",
"version": "126.0.0",
"version": "130.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@sparticuz/chromium",
"version": "126.0.0",
"version": "130.0.0",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"follow-redirects": "^1.15.9",
"tar-fs": "^3.0.6"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
"@tsconfig/strictest": "^2.0.5",
"@types/follow-redirects": "^1.14.4",
"@types/node": "^20.14.11",
"@types/node": "^20.16.10",
"@types/tar-fs": "^2.0.4",
"clean-modules": "^3.0.5",
"typescript": "^5.5.3"
"clean-modules": "^3.1.1",
"typescript": "^5.6.2"
},
"engines": {
"node": ">= 16"
@ -47,12 +47,13 @@
}
},
"node_modules/@types/node": {
"version": "20.14.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz",
"integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==",
"version": "20.16.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
"undici-types": "~6.19.2"
}
},
"node_modules/@types/tar-fs": {
@ -113,10 +114,11 @@
}
},
"node_modules/clean-modules": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/clean-modules/-/clean-modules-3.0.5.tgz",
"integrity": "sha512-gRW2hxNEE+xunuv/lkdPQ6UAqhs6CGoshLxOZ6eqy2ytkUAyzSoQ4fFj8/51jAfmJhrbuBGd/8hnvplIp8KRDg==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/clean-modules/-/clean-modules-3.1.1.tgz",
"integrity": "sha512-t/7dNtn6vQYxujYxdwZeLa0NsLE92KQ0XeV3CDJ2TXgLTvn3ijmjlQN0Dm9wjYQgC0miZiF66ClTQzgIeYw96A==",
"dev": true,
"license": "ISC",
"dependencies": {
"clipanion": "^3.2.1",
"picomatch": "^2.3.0",
@ -157,15 +159,16 @@
"integrity": "sha512-IgfweLvEpwyA4WgiQe9Nx6VV2QkML2NkvZnk1oKnIzXgXdWxuhF7zw4DvLTPZJn6PIUneiAXPF24QmoEqHTjyw=="
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
@ -299,10 +302,11 @@
"dev": true
},
"node_modules/typescript": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -312,10 +316,11 @@
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
"version": "6.19.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz",
"integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==",
"dev": true,
"license": "MIT"
},
"node_modules/wrappy": {
"version": "1.0.2",

View File

@ -1,6 +1,6 @@
{
"name": "@sparticuz/chromium",
"version": "126.0.0",
"version": "130.0.0",
"description": "Chromium Binary for Serverless Platforms",
"keywords": [
"aws",
@ -36,17 +36,17 @@
"test": "make clean && make && make pretest && make test"
},
"dependencies": {
"follow-redirects": "^1.15.6",
"follow-redirects": "^1.15.9",
"tar-fs": "^3.0.6"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
"@tsconfig/strictest": "^2.0.5",
"@types/follow-redirects": "^1.14.4",
"@types/node": "^20.14.11",
"@types/node": "^20.16.10",
"@types/tar-fs": "^2.0.4",
"clean-modules": "^3.0.5",
"typescript": "^5.5.3"
"clean-modules": "^3.1.1",
"typescript": "^5.6.2"
},
"engines": {
"node": ">= 16"

View File

@ -1,16 +1,18 @@
import { https } from "follow-redirects";
import { unlink } from "node:fs";
import { https } from "follow-redirects";
import { tmpdir } from "node:os";
import { extract } from "tar-fs";
import { parse } from "node:url";
import type { UrlWithStringQuery } from "node:url";
interface FollowRedirOptions extends URL {
interface FollowRedirOptions extends UrlWithStringQuery {
maxBodyLength: number;
}
export const isValidUrl = (input: string) => {
try {
return !!new URL(input);
} catch {
} catch (err) {
return false;
}
};
@ -55,20 +57,20 @@ export const isRunningInAwsLambdaNode20 = () => {
export const downloadAndExtract = async (url: string) =>
new Promise<string>((resolve, reject) => {
const getOptions = new URL(url) as FollowRedirOptions;
const getOptions = parse(url) as FollowRedirOptions;
getOptions.maxBodyLength = 60 * 1024 * 1024; // 60mb
const destinationDirectory = `${tmpdir()}/chromium-pack`;
const extractObject = extract(destinationDirectory);
const destDir = `${tmpdir()}/chromium-pack`;
const extractObj = extract(destDir);
https
.get(url, (response) => {
response.pipe(extractObject);
extractObject.on("finish", () => {
resolve(destinationDirectory);
response.pipe(extractObj);
extractObj.on("finish", () => {
resolve(destDir);
});
})
.on("error", (error) => {
unlink(destinationDirectory, () => {
reject(error);
.on("error", (err) => {
unlink(destDir, (_) => {
reject(err);
});
});
});

View File

@ -1,4 +1,3 @@
import { https } from "follow-redirects";
import {
access,
createWriteStream,
@ -6,18 +5,50 @@ import {
mkdirSync,
symlink,
} from "node:fs";
import { https } from "follow-redirects";
import LambdaFS from "./lambdafs";
import { join } from "node:path";
import { URL } from "node:url";
import {
downloadAndExtract,
isRunningInAwsLambda,
isRunningInAwsLambdaNode20,
isValidUrl,
isRunningInAwsLambdaNode20,
} from "./helper";
import { inflate } from "./lambdafs";
// Set up the environmental variables
/** Viewport taken from https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.viewport.md */
interface Viewport {
/**
* The page width in pixels.
*/
width: number;
/**
* The page height in pixels.
*/
height: number;
/**
* Specify device scale factor.
* See {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio | devicePixelRatio} for more info.
* @default 1
*/
deviceScaleFactor?: number;
/**
* Whether the `meta viewport` tag is taken into account.
* @default false
*/
isMobile?: boolean;
/**
* Specifies if the viewport is in landscape mode.
* @default false
*/
isLandscape?: boolean;
/**
* Specify if the viewport supports touch events.
* @default false
*/
hasTouch?: boolean;
}
if (isRunningInAwsLambda()) {
if (process.env["FONTCONFIG_PATH"] === undefined) {
process.env["FONTCONFIG_PATH"] = "/tmp/fonts";
@ -57,73 +88,208 @@ if (isRunningInAwsLambdaNode20()) {
}
class Chromium {
/**
* Determines the headless mode that chromium will run at
* https://developer.chrome.com/articles/new-headless/#try-out-the-new-headless
* @values true or "new"
*/
private static headlessMode: true | "shell" = "shell";
/**
* If true, the graphics stack and webgl is enabled,
* If false, webgl will be disabled.
* (If false, the swiftshader.tar.br file will also not extract)
*/
private static graphicsMode = true;
private static graphicsMode: boolean = true;
/**
* Downloads or symlinks a custom font and returns its basename, patching the environment so that Chromium can find it.
*/
static font(input: string): Promise<string> {
if (process.env["HOME"] === undefined) {
process.env["HOME"] = "/tmp";
}
if (existsSync(`${process.env["HOME"]}/.fonts`) !== true) {
mkdirSync(`${process.env["HOME"]}/.fonts`);
}
return new Promise((resolve, reject) => {
if (/^https?:[/][/]/i.test(input) !== true) {
input = `file://${input}`;
}
const url = new URL(input);
const output = `${process.env["HOME"]}/.fonts/${url.pathname
.split("/")
.pop()}`;
if (existsSync(output) === true) {
return resolve(output.split("/").pop() as string);
}
if (url.protocol === "file:") {
access(url.pathname, (error) => {
if (error != null) {
return reject(error);
}
symlink(url.pathname, output, (error) => {
return error != null
? reject(error)
: resolve(url.pathname.split("/").pop() as string);
});
});
} else {
https.get(input, (response) => {
if (response.statusCode !== 200) {
return reject(`Unexpected status code: ${response.statusCode}.`);
}
const stream = createWriteStream(output);
stream.once("error", (error) => {
return reject(error);
});
response.on("data", (chunk) => {
stream.write(chunk);
});
response.once("end", () => {
stream.end(() => {
return resolve(url.pathname.split("/").pop() as string);
});
});
});
}
});
}
/**
* Returns a list of additional Chromium flags recommended for serverless environments.
* The canonical list of flags can be found on https://peter.sh/experiments/chromium-command-line-switches/.
* Most of below can be found here: https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md
*/
static get args(): string[] {
/**
* These are the default args in puppeteer.
* https://github.com/puppeteer/puppeteer/blob/3a31070d054fa3cd8116ca31c578807ed8d6f987/packages/puppeteer-core/src/node/ChromeLauncher.ts#L185
*/
const puppeteerFlags = [
"--allow-pre-commit-input",
"--disable-background-networking",
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-breakpad",
"--disable-client-side-phishing-detection",
"--disable-component-extensions-with-background-pages",
"--disable-component-update",
"--disable-default-apps",
"--disable-dev-shm-usage",
"--disable-extensions",
"--disable-hang-monitor",
"--disable-ipc-flooding-protection",
"--disable-popup-blocking",
"--disable-prompt-on-repost",
"--disable-renderer-backgrounding",
"--disable-sync",
"--enable-automation",
// TODO(sadym): remove '--enable-blink-features=IdleDetection' once
// IdleDetection is turned on by default.
"--enable-blink-features=IdleDetection",
"--export-tagged-pdf",
"--force-color-profile=srgb",
"--metrics-recording-only",
"--no-first-run",
"--password-store=basic",
"--use-mock-keychain",
];
const puppeteerDisableFeatures = [
"Translate",
"BackForwardCache",
// AcceptCHFrame disabled because of crbug.com/1348106.
"AcceptCHFrame",
"MediaRouter",
"OptimizationHints",
];
const puppeteerEnableFeatures = ["NetworkServiceInProcess2"];
const chromiumFlags = [
"--ash-no-nudges", // Avoids blue bubble "user education" nudges (eg., "… give your browser a new look", Memory Saver)
"--disable-domain-reliability", // Disables Domain Reliability Monitoring, which tracks whether the browser has difficulty contacting Google-owned sites and uploads reports to Google.
"--disable-domain-reliability", // https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md#background-networking
"--disable-print-preview", // https://source.chromium.org/search?q=lang:cpp+symbol:kDisablePrintPreview&ss=chromium
// "--disable-speech-api", // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSpeechAPI&ss=chromium
"--disable-speech-api", // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSpeechAPI&ss=chromium
"--disk-cache-size=33554432", // https://source.chromium.org/search?q=lang:cpp+symbol:kDiskCacheSize&ss=chromium
"--no-default-browser-check", // Disable the default browser check, do not prompt to set it as such
"--no-pings", // Don't send hyperlink auditing pings
"--single-process", // Runs the renderer and plugins in the same process as the browser. NOTES: Needs to be single-process to avoid `prctl(PR_SET_NO_NEW_PRIVS) failed` error
"--mute-audio", // https://source.chromium.org/search?q=lang:cpp+symbol:kMuteAudio&ss=chromium
"--no-default-browser-check", // https://source.chromium.org/search?q=lang:cpp+symbol:kNoDefaultBrowserCheck&ss=chromium
"--no-pings", // https://source.chromium.org/search?q=lang:cpp+symbol:kNoPings&ss=chromium
"--single-process", // Needs to be single-process to avoid `prctl(PR_SET_NO_NEW_PRIVS) failed` error
"--font-render-hinting=none", // https://github.com/puppeteer/puppeteer/issues/2410#issuecomment-560573612
];
const chromiumDisableFeatures = [
// "AutofillServerCommunication", // Disables autofill server communication. This feature isn't disabled via other 'parent' flags.
"AudioServiceOutOfProcess",
// "DestroyProfileOnBrowserClose", // Disable the feature of: Destroy profiles when their last browser window is closed, instead of when the browser exits.
// "InterestFeedContentSuggestions", // Disables the Discover feed on NTP
"IsolateOrigins",
"site-per-process", // Disables OOPIF. https://www.chromium.org/Home/chromium-security/site-isolation
"site-per-process",
];
const chromiumEnableFeatures = ["SharedArrayBuffer"];
const graphicsFlags = [
"--hide-scrollbars", // https://source.chromium.org/search?q=lang:cpp+symbol:kHideScrollbars&ss=chromium
"--ignore-gpu-blocklist", // https://source.chromium.org/search?q=lang:cpp+symbol:kIgnoreGpuBlocklist&ss=chromium
"--in-process-gpu", // Saves some memory by moving GPU process into a browser process thread
"--window-size=1920,1080", // Sets the initial window size. Provided as string in the format "800,600".
"--in-process-gpu", // https://source.chromium.org/search?q=lang:cpp+symbol:kInProcessGPU&ss=chromium
"--window-size=1920,1080", // https://source.chromium.org/search?q=lang:cpp+symbol:kWindowSize&ss=chromium
];
// https://chromium.googlesource.com/chromium/src/+/main/docs/gpu/swiftshader.md
// https://developer.chrome.com/blog/supercharge-web-ai-testing
// https://www.browserless.io/blog/2023/08/31/browserless-gpu-instances/
this.graphics &&
graphicsFlags.push(
"--enable-gpu",
"--use-gl=angle",
"--use-angle=swiftshader"
);
// Blocked by https://github.com/Sparticuz/chromium/issues/247
//this.graphics
// ? graphicsFlags.push("--use-gl=angle", "--use-angle=swiftshader")
// : graphicsFlags.push("--disable-webgl");
graphicsFlags.push("--use-gl=angle", "--use-angle=swiftshader");
const insecureFlags = [
"--allow-running-insecure-content", // https://source.chromium.org/search?q=lang:cpp+symbol:kAllowRunningInsecureContent&ss=chromium
"--disable-setuid-sandbox", // Lambda runs as root, so this is required to allow Chromium to run as root
"--disable-setuid-sandbox", // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSetuidSandbox&ss=chromium
"--disable-site-isolation-trials", // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSiteIsolation&ss=chromium
"--disable-web-security", // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableWebSecurity&ss=chromium
"--no-sandbox", // Lambda runs as root, so this is required to allow Chromium to run as root
"--no-zygote", // https://codereview.chromium.org/2384163002
"--no-sandbox", // https://source.chromium.org/search?q=lang:cpp+symbol:kNoSandbox&ss=chromium
"--no-zygote", // https://source.chromium.org/search?q=lang:cpp+symbol:kNoZygote&ss=chromium
];
const headlessFlags = [
this.headless === "shell" ? "--headless='shell'" : "--headless",
];
return [
...puppeteerFlags,
...chromiumFlags,
`--disable-features=${[...chromiumDisableFeatures].join(",")}`,
`--enable-features=${[...chromiumEnableFeatures].join(",")}`,
`--disable-features=${[
...puppeteerDisableFeatures,
...chromiumDisableFeatures,
].join(",")}`,
`--enable-features=${[
...puppeteerEnableFeatures,
...chromiumEnableFeatures,
].join(",")}`,
...graphicsFlags,
...insecureFlags,
...headlessFlags,
];
}
/**
* Returns sensible default viewport settings for serverless environments.
*/
static get defaultViewport(): Required<Viewport> {
return {
deviceScaleFactor: 1,
hasTouch: false,
height: 1080,
isLandscape: true,
isMobile: false,
width: 1920,
};
}
/**
* Inflates the included version of Chromium
* @param input The location of the `bin` folder
@ -162,19 +328,19 @@ class Chromium {
// Extract the required files
const promises = [
inflate(`${input}/chromium.br`),
inflate(`${input}/fonts.tar.br`),
LambdaFS.inflate(`${input}/chromium.br`),
LambdaFS.inflate(`${input}/fonts.tar.br`),
];
if (this.graphics) {
// Only inflate graphics stack if needed
promises.push(inflate(`${input}/swiftshader.tar.br`));
promises.push(LambdaFS.inflate(`${input}/swiftshader.tar.br`));
}
if (isRunningInAwsLambda()) {
// If running in AWS Lambda, extract more required files
promises.push(inflate(`${input}/al2.tar.br`));
promises.push(LambdaFS.inflate(`${input}/al2.tar.br`));
}
if (isRunningInAwsLambdaNode20()) {
promises.push(inflate(`${input}/al2023.tar.br`));
promises.push(LambdaFS.inflate(`${input}/al2023.tar.br`));
}
// Await all extractions
@ -184,67 +350,33 @@ class Chromium {
}
/**
* Downloads or symlinks a custom font and returns its basename, patching the environment so that Chromium can find it.
* Returns the headless mode.
* "shell" means the 'old' (legacy, chromium < 112) headless mode.
* `true` means the 'new' headless mode.
* https://developer.chrome.com/articles/new-headless/#try-out-the-new-headless
* @returns true | "shell"
*/
static font(input: string): Promise<string> {
if (process.env["HOME"] === undefined) {
process.env["HOME"] = "/tmp";
public static get headless() {
return this.headlessMode;
}
/**
* Sets the headless mode.
* "shell" means the 'old' (legacy, chromium < 112) headless mode.
* `true` means the 'new' headless mode.
* https://developer.chrome.com/articles/new-headless/#try-out-the-new-headless
* @default "shell"
*/
public static set setHeadlessMode(value: true | "shell") {
if (
(typeof value === "string" && value !== "shell") ||
(typeof value === "boolean" && value !== true)
) {
throw new Error(
`Headless mode must be either \`true\` or 'shell', you entered '${value}'`
);
}
if (existsSync(`${process.env["HOME"]}/.fonts`) !== true) {
mkdirSync(`${process.env["HOME"]}/.fonts`);
}
return new Promise((resolve, reject) => {
if (/^https?:\/\//i.test(input) !== true) {
input = `file://${input}`;
}
const url = new URL(input);
const output = `${process.env["HOME"]}/.fonts/${url.pathname
.split("/")
.pop()}`;
if (existsSync(output) === true) {
return resolve(output.split("/").pop() as string);
}
if (url.protocol === "file:") {
access(url.pathname, (error) => {
if (error != null) {
return reject(error);
}
symlink(url.pathname, output, (error) => {
return error == null
? resolve(url.pathname.split("/").pop() as string)
: reject(error);
});
});
} else {
https.get(input, (response) => {
if (response.statusCode !== 200) {
return reject(`Unexpected status code: ${response.statusCode}.`);
}
const stream = createWriteStream(output);
stream.once("error", (error) => {
return reject(error);
});
response.on("data", (chunk) => {
stream.write(chunk);
});
response.once("end", () => {
stream.end(() => {
return resolve(url.pathname.split("/").pop() as string);
});
});
});
}
});
this.headlessMode = value;
}
/**
@ -264,7 +396,7 @@ class Chromium {
*/
public static set setGraphicsMode(value: boolean) {
if (typeof value !== "boolean") {
throw new TypeError(
throw new Error(
`Graphics mode must be a boolean, you entered '${value}'`
);
}

View File

@ -1,73 +1,75 @@
import { createReadStream, createWriteStream, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, join } from "node:path";
import { createBrotliDecompress, createUnzip } from "node:zlib";
import { extract } from "tar-fs";
import { createBrotliDecompress, createUnzip } from "node:zlib";
/**
* Decompresses a (tarballed) Brotli or Gzip compressed file and returns the path to the decompressed file/folder.
*
* @param filePath Path of the file to decompress.
*/
export const inflate = (filePath: string): Promise<string> => {
const output = filePath.includes("swiftshader")
? tmpdir()
: join(
tmpdir(),
basename(filePath).replace(
/\.(?:t(?:ar(?:\.(?:br|gz))?|br|gz)|br|gz)$/i,
""
)
);
class LambdaFS {
/**
* Decompresses a (tarballed) Brotli or Gzip compressed file and returns the path to the decompressed file/folder.
*
* @param filePath Path of the file to decompress.
*/
static inflate(filePath: string): Promise<string> {
const output = filePath.includes("swiftshader")
? tmpdir()
: join(
tmpdir(),
basename(filePath).replace(
/[.](?:t(?:ar(?:[.](?:br|gz))?|br|gz)|br|gz)$/i,
""
)
);
return new Promise((resolve, reject) => {
if (filePath.includes("swiftshader")) {
if (existsSync(`${output}/libGLESv2.so`)) {
return resolve(output);
return new Promise((resolve, reject) => {
if (filePath.includes("swiftshader")) {
if (existsSync(`${output}/libGLESv2.so`)) {
return resolve(output);
}
} else {
if (existsSync(output) === true) {
return resolve(output);
}
}
} else {
if (existsSync(output) === true) {
return resolve(output);
let source = createReadStream(filePath, { highWaterMark: 2 ** 23 });
let target = null;
if (/[.](?:t(?:ar(?:[.](?:br|gz))?|br|gz))$/i.test(filePath) === true) {
target = extract(output);
target.once("finish", () => {
return resolve(output);
});
} else {
target = createWriteStream(output, { mode: 0o700 });
}
}
const source = createReadStream(filePath, { highWaterMark: 2 ** 23 });
let target = null;
source.once("error", (error: Error) => {
return reject(error);
});
if (/\.t(?:ar(?:\.(?:br|gz))?|br|gz)$/i.test(filePath) === true) {
target = extract(output);
target.once("error", (error: Error) => {
return reject(error);
});
target.once("finish", () => {
target.once("close", () => {
return resolve(output);
});
} else {
target = createWriteStream(output, { mode: 0o700 });
}
source.once("error", (error: Error) => {
return reject(error);
if (/(?:br|gz)$/i.test(filePath) === true) {
source
.pipe(
/br$/i.test(filePath)
? createBrotliDecompress({ chunkSize: 2 ** 21 })
: createUnzip({ chunkSize: 2 ** 21 })
)
.pipe(target);
} else {
source.pipe(target);
}
});
}
}
target.once("error", (error: Error) => {
return reject(error);
});
target.once("close", () => {
return resolve(output);
});
if (/(?:br|gz)$/i.test(filePath) === true) {
source
.pipe(
/br$/i.test(filePath)
? createBrotliDecompress({ chunkSize: 2 ** 21 })
: createUnzip({ chunkSize: 2 ** 21 })
)
.pipe(target);
} else {
source.pipe(target);
}
});
};
export default inflate;
export default LambdaFS;