diff --git a/README.md b/README.md index a5af33f..15e55d5 100644 --- a/README.md +++ b/README.md @@ -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 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 @@ -50,14 +50,6 @@ 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" @@ -65,10 +57,20 @@ await chromium.font( test("Check the page title of example.com", async (t) => { const browser = await puppeteer.launch({ - args: chromium.args, - defaultViewport: chromium.defaultViewport, + args: puppeteer.defaultArgs({ + args: chromium.args, + headless: "shell", + }), + defaultViewport: { + deviceScaleFactor: 1, + hasTouch: false, + height: 1080, + isLandscape: true, + isMobile: false, + width: 1920, + }, executablePath: await chromium.executablePath(), - headless: chromium.headless, + headless: "shell", }); const page = await browser.newPage(); @@ -92,7 +94,7 @@ test("Check the page title of example.com", async (t) => { const browser = await playwright.launch({ args: chromium.args, executablePath: await chromium.executablePath(), - headless: chromium.headless, + headless: true, }); const context = await browser.newContext(); @@ -127,10 +129,13 @@ In this example, /opt/chromium contains all the brotli files ```javascript const browser = await puppeteer.launch({ - args: chromium.args, + args: puppeteer.defaultArgs({ + args: chromium.args, + headless: "shell", + }), defaultViewport: chromium.defaultViewport, 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 const browser = await puppeteer.launch({ - args: chromium.args, + args: puppeteer.defaultArgs({ + args: chromium.args, + headless: "shell", + }), defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath( "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 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, executablePath: process.env.IS_LOCAL ? "/tmp/localChromium/chromium/linux-1122391/chrome-linux/chrome" : 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 +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` | Provisions a custom font and returns its basename. | -| `args` | `Array` | 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` | 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 | +| Method / Property | Returns | Description | +| ----------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `font(url)` | `Promise` | Provisions a custom font and returns its basename. | +| `args` | `Array` | 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` | 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 | ## Compiling @@ -357,11 +365,13 @@ exports.handler = async (event, context, callback) => { try { - browser = await chromium.puppeteer.launch({ + browser = await puppeteer.launch({ - args: chromium.args, +- args: chromium.args, ++ args: puppeteer.defaultArgs({ args: chromium.args, headless: "shell" }), defaultViewport: chromium.defaultViewport, - executablePath: await chromium.executablePath, + executablePath: await chromium.executablePath(), - headless: chromium.headless, +- headless: chromium.headless, ++ headless: "shell", ignoreHTTPSErrors: true, }); diff --git a/_/amazon/events/example.com.json b/_/amazon/events/example.com.json index dd37dc8..eaea77d 100644 --- a/_/amazon/events/example.com.json +++ b/_/amazon/events/example.com.json @@ -3,14 +3,14 @@ "url": "https://example.com", "expected": { "title": "Example Domain", - "screenshot": "e610a8be5568f23c453b08928460aae3ae0b4b0a" + "screenshot": "6b6bfde6d0cd0035255ac81eb4c6a8823fc74f02" } }, { "url": "https://get.webgl.org", "expected": { "remove": "logo-container", - "screenshot": "ec6c79a571b4cb5727c6fc23f9da30de3868138c" + "screenshot": "1e4d1ce70b48ca14c1cee2a8e5f458f6ea1b987d" } } ] diff --git a/_/amazon/handlers/index.js b/_/amazon/handlers/index.js index 9c12578..a22fc30 100644 --- a/_/amazon/handlers/index.js +++ b/_/amazon/handlers/index.js @@ -3,16 +3,18 @@ const { createHash } = require("node:crypto"); const puppeteer = require("puppeteer-core"); const chromium = require("@sparticuz/chromium"); -exports.handler = async (event, context) => { +exports.handler = async (event) => { let browser = null; try { browser = await puppeteer.launch({ - args: chromium.args, - defaultViewport: chromium.defaultViewport, + args: puppeteer.defaultArgs({ + args: chromium.args, + headless: "shell", + }), dumpio: true, executablePath: await chromium.executablePath(), - headless: chromium.headless, + headless: "shell", ignoreHTTPSErrors: true, }); diff --git a/examples/aws-sam/functions/exampleFunction/app.mjs b/examples/aws-sam/functions/exampleFunction/app.mjs index de94ba7..ec5eeb2 100644 --- a/examples/aws-sam/functions/exampleFunction/app.mjs +++ b/examples/aws-sam/functions/exampleFunction/app.mjs @@ -1,12 +1,14 @@ -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: chromium.args, - defaultViewport: chromium.defaultViewport, + args: puppeteer.defaultArgs({ + args: chromium.args, + headless: "shell", + }), executablePath: await chromium.executablePath(), - headless: chromium.headless, + headless: "shell", }); const page = await browser.newPage(); @@ -20,5 +22,5 @@ export const lambdaHandler = async (event, context) => { await browser.close(); - return { result: 'success', browserVersion, pageTitle }; -} + return { result: "success", browserVersion, pageTitle }; +}; diff --git a/examples/production-dependency/index.js b/examples/production-dependency/index.js index 8f2ea05..bdfc931 100644 --- a/examples/production-dependency/index.js +++ b/examples/production-dependency/index.js @@ -4,10 +4,12 @@ const chromium = require("@sparticuz/chromium"); const handler = async () => { try { const browser = await puppeteer.launch({ - args: chromium.args, - defaultViewport: chromium.defaultViewport, + args: puppeteer.defaultArgs({ + args: chromium.args, + headless: "shell", + }), executablePath: await chromium.executablePath(), - headless: chromium.headless, + headless: "shell", ignoreHTTPSErrors: true, }); diff --git a/examples/remote-min-binary/index.js b/examples/remote-min-binary/index.js index c5ca961..8c2e07c 100644 --- a/examples/remote-min-binary/index.js +++ b/examples/remote-min-binary/index.js @@ -4,8 +4,10 @@ const chromium = require("@sparticuz/chromium-min"); const handler = async () => { try { const browser = await puppeteer.launch({ - args: chromium.args, - defaultViewport: chromium.defaultViewport, + args: puppeteer.defaultArgs({ + args: chromium.args, + headless: "shell", + }), executablePath: await chromium.executablePath( "https://github.com/Sparticuz/chromium/releases/download/v110.0.1/chromium-v110.0.1-pack.tar" ), diff --git a/examples/serverless-with-lambda-layer/index.js b/examples/serverless-with-lambda-layer/index.js index 039cdec..c7e9ad3 100644 --- a/examples/serverless-with-lambda-layer/index.js +++ b/examples/serverless-with-lambda-layer/index.js @@ -5,10 +5,12 @@ module.exports = { handler: async () => { try { const browser = await puppeteer.launch({ - args: chromium.args, - defaultViewport: chromium.defaultViewport, + args: puppeteer.defaultArgs({ + args: chromium.args, + headless: "shell", + }), executablePath: await chromium.executablePath(), - headless: chromium.headless, + headless: "shell", ignoreHTTPSErrors: true, }); diff --git a/package-lock.json b/package-lock.json index b2be086..6d7832b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@tsconfig/node20": "^20.1.4", "@tsconfig/strictest": "^2.0.5", "@types/follow-redirects": "^1.14.4", - "@types/node": "^20.14.10", + "@types/node": "^20.14.11", "@types/tar-fs": "^2.0.4", "clean-modules": "^3.0.5", "typescript": "^5.5.3" @@ -47,9 +47,9 @@ } }, "node_modules/@types/node": { - "version": "20.14.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", - "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", "dev": true, "dependencies": { "undici-types": "~5.26.4" diff --git a/package.json b/package.json index b2382a1..1f5b6b0 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@tsconfig/node20": "^20.1.4", "@tsconfig/strictest": "^2.0.5", "@types/follow-redirects": "^1.14.4", - "@types/node": "^20.14.10", + "@types/node": "^20.14.11", "@types/tar-fs": "^2.0.4", "clean-modules": "^3.0.5", "typescript": "^5.5.3" diff --git a/source/helper.ts b/source/helper.ts index cc03c92..7bfe4ec 100644 --- a/source/helper.ts +++ b/source/helper.ts @@ -1,18 +1,16 @@ -import { unlink } from "node:fs"; import { https } from "follow-redirects"; +import { unlink } from "node:fs"; import { tmpdir } from "node:os"; 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; } export const isValidUrl = (input: string) => { try { return !!new URL(input); - } catch (err) { + } catch { return false; } }; @@ -57,20 +55,20 @@ export const isRunningInAwsLambdaNode20 = () => { export const downloadAndExtract = async (url: string) => new Promise((resolve, reject) => { - const getOptions = parse(url) as FollowRedirOptions; + const getOptions = new URL(url) as FollowRedirOptions; getOptions.maxBodyLength = 60 * 1024 * 1024; // 60mb - const destDir = `${tmpdir()}/chromium-pack`; - const extractObj = extract(destDir); + const destinationDirectory = `${tmpdir()}/chromium-pack`; + const extractObject = extract(destinationDirectory); https .get(url, (response) => { - response.pipe(extractObj); - extractObj.on("finish", () => { - resolve(destDir); + response.pipe(extractObject); + extractObject.on("finish", () => { + resolve(destinationDirectory); }); }) - .on("error", (err) => { - unlink(destDir, (_) => { - reject(err); + .on("error", (error) => { + unlink(destinationDirectory, () => { + reject(error); }); }); }); diff --git a/source/index.ts b/source/index.ts index ef38113..3ef1dfe 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,3 +1,4 @@ +import { https } from "follow-redirects"; import { access, createWriteStream, @@ -5,50 +6,18 @@ 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, - isValidUrl, isRunningInAwsLambdaNode20, + isValidUrl, } from "./helper"; +import { inflate } from "./lambdafs"; -/** 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; -} - +// Set up the environmental variables if (isRunningInAwsLambda()) { if (process.env["FONTCONFIG_PATH"] === undefined) { process.env["FONTCONFIG_PATH"] = "/tmp/fonts"; @@ -88,208 +57,73 @@ 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: 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 { - 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); - }); - }); - }); - } - }); - } + private static graphicsMode = true; /** * 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 = [ - "--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-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 - "--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 + "--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 "--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", + "site-per-process", // Disables OOPIF. https://www.chromium.org/Home/chromium-security/site-isolation ]; 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", // 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 + "--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". ]; - // https://chromium.googlesource.com/chromium/src/+/main/docs/gpu/swiftshader.md - // 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"); + // 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" + ); const insecureFlags = [ "--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-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-zygote", // https://source.chromium.org/search?q=lang:cpp+symbol:kNoZygote&ss=chromium - ]; - - const headlessFlags = [ - this.headless === "shell" ? "--headless='shell'" : "--headless", + "--no-sandbox", // Lambda runs as root, so this is required to allow Chromium to run as root + "--no-zygote", // https://codereview.chromium.org/2384163002 ]; return [ - ...puppeteerFlags, ...chromiumFlags, - `--disable-features=${[ - ...puppeteerDisableFeatures, - ...chromiumDisableFeatures, - ].join(",")}`, - `--enable-features=${[ - ...puppeteerEnableFeatures, - ...chromiumEnableFeatures, - ].join(",")}`, + `--disable-features=${[...chromiumDisableFeatures].join(",")}`, + `--enable-features=${[...chromiumEnableFeatures].join(",")}`, ...graphicsFlags, ...insecureFlags, - ...headlessFlags, ]; } - /** - * Returns sensible default viewport settings for serverless environments. - */ - static get defaultViewport(): Required { - 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 @@ -328,19 +162,19 @@ class Chromium { // Extract the required files const promises = [ - LambdaFS.inflate(`${input}/chromium.br`), - LambdaFS.inflate(`${input}/fonts.tar.br`), + inflate(`${input}/chromium.br`), + inflate(`${input}/fonts.tar.br`), ]; if (this.graphics) { // Only inflate graphics stack if needed - promises.push(LambdaFS.inflate(`${input}/swiftshader.tar.br`)); + promises.push(inflate(`${input}/swiftshader.tar.br`)); } if (isRunningInAwsLambda()) { // 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()) { - promises.push(LambdaFS.inflate(`${input}/al2023.tar.br`)); + promises.push(inflate(`${input}/al2023.tar.br`)); } // Await all extractions @@ -350,33 +184,67 @@ class Chromium { } /** - * 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" + * Downloads or symlinks a custom font and returns its basename, patching the environment so that Chromium can find it. */ - 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}'` - ); + static font(input: string): Promise { + if (process.env["HOME"] === undefined) { + process.env["HOME"] = "/tmp"; } - 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) { if (typeof value !== "boolean") { - throw new Error( + throw new TypeError( `Graphics mode must be a boolean, you entered '${value}'` ); } diff --git a/source/lambdafs.ts b/source/lambdafs.ts index b032fa2..5937733 100644 --- a/source/lambdafs.ts +++ b/source/lambdafs.ts @@ -1,75 +1,73 @@ import { createReadStream, createWriteStream, existsSync } from "node:fs"; import { tmpdir } from "node:os"; import { basename, join } from "node:path"; -import { extract } from "tar-fs"; 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. - * - * @param filePath Path of the file to decompress. - */ - static inflate(filePath: string): Promise { - const output = filePath.includes("swiftshader") - ? tmpdir() - : join( - tmpdir(), - basename(filePath).replace( - /[.](?:t(?:ar(?:[.](?:br|gz))?|br|gz)|br|gz)$/i, - "" - ) - ); +/** + * 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 => { + 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); - } - } else { - if (existsSync(output) === true) { - return resolve(output); - } + return new Promise((resolve, reject) => { + if (filePath.includes("swiftshader")) { + if (existsSync(`${output}/libGLESv2.so`)) { + 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 }); + } else { + if (existsSync(output) === true) { + return resolve(output); } + } - source.once("error", (error: Error) => { - return reject(error); - }); + const source = createReadStream(filePath, { highWaterMark: 2 ** 23 }); + let target = null; - target.once("error", (error: Error) => { - return reject(error); - }); + if (/\.t(?:ar(?:\.(?:br|gz))?|br|gz)$/i.test(filePath) === true) { + target = extract(output); - target.once("close", () => { + target.once("finish", () => { return resolve(output); }); + } else { + target = createWriteStream(output, { mode: 0o700 }); + } - 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); - } + source.once("error", (error: Error) => { + return reject(error); }); - } -} -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;