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 install --fund=false --package-lock=false
npm run build npm run build
mkdir -p nodejs 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 npm pack
mkdir -p nodejs/node_modules/@sparticuz/chromium/ mkdir -p nodejs/node_modules/@sparticuz/chromium/
tar --directory nodejs/node_modules/@sparticuz/chromium/ --extract --file sparticuz-chromium-*.tgz --strip-components=1 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). [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. 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 ## Install
@ -50,6 +50,14 @@ const test = require("node:test");
const puppeteer = require("puppeteer-core"); const puppeteer = require("puppeteer-core");
const chromium = require("@sparticuz/chromium"); 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 // Optional: Load any fonts you need. Open Sans is included by default in AWS Lambda instances
await chromium.font( await chromium.font(
"https://raw.githack.com/googlei18n/noto-emoji/master/fonts/NotoColorEmoji.ttf" "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) => { test("Check the page title of example.com", async (t) => {
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
args: puppeteer.defaultArgs({ args: chromium.args,
args: chromium.args, defaultViewport: chromium.defaultViewport,
headless: "shell",
}),
defaultViewport: {
deviceScaleFactor: 1,
hasTouch: false,
height: 1080,
isLandscape: true,
isMobile: false,
width: 1920,
},
executablePath: await chromium.executablePath(), executablePath: await chromium.executablePath(),
headless: "shell", headless: chromium.headless,
}); });
const page = await browser.newPage(); const page = await browser.newPage();
@ -94,7 +92,7 @@ test("Check the page title of example.com", async (t) => {
const browser = await playwright.launch({ const browser = await playwright.launch({
args: chromium.args, args: chromium.args,
executablePath: await chromium.executablePath(), executablePath: await chromium.executablePath(),
headless: true, headless: chromium.headless,
}); });
const context = await browser.newContext(); const context = await browser.newContext();
@ -129,13 +127,10 @@ In this example, /opt/chromium contains all the brotli files
```javascript ```javascript
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
args: puppeteer.defaultArgs({ args: chromium.args,
args: chromium.args,
headless: "shell",
}),
defaultViewport: chromium.defaultViewport, defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath("/opt/chromium"), 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 ```javascript
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
args: puppeteer.defaultArgs({ args: chromium.args,
args: chromium.args,
headless: "shell",
}),
defaultViewport: chromium.defaultViewport, defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath( executablePath: await chromium.executablePath(
"https://www.example.com/chromiumPack.tar" "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 ```javascript
const browser = await puppeteer.launch({ 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, defaultViewport: chromium.defaultViewport,
executablePath: process.env.IS_LOCAL executablePath: process.env.IS_LOCAL
? "/tmp/localChromium/chromium/linux-1122391/chrome-linux/chrome" ? "/tmp/localChromium/chromium/linux-1122391/chrome-linux/chrome"
: await chromium.executablePath(), : 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 ## 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. 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 ## API
| Method / Property | Returns | Description | | Method / Property | Returns | Description |
| ----------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ----------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `font(url)` | `Promise<string>` | Provisions a custom font and returns its basename. | | `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. | | `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. | | `defaultViewport` | `Object` | Returns a sensible default viewport for serverless. |
| `executablePath(location?: string)` | `Promise<string>` | Returns the path the Chromium binary was extracted to. | | `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` | | `setHeadlessMode` | `void` | Sets the headless mode to either `true` or `"shell"` |
| `graphics` | `boolean` | Returns a boolean depending on whether webgl is enabled or disabled | | `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 ## Compiling
@ -365,13 +357,11 @@ exports.handler = async (event, context, callback) => {
try { try {
- browser = await chromium.puppeteer.launch({ - browser = await chromium.puppeteer.launch({
+ browser = await puppeteer.launch({ + browser = await puppeteer.launch({
- args: chromium.args, args: chromium.args,
+ args: puppeteer.defaultArgs({ args: chromium.args, headless: "shell" }),
defaultViewport: chromium.defaultViewport, defaultViewport: chromium.defaultViewport,
- executablePath: await chromium.executablePath, - executablePath: await chromium.executablePath,
+ executablePath: await chromium.executablePath(), + executablePath: await chromium.executablePath(),
- headless: chromium.headless, headless: chromium.headless,
+ headless: "shell",
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
}); });

View File

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

View File

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

View File

@ -14,4 +14,4 @@ instance_size=c7i.12xlarge
ansible_connection=ssh ansible_connection=ssh
ansible_python_interpreter=auto_silent ansible_python_interpreter=auto_silent
ansible_ssh_private_key_file=ansible.pem 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 chromium from '@sparticuz/chromium';
import puppeteer from "puppeteer-core"; import puppeteer from 'puppeteer-core';
export const lambdaHandler = async (event, context) => { export const lambdaHandler = async (event, context) => {
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
args: puppeteer.defaultArgs({ args: chromium.args,
args: chromium.args, defaultViewport: chromium.defaultViewport,
headless: "shell",
}),
executablePath: await chromium.executablePath(), executablePath: await chromium.executablePath(),
headless: "shell", headless: chromium.headless,
}); });
const page = await browser.newPage(); const page = await browser.newPage();
@ -22,5 +20,5 @@ export const lambdaHandler = async (event, context) => {
await browser.close(); 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 () => { const handler = async () => {
try { try {
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
args: puppeteer.defaultArgs({ args: chromium.args,
args: chromium.args, defaultViewport: chromium.defaultViewport,
headless: "shell",
}),
executablePath: await chromium.executablePath(), executablePath: await chromium.executablePath(),
headless: "shell", headless: chromium.headless,
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
}); });

View File

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

51
package-lock.json generated
View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import { https } from "follow-redirects";
import { import {
access, access,
createWriteStream, createWriteStream,
@ -6,18 +5,50 @@ import {
mkdirSync, mkdirSync,
symlink, symlink,
} from "node:fs"; } from "node:fs";
import { https } from "follow-redirects";
import LambdaFS from "./lambdafs";
import { join } from "node:path"; import { join } from "node:path";
import { URL } from "node:url"; import { URL } from "node:url";
import { import {
downloadAndExtract, downloadAndExtract,
isRunningInAwsLambda, isRunningInAwsLambda,
isRunningInAwsLambdaNode20,
isValidUrl, isValidUrl,
isRunningInAwsLambdaNode20,
} from "./helper"; } 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 (isRunningInAwsLambda()) {
if (process.env["FONTCONFIG_PATH"] === undefined) { if (process.env["FONTCONFIG_PATH"] === undefined) {
process.env["FONTCONFIG_PATH"] = "/tmp/fonts"; process.env["FONTCONFIG_PATH"] = "/tmp/fonts";
@ -57,73 +88,208 @@ if (isRunningInAwsLambdaNode20()) {
} }
class Chromium { 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 true, the graphics stack and webgl is enabled,
* If false, webgl will be disabled. * If false, webgl will be disabled.
* (If false, the swiftshader.tar.br file will also not extract) * (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. * 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/. * 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[] { 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 = [ const chromiumFlags = [
"--ash-no-nudges", // Avoids blue bubble "user education" nudges (eg., "… give your browser a new look", Memory Saver) "--disable-domain-reliability", // https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md#background-networking
"--disable-domain-reliability", // Disables Domain Reliability Monitoring, which tracks whether the browser has difficulty contacting Google-owned sites and uploads reports to Google.
"--disable-print-preview", // https://source.chromium.org/search?q=lang:cpp+symbol:kDisablePrintPreview&ss=chromium "--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 "--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 "--mute-audio", // https://source.chromium.org/search?q=lang:cpp+symbol:kMuteAudio&ss=chromium
"--no-pings", // Don't send hyperlink auditing pings "--no-default-browser-check", // https://source.chromium.org/search?q=lang:cpp+symbol:kNoDefaultBrowserCheck&ss=chromium
"--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 "--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 "--font-render-hinting=none", // https://github.com/puppeteer/puppeteer/issues/2410#issuecomment-560573612
]; ];
const chromiumDisableFeatures = [ const chromiumDisableFeatures = [
// "AutofillServerCommunication", // Disables autofill server communication. This feature isn't disabled via other 'parent' flags.
"AudioServiceOutOfProcess", "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", "IsolateOrigins",
"site-per-process", // Disables OOPIF. https://www.chromium.org/Home/chromium-security/site-isolation "site-per-process",
]; ];
const chromiumEnableFeatures = ["SharedArrayBuffer"]; const chromiumEnableFeatures = ["SharedArrayBuffer"];
const graphicsFlags = [ 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 "--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 "--in-process-gpu", // https://source.chromium.org/search?q=lang:cpp+symbol:kInProcessGPU&ss=chromium
"--window-size=1920,1080", // Sets the initial window size. Provided as string in the format "800,600". "--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://chromium.googlesource.com/chromium/src/+/main/docs/gpu/swiftshader.md
// https://developer.chrome.com/blog/supercharge-web-ai-testing // Blocked by https://github.com/Sparticuz/chromium/issues/247
// https://www.browserless.io/blog/2023/08/31/browserless-gpu-instances/ //this.graphics
this.graphics && // ? graphicsFlags.push("--use-gl=angle", "--use-angle=swiftshader")
graphicsFlags.push( // : graphicsFlags.push("--disable-webgl");
"--enable-gpu", graphicsFlags.push("--use-gl=angle", "--use-angle=swiftshader");
"--use-gl=angle",
"--use-angle=swiftshader"
);
const insecureFlags = [ const insecureFlags = [
"--allow-running-insecure-content", // https://source.chromium.org/search?q=lang:cpp+symbol:kAllowRunningInsecureContent&ss=chromium "--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-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 "--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-sandbox", // https://source.chromium.org/search?q=lang:cpp+symbol:kNoSandbox&ss=chromium
"--no-zygote", // https://codereview.chromium.org/2384163002 "--no-zygote", // https://source.chromium.org/search?q=lang:cpp+symbol:kNoZygote&ss=chromium
];
const headlessFlags = [
this.headless === "shell" ? "--headless='shell'" : "--headless",
]; ];
return [ return [
...puppeteerFlags,
...chromiumFlags, ...chromiumFlags,
`--disable-features=${[...chromiumDisableFeatures].join(",")}`, `--disable-features=${[
`--enable-features=${[...chromiumEnableFeatures].join(",")}`, ...puppeteerDisableFeatures,
...chromiumDisableFeatures,
].join(",")}`,
`--enable-features=${[
...puppeteerEnableFeatures,
...chromiumEnableFeatures,
].join(",")}`,
...graphicsFlags, ...graphicsFlags,
...insecureFlags, ...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 * Inflates the included version of Chromium
* @param input The location of the `bin` folder * @param input The location of the `bin` folder
@ -162,19 +328,19 @@ class Chromium {
// Extract the required files // Extract the required files
const promises = [ const promises = [
inflate(`${input}/chromium.br`), LambdaFS.inflate(`${input}/chromium.br`),
inflate(`${input}/fonts.tar.br`), LambdaFS.inflate(`${input}/fonts.tar.br`),
]; ];
if (this.graphics) { if (this.graphics) {
// Only inflate graphics stack if needed // Only inflate graphics stack if needed
promises.push(inflate(`${input}/swiftshader.tar.br`)); promises.push(LambdaFS.inflate(`${input}/swiftshader.tar.br`));
} }
if (isRunningInAwsLambda()) { if (isRunningInAwsLambda()) {
// If running in AWS Lambda, extract more required files // 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()) { if (isRunningInAwsLambdaNode20()) {
promises.push(inflate(`${input}/al2023.tar.br`)); promises.push(LambdaFS.inflate(`${input}/al2023.tar.br`));
} }
// Await all extractions // 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> { public static get headless() {
if (process.env["HOME"] === undefined) { return this.headlessMode;
process.env["HOME"] = "/tmp"; }
/**
* 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}'`
);
} }
this.headlessMode = 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);
});
});
});
}
});
} }
/** /**
@ -264,7 +396,7 @@ class Chromium {
*/ */
public static set setGraphicsMode(value: boolean) { public static set setGraphicsMode(value: boolean) {
if (typeof value !== "boolean") { if (typeof value !== "boolean") {
throw new TypeError( throw new Error(
`Graphics mode must be a boolean, you entered '${value}'` `Graphics mode must be a boolean, you entered '${value}'`
); );
} }

View File

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