Compare commits

..

12 Commits

Author SHA1 Message Date
Kyle McNally 39b6e95275 More updates 2024-08-19 09:56:38 -04:00
Kyle McNally 9b8de8ac89 fix test because of removal of viewport options 2024-07-17 10:34:38 -04:00
Kyle McNally db50d16c84 deps and docs update 2024-07-17 09:51:27 -04:00
Kyle McNally 7d6d9c4cf9 let playwright users specify this 2024-07-17 09:50:40 -04:00
Kyle McNally 8037abb918 parse is deprecated 2024-07-17 09:50:09 -04:00
Kyle McNally b63114df4b remove opinionated viewport 2024-07-17 09:49:05 -04:00
Sparticuz f8d0686174 Merge branch 'master' into feature/puppeteerArgs
* master:
  126.0.0
  don't disable the graphics stack https://github.com/Sparticuz/chromium/issues/247
  update to chromium 126
  ignore internal docker folder
  update deps
2024-07-17 08:30:38 -04:00
Kyle McNally e6f131ca70 lint and cleanup 2024-04-15 11:56:37 -04:00
Kyle McNally 1151d3bc96 don't disable the graphics stack https://github.com/Sparticuz/chromium/issues/247 2024-04-15 11:31:15 -04:00
Kyle McNally e96aadfaf8 cleanup readme 2024-04-15 11:30:32 -04:00
Kyle McNally 636fa64bfa BREAKING!!! Require the user to specify the headless type 2024-04-15 11:30:16 -04:00
Kyle McNally 9f3b102e5e BREAKING!!!! Start using puppeteer.defaultArgs instead of just including them in this package
browser = await puppeteer.launch({
-      args: chromium.args,
+      args: puppeteer.defaultArgs({
+        args: chromium.args,
+      }),
2024-04-15 11:15:59 -04:00
16 changed files with 270 additions and 391 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.6 follow-redirects@1.15.9 --bin-links=false --fund=false --omit=optional --omit=dev --package-lock=false --save=false 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 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 a 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 an additional set of predefined arguments tailored to serverless usage.
## Install ## Install
@ -50,14 +50,6 @@ 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"
@ -65,10 +57,20 @@ 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: chromium.args, args: puppeteer.defaultArgs({
defaultViewport: chromium.defaultViewport, args: chromium.args,
headless: "shell",
}),
defaultViewport: {
deviceScaleFactor: 1,
hasTouch: false,
height: 1080,
isLandscape: true,
isMobile: false,
width: 1920,
},
executablePath: await chromium.executablePath(), executablePath: await chromium.executablePath(),
headless: chromium.headless, headless: "shell",
}); });
const page = await browser.newPage(); const page = await browser.newPage();
@ -92,7 +94,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: chromium.headless, headless: true,
}); });
const context = await browser.newContext(); const context = await browser.newContext();
@ -127,10 +129,13 @@ In this example, /opt/chromium contains all the brotli files
```javascript ```javascript
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
args: chromium.args, args: puppeteer.defaultArgs({
args: chromium.args,
headless: "shell",
}),
defaultViewport: chromium.defaultViewport, defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath("/opt/chromium"), executablePath: await chromium.executablePath("/opt/chromium"),
headless: chromium.headless, headless: "shell",
}); });
``` ```
@ -142,12 +147,15 @@ 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: chromium.args, args: puppeteer.defaultArgs({
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: chromium.headless, headless: "shell",
}); });
``` ```
@ -177,12 +185,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() : chromium.args, args: process.env.IS_LOCAL ? puppeteer.defaultArgs() : puppeteer.defaultArgs({args:chromium.args, process.env.IS_LOCAL ? false : "shell"}),
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 : chromium.headless, headless: process.env.IS_LOCAL ? false : "shell",
}); });
``` ```
@ -294,20 +302,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). | | `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. | | `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. |
| `setHeadlessMode` | `void` | Sets the headless mode to either `true` or `"shell"` | | `setGraphicsMode` | `void` | Sets the graphics mode to either `true` or `false` |
| `headless` | `true \| "shell"` | Returns `true` or `"shell"` depending on what version of chrome's headless you are running | | `graphics` | `boolean` | Returns a boolean depending on whether webgl is enabled or disabled |
| `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
@ -357,11 +365,13 @@ 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": "e610a8be5568f23c453b08928460aae3ae0b4b0a" "screenshot": "6b6bfde6d0cd0035255ac81eb4c6a8823fc74f02"
} }
}, },
{ {
"url": "https://get.webgl.org", "url": "https://get.webgl.org",
"expected": { "expected": {
"remove": "logo-container", "remove": "logo-container",
"screenshot": "ec6c79a571b4cb5727c6fc23f9da30de3868138c" "screenshot": "1e4d1ce70b48ca14c1cee2a8e5f458f6ea1b987d"
} }
} }
] ]

View File

@ -3,17 +3,19 @@ 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, context) => { exports.handler = async (event) => {
let browser = null; let browser = null;
try { try {
browser = await puppeteer.launch({ browser = await puppeteer.launch({
args: chromium.args, args: puppeteer.defaultArgs({
defaultViewport: chromium.defaultViewport, args: chromium.args,
headless: "shell",
}),
dumpio: true, dumpio: true,
executablePath: await chromium.executablePath(), executablePath: await chromium.executablePath(),
headless: chromium.headless, headless: "shell",
acceptInsecureCerts: true, ignoreHTTPSErrors: true,
}); });
console.log("Chromium version", await browser.version()); console.log("Chromium version", await browser.version());
@ -38,7 +40,7 @@ exports.handler = async (event, context) => {
document.getElementById(selector).remove(); document.getElementById(selector).remove();
}, job.expected.remove); }, job.expected.remove);
} }
const screenshot = Buffer.from(await page.screenshot()); const screenshot = 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=1343869 chromium_revision=1300313

Binary file not shown.

Binary file not shown.

View File

@ -1,12 +1,14 @@
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: chromium.args, args: puppeteer.defaultArgs({
defaultViewport: chromium.defaultViewport, args: chromium.args,
headless: "shell",
}),
executablePath: await chromium.executablePath(), executablePath: await chromium.executablePath(),
headless: chromium.headless, headless: "shell",
}); });
const page = await browser.newPage(); const page = await browser.newPage();
@ -20,5 +22,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,10 +4,12 @@ const chromium = require("@sparticuz/chromium");
const handler = async () => { const handler = async () => {
try { try {
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
args: chromium.args, args: puppeteer.defaultArgs({
defaultViewport: chromium.defaultViewport, args: chromium.args,
headless: "shell",
}),
executablePath: await chromium.executablePath(), executablePath: await chromium.executablePath(),
headless: chromium.headless, headless: "shell",
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
}); });

View File

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

51
package-lock.json generated
View File

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

View File

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

View File

@ -1,3 +1,4 @@
import { https } from "follow-redirects";
import { import {
access, access,
createWriteStream, createWriteStream,
@ -5,50 +6,18 @@ 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,
isValidUrl,
isRunningInAwsLambdaNode20, isRunningInAwsLambdaNode20,
isValidUrl,
} from "./helper"; } from "./helper";
import { inflate } from "./lambdafs";
/** Viewport taken from https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.viewport.md */ // Set up the environmental variables
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";
@ -88,208 +57,73 @@ 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: boolean = true; private static graphicsMode = 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 = [
"--disable-domain-reliability", // https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md#background-networking "--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-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
"--mute-audio", // https://source.chromium.org/search?q=lang:cpp+symbol:kMuteAudio&ss=chromium "--no-default-browser-check", // Disable the default browser check, do not prompt to set it as such
"--no-default-browser-check", // https://source.chromium.org/search?q=lang:cpp+symbol:kNoDefaultBrowserCheck&ss=chromium "--no-pings", // Don't send hyperlink auditing pings
"--no-pings", // https://source.chromium.org/search?q=lang:cpp+symbol:kNoPings&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
"--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", "site-per-process", // Disables OOPIF. https://www.chromium.org/Home/chromium-security/site-isolation
]; ];
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", // https://source.chromium.org/search?q=lang:cpp+symbol:kInProcessGPU&ss=chromium "--in-process-gpu", // Saves some memory by moving GPU process into a browser process thread
"--window-size=1920,1080", // https://source.chromium.org/search?q=lang:cpp+symbol:kWindowSize&ss=chromium "--window-size=1920,1080", // Sets the initial window size. Provided as string in the format "800,600".
]; ];
// https://chromium.googlesource.com/chromium/src/+/main/docs/gpu/swiftshader.md // https://chromium.googlesource.com/chromium/src/+/main/docs/gpu/swiftshader.md
// Blocked by https://github.com/Sparticuz/chromium/issues/247 // https://developer.chrome.com/blog/supercharge-web-ai-testing
//this.graphics // https://www.browserless.io/blog/2023/08/31/browserless-gpu-instances/
// ? graphicsFlags.push("--use-gl=angle", "--use-angle=swiftshader") this.graphics &&
// : graphicsFlags.push("--disable-webgl"); graphicsFlags.push(
graphicsFlags.push("--use-gl=angle", "--use-angle=swiftshader"); "--enable-gpu",
"--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", // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSetuidSandbox&ss=chromium "--disable-setuid-sandbox", // Lambda runs as root, so this is required to allow Chromium to run as root
"--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", // https://source.chromium.org/search?q=lang:cpp+symbol:kNoSandbox&ss=chromium "--no-sandbox", // Lambda runs as root, so this is required to allow Chromium to run as root
"--no-zygote", // https://source.chromium.org/search?q=lang:cpp+symbol:kNoZygote&ss=chromium "--no-zygote", // https://codereview.chromium.org/2384163002
];
const headlessFlags = [
this.headless === "shell" ? "--headless='shell'" : "--headless",
]; ];
return [ return [
...puppeteerFlags,
...chromiumFlags, ...chromiumFlags,
`--disable-features=${[ `--disable-features=${[...chromiumDisableFeatures].join(",")}`,
...puppeteerDisableFeatures, `--enable-features=${[...chromiumEnableFeatures].join(",")}`,
...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
@ -328,19 +162,19 @@ class Chromium {
// Extract the required files // Extract the required files
const promises = [ const promises = [
LambdaFS.inflate(`${input}/chromium.br`), inflate(`${input}/chromium.br`),
LambdaFS.inflate(`${input}/fonts.tar.br`), 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(LambdaFS.inflate(`${input}/swiftshader.tar.br`)); promises.push(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(LambdaFS.inflate(`${input}/al2.tar.br`)); promises.push(inflate(`${input}/al2.tar.br`));
} }
if (isRunningInAwsLambdaNode20()) { if (isRunningInAwsLambdaNode20()) {
promises.push(LambdaFS.inflate(`${input}/al2023.tar.br`)); promises.push(inflate(`${input}/al2023.tar.br`));
} }
// Await all extractions // Await all extractions
@ -350,33 +184,67 @@ class Chromium {
} }
/** /**
* Returns the headless mode. * Downloads or symlinks a custom font and returns its basename, patching the environment so that Chromium can find it.
* "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"
*/ */
public static get headless() { static font(input: string): Promise<string> {
return this.headlessMode; if (process.env["HOME"] === undefined) {
} 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);
});
});
});
}
});
} }
/** /**
@ -396,7 +264,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 Error( throw new TypeError(
`Graphics mode must be a boolean, you entered '${value}'` `Graphics mode must be a boolean, you entered '${value}'`
); );
} }

View File

@ -1,75 +1,73 @@
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 { extract } from "tar-fs";
import { createBrotliDecompress, createUnzip } from "node:zlib"; import { createBrotliDecompress, createUnzip } from "node:zlib";
import { extract } from "tar-fs";
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> => {
static inflate(filePath: string): Promise<string> { const output = filePath.includes("swiftshader")
const output = filePath.includes("swiftshader") ? tmpdir()
? tmpdir() : join(
: join( tmpdir(),
tmpdir(), basename(filePath).replace(
basename(filePath).replace( /\.(?:t(?:ar(?:\.(?:br|gz))?|br|gz)|br|gz)$/i,
/[.](?: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 {
let source = createReadStream(filePath, { highWaterMark: 2 ** 23 }); if (existsSync(output) === true) {
let target = null; return resolve(output);
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 });
} }
}
source.once("error", (error: Error) => { const source = createReadStream(filePath, { highWaterMark: 2 ** 23 });
return reject(error); let target = null;
});
target.once("error", (error: Error) => { if (/\.t(?:ar(?:\.(?:br|gz))?|br|gz)$/i.test(filePath) === true) {
return reject(error); target = extract(output);
});
target.once("close", () => { target.once("finish", () => {
return resolve(output); return resolve(output);
}); });
} else {
target = createWriteStream(output, { mode: 0o700 });
}
if (/(?:br|gz)$/i.test(filePath) === true) { source.once("error", (error: Error) => {
source return reject(error);
.pipe(
/br$/i.test(filePath)
? createBrotliDecompress({ chunkSize: 2 ** 21 })
: createUnzip({ chunkSize: 2 ** 21 })
)
.pipe(target);
} else {
source.pipe(target);
}
}); });
}
}
export default LambdaFS; 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;