├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md └── workflows │ └── aws.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── Makefile ├── README.md ├── _ ├── amazon │ ├── code │ │ └── .gitignore │ ├── events │ │ └── example.com.json │ ├── handlers │ │ └── index.js │ └── template.yml └── ansible │ ├── Makefile │ ├── README.md │ ├── ansible.cfg │ ├── inventory.ini │ └── plays │ ├── .gclient │ └── chromium.yml ├── bin ├── aws.tar.br ├── chromium.br └── swiftshader.tar.br ├── incrementVersion ├── package.json ├── source ├── hooks │ ├── adblock.ts │ ├── agent.ts │ ├── chrome.ts │ ├── languages.ts │ ├── permissions.ts │ ├── timezone.ts │ ├── webdriver.ts │ └── window.ts ├── index.ts ├── lambdafs.ts └── puppeteer │ └── lib │ ├── Browser.ts │ ├── BrowserContext.ts │ ├── ElementHandle.ts │ ├── Frame.ts │ └── Page.ts ├── tsconfig.json └── typings └── chrome-aws-lambda.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.json] 14 | indent_size = 2 15 | indent_style = space 16 | 17 | [Makefile] 18 | indent_size = 4 19 | indent_style = tab 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github:Sparticuz 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Standard Bug Report 4 | title: "[BUG]" 5 | labels: bug 6 | --- 7 | 8 | 12 | 13 | ## Environment 14 | * `chrome-aws-lambda` Version: 15 | * `puppeteer` / `puppeteer-core` Version: 16 | * OS: 17 | * Node.js Version: 18 | * Lambda / GCF Runtime: 19 | 20 | ## Expected Behavior 21 | 22 | 23 | 24 | ## Current Behavior 25 | 26 | 27 | 28 | ## Steps to Reproduce 29 | 30 | 31 | 32 | 66 | 67 | ## Possible Solution 68 | 69 | 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an Idea or Improvement 4 | title: "[REQUEST]" 5 | labels: enhancement 6 | --- 7 | 8 | ## What would you like to have implemented? 9 | 10 | 11 | 12 | ## Why would it be useful? 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/aws.yml: -------------------------------------------------------------------------------- 1 | name: AWS Lambda CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: Build Lambda Layer 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 16.x 21 | 22 | - name: Install Packages 23 | run: npm install 24 | 25 | - name: Create Lambda Layer 26 | run: make chrome_aws_lambda.zip 27 | 28 | - name: Upload Layer Artifact 29 | uses: actions/upload-artifact@v3 30 | with: 31 | name: chrome_aws_lambda 32 | path: chrome_aws_lambda.zip 33 | 34 | execute: 35 | name: Lambda (Node ${{ matrix.version }}.x) 36 | needs: build 37 | runs-on: ubuntu-latest 38 | strategy: 39 | matrix: 40 | event: 41 | - example.com 42 | version: 43 | - 14 44 | - 16 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v3 48 | 49 | - name: Setup Python 50 | uses: actions/setup-python@v3 51 | with: 52 | python-version: '3.x' 53 | 54 | - name: Setup AWS SAM CLI 55 | uses: aws-actions/setup-sam@v2 56 | 57 | - name: Download Layer Artifact 58 | uses: actions/download-artifact@v3 59 | with: 60 | name: chrome_aws_lambda 61 | 62 | - name: Provision Layer 63 | run: unzip chrome_aws_lambda.zip -d _/amazon/code 64 | 65 | - name: Invoke Lambda on SAM 66 | run: sam local invoke --template _/amazon/template.yml --event _/amazon/events/${{ matrix.event }}.json node${{ matrix.version }} 2>&1 | (grep 'Error' && exit 1 || exit 0) 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .fonts 2 | .idea 3 | *.log 4 | *.pem 5 | *.pem.pub 6 | *.zip 7 | bin/chromium-*.br 8 | build 9 | node_modules 10 | nodejs 11 | package-lock.json 12 | _/amazon/samconfig.toml 13 | _/amazon/.aws-sam 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | _ 2 | .fonts 3 | .idea 4 | *.zip 5 | Dockerfile 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alix Axel 4 | Copyright (c) 2022 Kyle McNally 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean 2 | 3 | clean: 4 | rm -rf chrome_aws_lambda.zip _/amazon/code/nodejs 5 | 6 | pretest: 7 | unzip chrome_aws_lambda.zip -d _/amazon/code 8 | 9 | test: 10 | sam local invoke --template _/amazon/template.yml --event _/amazon/events/example.com.json node16 11 | 12 | .fonts.zip: 13 | zip -9 --filesync --move --recurse-paths .fonts.zip .fonts/ 14 | 15 | %.zip: 16 | npm install --fund=false --package-lock=false 17 | mkdir -p nodejs 18 | npm install --prefix nodejs/ tar-fs@2.1.1 puppeteer-core@17.1.3 --bin-links=false --fund=false --omit=optional --omit=dev --package-lock=false --save=false 19 | npm pack 20 | mkdir -p nodejs/node_modules/@sparticuz/chrome-aws-lambda/ 21 | tar --directory nodejs/node_modules/@sparticuz/chrome-aws-lambda/ --extract --file sparticuz-chrome-aws-lambda-*.tgz --strip-components=1 22 | npx clean-modules --directory nodejs --include "**/*.d.ts" "**/@types/**" "**/*.@(yaml|yml)" --yes 23 | rm sparticuz-chrome-aws-lambda-*.tgz 24 | mkdir -p $(dir $@) 25 | zip -9 --filesync --move --recurse-paths $@ nodejs 26 | 27 | .DEFAULT_GOAL := chrome_aws_lambda.zip 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation Notice!!! 2 | ## This package has been deprecated in favor of [@sparticuz/chromium](https://www.npmjs.com/package/@sparticuz/chromium). This new package doesn't need to be updated with each `puppeteer` version, and should have better compatibility with other test runners like `playwright`. Please see the migration guide [here](https://github.com/Sparticuz/chromium#migration-from-chrome-aws-lambda). Please migrate to this new package. 3 | 4 | # chrome-aws-lambda 5 | 6 | [![@sparticuz/chrome-aws-lambda](https://img.shields.io/npm/v/@sparticuz/chrome-aws-lambda.svg?style=for-the-badge)](https://www.npmjs.com/package/@sparticuz/chrome-aws-lambda) 7 | [![TypeScript](https://img.shields.io/npm/types/chrome-aws-lambda?style=for-the-badge)](https://www.typescriptlang.org/dt/search?search=chrome-aws-lambda) 8 | [![Chromium](https://img.shields.io/badge/chromium-48_MB-brightgreen.svg?style=for-the-badge)](bin/) 9 | [![Donate](https://img.shields.io/badge/donate-paypal-orange.svg?style=for-the-badge)](https://paypal.me/sparticuz) 10 | 11 | Chromium Binary for AWS Lambda and Google Cloud Functions 12 | 13 | ### Difference from alixaxel/chrome-aws-lambda 14 | 15 | This fork was born out of [alixaxel/chrome-aws-lambda#264](https://github.com/alixaxel/chrome-aws-lambda/pull/264). 16 | The biggest difference, besides the chromium version, is the inclusion of some code from https://github.com/alixaxel/lambdafs, 17 | as well as dropping that as a dependency. Due to some changes in WebGL, the files in bin/swiftshader.tar.br need to 18 | be extracted to `/tmp` instead of `/tmp/swiftshader`. This necessitated changes in lambdafs. 19 | 20 | ## Install 21 | 22 | ```shell 23 | npm install @sparticuz/chrome-aws-lambda --save-prod 24 | ``` 25 | 26 | This will ship with appropriate binary for the latest stable release of [`puppeteer`](https://github.com/GoogleChrome/puppeteer) (usually updated within a few days). 27 | 28 | You also need to install the corresponding version of `puppeteer-core` (or `puppeteer`): 29 | 30 | ```shell 31 | npm install puppeteer-core --save-prod 32 | ``` 33 | 34 | If you wish to install an older version of Chromium, take a look at [Versioning](https://github.com/Sparticuz/chrome-aws-lambda#versioning). 35 | 36 | ## Usage 37 | 38 | This package works with all the currently supported AWS Lambda Node.js runtimes out of the box. 39 | 40 | ```javascript 41 | const chromium = require('@sparticuz/chrome-aws-lambda'); 42 | 43 | exports.handler = async (event, context, callback) => { 44 | let result = null; 45 | let browser = null; 46 | 47 | try { 48 | browser = await chromium.puppeteer.launch({ 49 | args: chromium.args, 50 | defaultViewport: chromium.defaultViewport, 51 | executablePath: await chromium.executablePath, 52 | headless: chromium.headless, 53 | ignoreHTTPSErrors: true, 54 | }); 55 | 56 | let page = await browser.newPage(); 57 | 58 | await page.goto(event.url || 'https://example.com'); 59 | 60 | result = await page.title(); 61 | } catch (error) { 62 | return callback(error); 63 | } finally { 64 | if (browser !== null) { 65 | await browser.close(); 66 | } 67 | } 68 | 69 | return callback(null, result); 70 | }; 71 | ``` 72 | 73 | ### Usage with Playwright 74 | 75 | ```javascript 76 | const chromium = require('@sparticuz/chrome-aws-lambda'); 77 | const playwright = require('playwright-core'); 78 | 79 | (async () => { 80 | const browser = await playwright.chromium.launch({ 81 | args: chromium.args, 82 | executablePath: await chromium.executablePath, 83 | headless: chromium.headless, 84 | }); 85 | 86 | // ... 87 | 88 | await browser.close(); 89 | })(); 90 | ``` 91 | 92 | You should allocate at least 512 MB of RAM to your Lambda, however 1600 MB (or more) is recommended. 93 | 94 | ### Running Locally 95 | 96 | Please refer to the [Local Development Wiki page](https://github.com/alixaxel/chrome-aws-lambda/wiki/HOWTO:-Local-Development) for instructions and troubleshooting. 97 | 98 | ## API 99 | 100 | | Method / Property | Returns | Description | 101 | | ----------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | 102 | | `font(url)` | `{?Promise}` | Provisions a custom font and returns its basename. | 103 | | `args` | `{!Array}` | Provides a list of recommended additional [Chromium flags](https://github.com/GoogleChrome/chrome-launcher/blob/master/docs/chrome-flags-for-tools.md). | 104 | | `defaultViewport` | `{!Object}` | Returns more sensible default viewport settings. | 105 | | `executablePath` | `{?Promise}` | Returns the path the Chromium binary was extracted to. | 106 | | `headless` | `{!boolean}` | Returns `true` if we are running on AWS Lambda or GCF. | 107 | | `puppeteer` | `{!Object}` | Overloads `puppeteer` and returns the resolved package. | 108 | 109 | ## Fonts 110 | 111 | The Amazon Linux 2 AWS Lambda runtime is no longer provisioned with any font faces. 112 | 113 | Because of this, this package ships with [Open Sans](https://fonts.google.com/specimen/Open+Sans), which supports the following scripts: 114 | 115 | * Latin 116 | * Greek 117 | * Cyrillic 118 | 119 | To provision additional fonts, simply call the `font()` method with an absolute path or URL: 120 | 121 | ```typescript 122 | await chromium.font('/var/task/fonts/NotoColorEmoji.ttf'); 123 | // or 124 | await chromium.font('https://raw.githack.com/googlei18n/noto-emoji/master/fonts/NotoColorEmoji.ttf'); 125 | ``` 126 | 127 | > `Noto Color Emoji` (or similar) is needed if you want to [render emojis](https://getemoji.com/). 128 | 129 | > For URLs, it's recommended that you use a CDN, like [raw.githack.com](https://raw.githack.com/) or [gitcdn.xyz](https://gitcdn.xyz/). 130 | 131 | This method should be invoked _before_ launching Chromium. 132 | 133 | > On non-serverless environments, the `font()` method is a no-op to avoid polluting the user space. 134 | 135 | --- 136 | 137 | Alternatively, it's also possible to provision fonts via AWS Lambda Layers. 138 | 139 | Simply create a directory named `.fonts` and place any font faces you want there: 140 | 141 | ``` 142 | .fonts 143 | ├── NotoColorEmoji.ttf 144 | └── Roboto.ttf 145 | ``` 146 | 147 | Afterwards, you just need to ZIP the directory and upload it as a AWS Lambda Layer: 148 | 149 | ```shell 150 | zip -9 --filesync --move --recurse-paths .fonts.zip .fonts/ 151 | ``` 152 | 153 | ## Overloading 154 | 155 | Since version `8.0.0`, it's possible to [overload puppeteer](/typings/chrome-aws-lambda.d.ts) with the following convenient API: 156 | 157 | ```typescript 158 | interface Browser { 159 | defaultPage(...hooks: ((page: Page) => Promise)[]) 160 | newPage(...hooks: ((page: Page) => Promise)[]) 161 | } 162 | 163 | interface BrowserContext { 164 | defaultPage(...hooks: ((page: Page) => Promise)[]) 165 | newPage(...hooks: ((page: Page) => Promise)[]) 166 | } 167 | 168 | interface Page { 169 | block(patterns: string[]) 170 | clear(selector: string) 171 | clickAndWaitForNavigation(selector: string, options?: WaitForOptions) 172 | clickAndWaitForRequest(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions) 173 | clickAndWaitForRequest(selector: string, predicate: ((request: HTTPRequest) => boolean | Promise), options?: WaitTimeoutOptions) 174 | clickAndWaitForResponse(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions) 175 | clickAndWaitForResponse(selector: string, predicate: ((request: HTTPResponse) => boolean | Promise), options?: WaitTimeoutOptions) 176 | count(selector: string) 177 | exists(selector: string) 178 | fillFormByLabel(selector: string, data: Record) 179 | fillFormByName(selector: string, data: Record) 180 | fillFormBySelector(selector: string, data: Record) 181 | fillFormByXPath(selector: string, data: Record) 182 | number(selector: string, decimal?: string, property?: string) 183 | selectByLabel(selector: string, ...values: string[]) 184 | string(selector: string, property?: string) 185 | waitForInflightRequests(requests?: number, alpha: number, omega: number, options?: WaitTimeoutOptions) 186 | waitForText(predicate: string, options?: WaitTimeoutOptions) 187 | waitUntilVisible(selector: string, options?: WaitTimeoutOptions) 188 | waitWhileVisible(selector: string, options?: WaitTimeoutOptions) 189 | withTracing(options: TracingOptions, callback: (page: Page) => Promise) 190 | } 191 | 192 | interface Frame { 193 | clear(selector: string) 194 | clickAndWaitForNavigation(selector: string, options?: WaitForOptions) 195 | clickAndWaitForRequest(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions) 196 | clickAndWaitForRequest(selector: string, predicate: ((request: HTTPRequest) => boolean | Promise), options?: WaitTimeoutOptions) 197 | clickAndWaitForResponse(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions) 198 | clickAndWaitForResponse(selector: string, predicate: ((request: HTTPResponse) => boolean | Promise), options?: WaitTimeoutOptions) 199 | count(selector: string) 200 | exists(selector: string) 201 | fillFormByLabel(selector: string, data: Record) 202 | fillFormByName(selector: string, data: Record) 203 | fillFormBySelector(selector: string, data: Record) 204 | fillFormByXPath(selector: string, data: Record) 205 | number(selector: string, decimal?: string, property?: string) 206 | selectByLabel(selector: string, ...values: string[]) 207 | string(selector: string, property?: string) 208 | waitForText(predicate: string, options?: WaitTimeoutOptions) 209 | waitUntilVisible(selector: string, options?: WaitTimeoutOptions) 210 | waitWhileVisible(selector: string, options?: WaitTimeoutOptions) 211 | } 212 | 213 | interface ElementHandle { 214 | clear() 215 | clickAndWaitForNavigation(options?: WaitForOptions) 216 | clickAndWaitForRequest(predicate: string | RegExp, options?: WaitTimeoutOptions) 217 | clickAndWaitForRequest(predicate: ((request: HTTPRequest) => boolean | Promise), options?: WaitTimeoutOptions) 218 | clickAndWaitForResponse(predicate: string | RegExp, options?: WaitTimeoutOptions) 219 | clickAndWaitForResponse(predicate: ((request: HTTPResponse) => boolean | Promise), options?: WaitTimeoutOptions) 220 | fillFormByLabel(data: Record) 221 | fillFormByName(data: Record) 222 | fillFormBySelector(data: Record) 223 | fillFormByXPath(data: Record) 224 | getInnerHTML() 225 | getInnerText() 226 | number(decimal?: string, property?: string) 227 | selectByLabel(...values: string[]) 228 | string(property?: string) 229 | } 230 | ``` 231 | 232 | To enable this behavior, simply call the `puppeteer` property exposed by this package. 233 | 234 | > Refer to the [TypeScript typings](/typings/chrome-aws-lambda.d.ts) for general documentation. 235 | 236 | ## Page Hooks 237 | 238 | When overloaded, you can specify a list of hooks to automatically apply to pages. 239 | 240 | For instance, to remove the `Headless` substring from the user agent: 241 | 242 | ```typescript 243 | async function replaceUserAgent(page: Page): Promise { 244 | let value = await page.browser().userAgent(); 245 | 246 | if (value.includes('Headless') === true) { 247 | await page.setUserAgent(value.replace('Headless', '')); 248 | } 249 | 250 | return page; 251 | } 252 | ``` 253 | 254 | And then simply pass that page hook to `defaultPage()` or `newPage()`: 255 | 256 | ```typescript 257 | let page = await browser.defaultPage(replaceUserAgent); 258 | ``` 259 | 260 | > Additional bundled page hooks can be found on [`/build/hooks`](/source/hooks). 261 | 262 | ## Versioning 263 | 264 | This package is versioned based on the underlying `puppeteer` minor version: 265 | 266 | | `puppeteer` Version | `chrome-aws-lambda` Version | Chromium Revision | 267 | | ------------------- | --------------------------------------------- | ------------------------------------------------------- | 268 | | `17.1.*` | `npm i @sparticuz/chrome-aws-lambda@~17.1.1` | [`1036745`](https://crrev.com/1036745) (`106.0.5249.0`) | 269 | | `16.1.*` | `npm i @sparticuz/chrome-aws-lambda@~16.1.0` | [`1022525`](https://crrev.com/1011831) (`105.0.5173.0`) | 270 | | `15.5.*` | `npm i @sparticuz/chrome-aws-lambda@~15.5.0` | [`1022525`](https://crrev.com/1011831) (`105.0.5173.0`) | 271 | | `14.4.*` | `npm i @sparticuz/chrome-aws-lambda@~14.4.1` | [`1002410`](https://crrev.com/1002410) (`103.0.5058.0`) | 272 | | `14.3.*` | `npm i @sparticuz/chrome-aws-lambda@~14.3.0` | [`1002410`](https://crrev.com/1002410) (`103.0.5058.0`) | 273 | | `14.2.*` | `npm i @sparticuz/chrome-aws-lambda@~14.2.0` | [`1002410`](https://crrev.com/1002410) (`103.0.5058.0`) | 274 | | `14.1.*` | `npm i @sparticuz/chrome-aws-lambda@~14.1.1` | [`991974`](https://crrev.com/991974) (`102.0.5002.0`) | 275 | | `10.1.*` | `npm i chrome-aws-lambda@~10.1.0` | [`884014`](https://crrev.com/884014) (`92.0.4512.0`) | 276 | | `10.0.*` | `npm i chrome-aws-lambda@~10.0.0` | [`884014`](https://crrev.com/884014) (`92.0.4512.0`) | 277 | | `9.1.*` | `npm i chrome-aws-lambda@~9.1.0` | [`869685`](https://crrev.com/869685) (`91.0.4469.0`) | 278 | | `9.0.*` | `npm i chrome-aws-lambda@~9.0.0` | [`869685`](https://crrev.com/869685) (`91.0.4469.0`) | 279 | | `8.0.*` | `npm i chrome-aws-lambda@~8.0.2` | [`856583`](https://crrev.com/856583) (`90.0.4427.0`) | 280 | | `7.0.*` | `npm i chrome-aws-lambda@~7.0.0` | [`848005`](https://crrev.com/848005) (`90.0.4403.0`) | 281 | | `6.0.*` | `npm i chrome-aws-lambda@~6.0.0` | [`843427`](https://crrev.com/843427) (`89.0.4389.0`) | 282 | | `5.5.*` | `npm i chrome-aws-lambda@~5.5.0` | [`818858`](https://crrev.com/818858) (`88.0.4298.0`) | 283 | | `5.4.*` | `npm i chrome-aws-lambda@~5.4.0` | [`809590`](https://crrev.com/809590) (`87.0.4272.0`) | 284 | | `5.3.*` | `npm i chrome-aws-lambda@~5.3.1` | [`800071`](https://crrev.com/800071) (`86.0.4240.0`) | 285 | | `5.2.*` | `npm i chrome-aws-lambda@~5.2.1` | [`782078`](https://crrev.com/782078) (`85.0.4182.0`) | 286 | | `5.1.*` | `npm i chrome-aws-lambda@~5.1.0` | [`768783`](https://crrev.com/768783) (`84.0.4147.0`) | 287 | | `5.0.*` | `npm i chrome-aws-lambda@~5.0.0` | [`756035`](https://crrev.com/756035) (`83.0.4103.0`) | 288 | | `3.1.*` | `npm i chrome-aws-lambda@~3.1.1` | [`756035`](https://crrev.com/756035) (`83.0.4103.0`) | 289 | | `3.0.*` | `npm i chrome-aws-lambda@~3.0.4` | [`737027`](https://crrev.com/737027) (`81.0.4044.0`) | 290 | | `2.1.*` | `npm i chrome-aws-lambda@~2.1.1` | [`722234`](https://crrev.com/722234) (`80.0.3987.0`) | 291 | | `2.0.*` | `npm i chrome-aws-lambda@~2.0.2` | [`705776`](https://crrev.com/705776) (`79.0.3945.0`) | 292 | | `1.20.*` | `npm i chrome-aws-lambda@~1.20.4` | [`686378`](https://crrev.com/686378) (`78.0.3882.0`) | 293 | | `1.19.*` | `npm i chrome-aws-lambda@~1.19.0` | [`674921`](https://crrev.com/674921) (`77.0.3844.0`) | 294 | | `1.18.*` | `npm i chrome-aws-lambda@~1.18.1` | [`672088`](https://crrev.com/672088) (`77.0.3835.0`) | 295 | | `1.18.*` | `npm i chrome-aws-lambda@~1.18.0` | [`669486`](https://crrev.com/669486) (`77.0.3827.0`) | 296 | | `1.17.*` | `npm i chrome-aws-lambda@~1.17.1` | [`662092`](https://crrev.com/662092) (`76.0.3803.0`) | 297 | | `1.16.*` | `npm i chrome-aws-lambda@~1.16.1` | [`656675`](https://crrev.com/656675) (`76.0.3786.0`) | 298 | | `1.15.*` | `npm i chrome-aws-lambda@~1.15.1` | [`650583`](https://crrev.com/650583) (`75.0.3765.0`) | 299 | | `1.14.*` | `npm i chrome-aws-lambda@~1.14.0` | [`641577`](https://crrev.com/641577) (`75.0.3738.0`) | 300 | | `1.13.*` | `npm i chrome-aws-lambda@~1.13.0` | [`637110`](https://crrev.com/637110) (`74.0.3723.0`) | 301 | | `1.12.*` | `npm i chrome-aws-lambda@~1.12.2` | [`624492`](https://crrev.com/624492) (`73.0.3679.0`) | 302 | | `1.11.*` | `npm i chrome-aws-lambda@~1.11.2` | [`609904`](https://crrev.com/609904) (`72.0.3618.0`) | 303 | | `1.10.*` | `npm i chrome-aws-lambda@~1.10.1` | [`604907`](https://crrev.com/604907) (`72.0.3582.0`) | 304 | | `1.9.*` | `npm i chrome-aws-lambda@~1.9.1` | [`594312`](https://crrev.com/594312) (`71.0.3563.0`) | 305 | | `1.8.*` | `npm i chrome-aws-lambda@~1.8.0` | [`588429`](https://crrev.com/588429) (`71.0.3542.0`) | 306 | | `1.7.*` | `npm i chrome-aws-lambda@~1.7.0` | [`579032`](https://crrev.com/579032) (`70.0.3508.0`) | 307 | | `1.6.*` | `npm i chrome-aws-lambda@~1.6.3` | [`575458`](https://crrev.com/575458) (`69.0.3494.0`) | 308 | | `1.5.*` | `npm i chrome-aws-lambda@~1.5.0` | [`564778`](https://crrev.com/564778) (`69.0.3452.0`) | 309 | | `1.4.*` | `npm i chrome-aws-lambda@~1.4.0` | [`555668`](https://crrev.com/555668) (`68.0.3419.0`) | 310 | | `1.3.*` | `npm i chrome-aws-lambda@~1.3.0` | [`549031`](https://crrev.com/549031) (`67.0.3391.0`) | 311 | | `1.2.*` | `npm i chrome-aws-lambda@~1.2.0` | [`543305`](https://crrev.com/543305) (`67.0.3372.0`) | 312 | | `1.1.*` | `npm i chrome-aws-lambda@~1.1.0` | [`536395`](https://crrev.com/536395) (`66.0.3347.0`) | 313 | | `1.0.*` | `npm i chrome-aws-lambda@~1.0.0` | [`526987`](https://crrev.com/526987) (`65.0.3312.0`) | 314 | | `0.13.*` | `npm i chrome-aws-lambda@~0.13.0` | [`515411`](https://crrev.com/515411) (`64.0.3264.0`) | 315 | 316 | Patch versions are reserved for bug fixes in `chrome-aws-lambda` and general maintenance. 317 | 318 | ## Compiling 319 | 320 | To compile your own version of Chromium check the [Ansible playbook instructions](_/ansible). 321 | 322 | ## AWS Lambda Layer 323 | 324 | [Lambda Layers](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html) is a new convenient way to manage common dependencies between different Lambda Functions. 325 | 326 | The following set of (Linux) commands will create a layer of this package alongside `puppeteer-core`: 327 | 328 | ```shell 329 | git clone --depth=1 https://github.com/Sparticuz/chrome-aws-lambda.git && \ 330 | cd chrome-aws-lambda && \ 331 | make chrome_aws_lambda.zip 332 | ``` 333 | 334 | The above will create a `chrome-aws-lambda.zip` file, which can be uploaded to your Layers console. 335 | 336 | Alternatively, you can also download the layer artifact from one of our [CI workflow runs](https://github.com/Sparticuz/chrome-aws-lambda/actions/workflows/aws.yml?query=is%3Asuccess+branch%3Amaster). 337 | 338 | ## Google Cloud Functions 339 | 340 | Since version `1.11.2`, it's also possible to use this package on Google/Firebase Cloud Functions. 341 | 342 | According to our benchmarks, it's 40% to 50% faster than using the off-the-shelf `puppeteer` bundle. 343 | 344 | ## Compression 345 | 346 | The Chromium binary is compressed using the Brotli algorithm. 347 | 348 | This allows us to get the best compression ratio and faster decompression times. 349 | 350 | | File | Algorithm | Level | Bytes | MiB | % | Inflation | 351 | | ------------- | --------- | ----- | --------- | --------- | ---------- | ---------- | 352 | | `chromium` | - | - | 136964856 | 130.62 | - | - | 353 | | `chromium.gz` | Gzip | 1 | 51662087 | 49.27 | 62.28% | 1.035s | 354 | | `chromium.gz` | Gzip | 2 | 50438352 | 48.10 | 63.17% | 1.016s | 355 | | `chromium.gz` | Gzip | 3 | 49428459 | 47.14 | 63.91% | 0.968s | 356 | | `chromium.gz` | Gzip | 4 | 47873978 | 45.66 | 65.05% | 0.950s | 357 | | `chromium.gz` | Gzip | 5 | 46929422 | 44.76 | 65.74% | 0.938s | 358 | | `chromium.gz` | Gzip | 6 | 46522529 | 44.37 | 66.03% | 0.919s | 359 | | `chromium.gz` | Gzip | 7 | 46406406 | 44.26 | 66.12% | 0.917s | 360 | | `chromium.gz` | Gzip | 8 | 46297917 | 44.15 | 66.20% | 0.916s | 361 | | `chromium.gz` | Gzip | 9 | 46270972 | 44.13 | 66.22% | 0.968s | 362 | | `chromium.gz` | Zopfli | 10 | 45089161 | 43.00 | 67.08% | 0.919s | 363 | | `chromium.gz` | Zopfli | 20 | 45085868 | 43.00 | 67.08% | 0.919s | 364 | | `chromium.gz` | Zopfli | 30 | 45085003 | 43.00 | 67.08% | 0.925s | 365 | | `chromium.gz` | Zopfli | 40 | 45084328 | 43.00 | 67.08% | 0.921s | 366 | | `chromium.gz` | Zopfli | 50 | 45084098 | 43.00 | 67.08% | 0.935s | 367 | | `chromium.br` | Brotli | 0 | 55401211 | 52.83 | 59.55% | 0.778s | 368 | | `chromium.br` | Brotli | 1 | 54429523 | 51.91 | 60.26% | 0.757s | 369 | | `chromium.br` | Brotli | 2 | 46436126 | 44.28 | 66.10% | 0.659s | 370 | | `chromium.br` | Brotli | 3 | 46122033 | 43.99 | 66.33% | 0.616s | 371 | | `chromium.br` | Brotli | 4 | 45050239 | 42.96 | 67.11% | 0.692s | 372 | | `chromium.br` | Brotli | 5 | 40813510 | 38.92 | 70.20% | **0.598s** | 373 | | `chromium.br` | Brotli | 6 | 40116951 | 38.26 | 70.71% | 0.601s | 374 | | `chromium.br` | Brotli | 7 | 39302281 | 37.48 | 71.30% | 0.615s | 375 | | `chromium.br` | Brotli | 8 | 39038303 | 37.23 | 71.50% | 0.668s | 376 | | `chromium.br` | Brotli | 9 | 38853994 | 37.05 | 71.63% | 0.673s | 377 | | `chromium.br` | Brotli | 10 | 36090087 | 34.42 | 73.65% | 0.765s | 378 | | `chromium.br` | Brotli | 11 | 34820408 | **33.21** | **74.58%** | 0.712s | 379 | 380 | ## License 381 | 382 | MIT 383 | -------------------------------------------------------------------------------- /_/amazon/code/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sparticuz/chrome-aws-lambda/2fb71eb0ca2e08292973efe3ca59182debbbffc8/_/amazon/code/.gitignore -------------------------------------------------------------------------------- /_/amazon/events/example.com.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://example.com", 4 | "expected": { 5 | "title": "Example Domain", 6 | "screenshot": "72e10960dcf78c864f3d3635e3beb5be394daf40" 7 | } 8 | }, 9 | { 10 | "url": "https://example.com", 11 | "expected": { 12 | "title": "Example Domain", 13 | "screenshot": "72e10960dcf78c864f3d3635e3beb5be394daf40" 14 | } 15 | }, 16 | { 17 | "url": "https://get.webgl.org", 18 | "expected": { 19 | "remove": "logo-container", 20 | "screenshot": "25ac96a4e44f338f5362c18da2b2823ee599c330" 21 | } 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /_/amazon/handlers/index.js: -------------------------------------------------------------------------------- 1 | const { ok } = require('assert'); 2 | const { createHash } = require('crypto'); 3 | const chromium = require('@sparticuz/chrome-aws-lambda'); 4 | 5 | exports.handler = async (event, context) => { 6 | let browser = null; 7 | 8 | try { 9 | const browser = await chromium.puppeteer.launch({ 10 | args: chromium.args, 11 | defaultViewport: chromium.defaultViewport, 12 | executablePath: await chromium.executablePath, 13 | headless: chromium.headless, 14 | ignoreHTTPSErrors: true, 15 | }); 16 | 17 | const contexts = [ 18 | browser.defaultBrowserContext(), 19 | ]; 20 | 21 | while (contexts.length < event.length) { 22 | contexts.push(await browser.createIncognitoBrowserContext()); 23 | } 24 | 25 | for (let context of contexts) { 26 | const job = event.shift(); 27 | const page = await context.defaultPage(); 28 | 29 | if (job.hasOwnProperty('url') === true) { 30 | await page.goto(job.url, { waitUntil: ['domcontentloaded', 'load'] }); 31 | 32 | if (job.hasOwnProperty('expected') === true) { 33 | if (job.expected.hasOwnProperty('title') === true) { 34 | ok(await page.title() === job.expected.title, `Title assertion failed.`); 35 | } 36 | 37 | if (job.expected.hasOwnProperty('screenshot') === true) { 38 | if (job.expected.hasOwnProperty('remove') === true ) { 39 | await page.evaluate((selector) => { 40 | document.getElementById(selector).remove(); 41 | }, job.expected.remove); 42 | } 43 | ok(createHash('sha1').update((await page.screenshot()).toString('base64')).digest('hex') === job.expected.screenshot, `Screenshot assertion failed.`); 44 | } 45 | } 46 | } 47 | } 48 | } catch (error) { 49 | throw error.message; 50 | } finally { 51 | if (browser !== null) { 52 | await browser.close(); 53 | } 54 | } 55 | 56 | return true; 57 | }; 58 | -------------------------------------------------------------------------------- /_/amazon/template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Globals: 4 | Function: 5 | MemorySize: 2048 6 | Timeout: 30 7 | 8 | Resources: 9 | layer: 10 | Type: AWS::Serverless::LayerVersion 11 | Properties: 12 | LayerName: sparticuz-chrome-aws-lambda 13 | ContentUri: code/ 14 | CompatibleRuntimes: 15 | - nodejs14.x 16 | - nodejs16.x 17 | 18 | node14: 19 | Type: AWS::Serverless::Function 20 | Properties: 21 | Layers: 22 | - !Ref layer 23 | Handler: handlers/index.handler 24 | Runtime: nodejs14.x 25 | Policies: 26 | - AWSLambdaBasicExecutionRole 27 | - AWSXRayDaemonWriteAccess 28 | Tracing: Active 29 | node16: 30 | Type: AWS::Serverless::Function 31 | Properties: 32 | Layers: 33 | - !Ref layer 34 | Handler: handlers/index.handler 35 | Runtime: nodejs16.x 36 | Policies: 37 | - AWSLambdaBasicExecutionRole 38 | - AWSXRayDaemonWriteAccess 39 | Tracing: Active 40 | -------------------------------------------------------------------------------- /_/ansible/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: ansible chromium 2 | 3 | dependencies: 4 | sudo apt install python3-pip zip 5 | pip install ansible boto boto3 aws-sam-cli 6 | echo "Docker is also required in order to test the package, please install docker or Docker Desktop" 7 | 8 | chromium: 9 | ansible-playbook plays/chromium.yml -i inventory.ini 10 | -------------------------------------------------------------------------------- /_/ansible/README.md: -------------------------------------------------------------------------------- 1 | # Chromium Playbook 2 | 3 | This Ansible playbook will launch an EC2 `c6a.12xlarge` Spot Instance and compile Chromium statically. 4 | 5 | Once the compilation finishes, the binary will be compressed with Brotli and downloaded. 6 | 7 | The whole process usually takes around 1 hour to on a `c6a.12xlarge` instance. 8 | 9 | ## Chromium Version 10 | 11 | To compile a specific version of Chromium, update the `puppeteer_version` variable in the Ansible inventory, i.e.: 12 | 13 | ```shell 14 | puppeteer_version=v1.9.0 15 | ``` 16 | 17 | If not specified, the current `main` will be used. 18 | 19 | ## Usage 20 | 21 | ```shell 22 | AWS_REGION=us-east-1 \ 23 | AWS_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXX \ 24 | AWS_SECRET_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \ 25 | make chromium 26 | ``` 27 | 28 | ## Requirements 29 | 30 | - [Ansible](http://docs.ansible.com/ansible/latest/intro_installation.html#latest-releases-via-apt-ubuntu) 31 | - AWS SDK for Python (`boto` and `boto3`) 32 | -------------------------------------------------------------------------------- /_/ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | hash_behaviour = merge 3 | host_key_checking = false 4 | retry_files_enabled = false 5 | 6 | [ssh_connection] 7 | ssh_args = -C -o ControlMaster=auto -o ControlPersist=60 -o ServerAliveInterval=30 8 | pipelining = true 9 | -------------------------------------------------------------------------------- /_/ansible/inventory.ini: -------------------------------------------------------------------------------- 1 | [localhost] 2 | 127.0.0.1 3 | 4 | [localhost:vars] 5 | ansible_connection=local 6 | ansible_python_interpreter=python 7 | image=ami-0309aede310b9cc1f 8 | region=us-east-1 9 | instance_size=c6a.8xlarge 10 | 11 | [aws] 12 | 13 | [aws:vars] 14 | ansible_connection=ssh 15 | ansible_python_interpreter=auto_silent 16 | ansible_ssh_private_key_file=ansible.pem 17 | puppeteer_version=v17.1.3 18 | -------------------------------------------------------------------------------- /_/ansible/plays/.gclient: -------------------------------------------------------------------------------- 1 | solutions = [ 2 | { 3 | "name": "src", 4 | "url": "https://chromium.googlesource.com/chromium/src.git", 5 | "managed": False, 6 | "custom_deps": {}, 7 | "custom_vars": { 8 | "checkout_pgo_profiles": True, 9 | }, 10 | }, 11 | ] 12 | -------------------------------------------------------------------------------- /_/ansible/plays/chromium.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Bootstrap AWS 3 | hosts: localhost 4 | gather_facts: false 5 | 6 | tasks: 7 | - name: Creating SSH Key 8 | shell: | 9 | ssh-keygen -b 2048 -t rsa -f ansible.pem -q -N '' && \ 10 | chmod 0600 ansible.pem.pub 11 | args: 12 | chdir: .. 13 | creates: ansible.pem 14 | 15 | - name: Creating EC2 Key Pair 16 | amazon.aws.ec2_key: 17 | name: ansible 18 | state: present 19 | region: "{{ region }}" 20 | key_material: "{{ item }}" 21 | with_file: ../ansible.pem.pub 22 | 23 | - name: Creating Security Group 24 | amazon.aws.ec2_group: 25 | name: Chromium 26 | description: SSH Access 27 | state: present 28 | region: "{{ region }}" 29 | rules: 30 | - proto: tcp 31 | to_port: 22 32 | from_port: 22 33 | cidr_ip: 0.0.0.0/0 34 | rules_egress: 35 | - proto: all 36 | cidr_ip: 0.0.0.0/0 37 | 38 | - name: Request EC2 Instance 39 | amazon.aws.ec2_instance: 40 | count: 1 41 | ebs_optimized: yes 42 | image: 43 | id: "{{ image }}" 44 | instance_initiated_shutdown_behavior: terminate 45 | instance_type: "{{ instance_size }}" 46 | key_name: ansible 47 | network: 48 | assign_public_ip: yes 49 | delete_on_termination: yes 50 | groups: Chromium 51 | region: "{{ region }}" 52 | security_group: Chromium 53 | state: present 54 | tags: 55 | Name: Chromium 56 | volumes: 57 | - device_name: /dev/xvda 58 | ebs: 59 | delete_on_termination: true 60 | volume_type: io2 61 | volume_size: 128 62 | iops: 3000 63 | register: ec2 64 | 65 | - name: Registering Host 66 | add_host: 67 | hostname: "{{ item.public_ip_address }}" 68 | groupname: aws 69 | with_items: "{{ ec2.instances }}" 70 | 71 | - name: Waiting for SSH 72 | wait_for: 73 | host: "{{ item.public_ip_address }}" 74 | port: 22 75 | timeout: 120 76 | state: started 77 | with_items: "{{ ec2.instances }}" 78 | 79 | - name: AWS 80 | user: ec2-user 81 | hosts: aws 82 | gather_facts: true 83 | environment: 84 | LANG: en_US.UTF-8 85 | LC_ALL: en_US.UTF-8 86 | PATH: "{{ ansible_env.PATH }}:/srv/source/depot_tools" 87 | 88 | tasks: 89 | - name: Update system 90 | become: true 91 | become_user: root 92 | shell: | 93 | dnf update --releasever=2022.0.20220831 -y 94 | 95 | - name: Installing Packages 96 | become: true 97 | become_user: root 98 | dnf: 99 | name: 100 | - "@Development Tools" 101 | - alsa-lib-devel 102 | - atk-devel 103 | - bc 104 | - bluez-libs-devel 105 | - bzip2-devel 106 | - cairo-devel 107 | - cmake 108 | - cups-devel 109 | - dbus-devel 110 | - dbus-glib-devel 111 | - dbus-x11 112 | - expat-devel 113 | - glibc 114 | - glibc-langpack-en 115 | - gperf 116 | - gtk3-devel 117 | - httpd 118 | - java-17-amazon-corretto 119 | - libatomic 120 | - libcap-devel 121 | - libjpeg-devel 122 | - libstdc++ 123 | - libXScrnSaver-devel 124 | - libxkbcommon-x11-devel 125 | - mod_ssl 126 | - ncurses-compat-libs 127 | - nspr-devel 128 | - nss-devel 129 | - pam-devel 130 | - pciutils-devel 131 | - perl 132 | - php 133 | - php-cli 134 | - pulseaudio-libs-devel 135 | - python 136 | - python-psutil 137 | - python-setuptools 138 | - ruby 139 | - xorg-x11-server-Xvfb 140 | - zlib 141 | state: latest 142 | update_cache: true 143 | 144 | - name: Checking for Directory Structure 145 | stat: 146 | path: /srv/source/chromium 147 | register: 148 | structure 149 | 150 | - name: Creating Directory Structure 151 | become: true 152 | become_user: root 153 | file: 154 | path: /srv/{{ item }}/chromium 155 | state: directory 156 | group: ec2-user 157 | owner: ec2-user 158 | recurse: true 159 | with_items: 160 | - build 161 | - source 162 | when: structure.stat.exists != true 163 | 164 | - name: Cloning Depot Tools 165 | git: 166 | repo: https://chromium.googlesource.com/chromium/tools/depot_tools.git 167 | dest: /srv/source/depot_tools 168 | force: yes 169 | update: yes 170 | 171 | - name: Upload .gclient 172 | copy: 173 | src: .gclient 174 | dest: /srv/source/chromium/.gclient 175 | owner: ec2-user 176 | group: ec2-user 177 | mode: '0664' 178 | 179 | - name: Checking for Chromium 180 | stat: 181 | path: /srv/source/chromium/.gclient 182 | register: gclient 183 | 184 | - name: Resolving Puppeteer Version 185 | uri: 186 | url: "https://raw.githubusercontent.com/puppeteer/puppeteer/{{ puppeteer_version | default('main') }}/src/revisions.ts" 187 | return_content: yes 188 | register: puppeteer_revisions 189 | 190 | - name: Resolving Chromium Revision from Puppeteer Version 191 | set_fact: 192 | chromium_revision: > 193 | {{ puppeteer_revisions.content | regex_search("chromium: [']([0-9]*)[']", '\1') | first }} 194 | 195 | - name: Resolving Git Commit from Chromium Revision 196 | uri: 197 | url: "https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/{{ chromium_revision }}" 198 | return_content: yes 199 | register: revision 200 | 201 | - name: Parse Result 202 | set_fact: 203 | gitsha: > 204 | {{ revision.content | regex_search('"git_sha":"([a-zA-Z0-9_]*)"', '\1') | trim }} 205 | 206 | - name: Checking Out Chromium revision 207 | shell: | 208 | gclient sync --delete_unversioned_trees --revision {{ gitsha | first }} --with_branch_heads 209 | args: 210 | chdir: /srv/source/chromium 211 | 212 | - name: Run Chromium hooks 213 | shell: | 214 | gclient runhooks 215 | args: 216 | chdir: /srv/source/chromium 217 | 218 | - name: Patching Chromium 219 | lineinfile: 220 | path: "/srv/source/chromium/src/content/browser/{{ item.path }}" 221 | line: "{{ item.line }}" 222 | regexp: "{{ item.regexp }}" 223 | state: present 224 | backrefs: yes 225 | with_items: 226 | - { 227 | path: 'sandbox_ipc_linux.cc', 228 | line: '\1PLOG(WARNING) << "poll"; failed_polls = 0;', 229 | regexp: '^(\s+)PLOG[(]WARNING[)] << "poll";$', 230 | } 231 | - { 232 | path: 'renderer_host/render_process_host_impl.cc', 233 | line: '\1// \2\3', 234 | regexp: '^( )(\s*)(CHECK[(]render_process_host->InSameStoragePartition[(])$', 235 | } 236 | - { 237 | path: 'renderer_host/render_process_host_impl.cc', 238 | line: '\1// \2\3', 239 | regexp: '^( )(\s*)(browser_context->GetStoragePartition[(]site_instance,)$', 240 | } 241 | - { 242 | path: 'renderer_host/render_process_host_impl.cc', 243 | line: '\1// \2\3', 244 | regexp: '^( )(\s*)(false /[*] can_create [*]/[)][)][)];)$', 245 | } 246 | 247 | - name: Creating Build Configuration Directory 248 | file: 249 | mode: 0755 250 | path: /srv/source/chromium/src/out/Headless 251 | state: directory 252 | 253 | - name: Mounting Build Directory in Memory 254 | become: true 255 | become_user: root 256 | shell: | 257 | mount --types tmpfs --options size=24G,nr_inodes=128k,mode=1777 tmpfs /srv/source/chromium/src/out/Headless 258 | args: 259 | warn: false 260 | 261 | - name: Creating Headless Chromium Configuration 262 | copy: 263 | content: | 264 | import("//build/args/headless.gn") 265 | blink_symbol_level = 0 266 | dcheck_always_on = false 267 | disable_histogram_support = false 268 | enable_basic_print_dialog = false 269 | enable_basic_printing = true 270 | enable_keystone_registration_framework = false 271 | enable_linux_installer = false 272 | enable_media_remoting = false 273 | enable_one_click_signin = false 274 | ffmpeg_branding = "Chrome" 275 | is_component_build = false 276 | is_debug = false 277 | is_official_build = true 278 | proprietary_codecs = true 279 | symbol_level = 0 280 | target_cpu = "x64" 281 | target_os = "linux" 282 | use_brlapi = 0 283 | use_sysroot = true 284 | v8_symbol_level = 0 285 | v8_target_cpu = "x64" 286 | dest: /srv/source/chromium/src/out/Headless/args.gn 287 | 288 | - name: Generating Headless Chromium Configuration 289 | shell: | 290 | gn gen out/Headless 291 | args: 292 | chdir: /srv/source/chromium/src 293 | 294 | - name: Compiling Headless Chromium 295 | shell: | 296 | autoninja -C out/Headless headless_shell 297 | args: 298 | chdir: /srv/source/chromium/src 299 | 300 | - name: Getting Chromium Version 301 | shell: | 302 | sed --regexp-extended 's~[^0-9]+~~g' chrome/VERSION | tr '\n' '.' | sed 's~[.]$~~' 303 | args: 304 | chdir: /srv/source/chromium/src 305 | warn: false 306 | register: version 307 | 308 | - name: Striping Symbols from Chromium Binary 309 | shell: | 310 | strip -o /srv/build/chromium/chromium-{{ version.stdout | quote }} out/Headless/headless_shell 311 | args: 312 | chdir: /srv/source/chromium/src 313 | 314 | - name: Compressing Chromium 315 | shell: | 316 | brotli --best --force {{ item }} 317 | args: 318 | chdir: /srv/build/chromium 319 | with_items: 320 | - "chromium-{{ version.stdout }}" 321 | 322 | - name: Downloading Chromium 323 | fetch: 324 | src: "/srv/build/chromium/{{ item }}" 325 | dest: ../../../bin/ 326 | flat: yes 327 | fail_on_missing: true 328 | with_items: 329 | - "chromium-{{ version.stdout }}.br" 330 | 331 | - name: Archiving OpenGL ES driver 332 | shell: | 333 | tar --directory /srv/source/chromium/src/out/Headless --create --file swiftshader.tar libEGL.so libGLESv2.so libvk_swiftshader.so libvulkan.so.1 vk_swiftshader_icd.json 334 | args: 335 | chdir: /srv/build/chromium 336 | creates: /srv/build/chromium/swiftshader.tar 337 | warn: false 338 | 339 | - name: Compressing OpenGL ES driver 340 | shell: | 341 | brotli --best --force swiftshader.tar 342 | args: 343 | chdir: /srv/build/chromium 344 | creates: /srv/build/chromium/swiftshader.tar.br 345 | 346 | - name: Downloading OpenGL ES driver 347 | fetch: 348 | src: /srv/build/chromium/swiftshader.tar.br 349 | dest: ../../../bin/ 350 | flat: yes 351 | fail_on_missing: true 352 | 353 | - name: Teardown AWS 354 | hosts: localhost 355 | gather_facts: false 356 | 357 | tasks: 358 | - name: Terminating EC2 Instance 359 | amazon.aws.ec2_instance: 360 | wait: yes 361 | state: absent 362 | instance_ids: '{{ ec2.instance_ids }}' 363 | region: "{{ region }}" 364 | 365 | - name: Deleting Security Group 366 | amazon.aws.ec2_group: 367 | name: Chromium 368 | state: absent 369 | region: "{{ region }}" 370 | 371 | - name: Deleting EC2 Key Pair 372 | amazon.aws.ec2_key: 373 | name: ansible 374 | state: absent 375 | region: "{{ region }}" 376 | 377 | - name: Deleting SSH Key 378 | file: 379 | path: "../{{ item }}" 380 | state: absent 381 | with_items: 382 | - ansible.pem 383 | - ansible.pem.pub 384 | -------------------------------------------------------------------------------- /bin/aws.tar.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sparticuz/chrome-aws-lambda/2fb71eb0ca2e08292973efe3ca59182debbbffc8/bin/aws.tar.br -------------------------------------------------------------------------------- /bin/chromium.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sparticuz/chrome-aws-lambda/2fb71eb0ca2e08292973efe3ca59182debbbffc8/bin/chromium.br -------------------------------------------------------------------------------- /bin/swiftshader.tar.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sparticuz/chrome-aws-lambda/2fb71eb0ca2e08292973efe3ca59182debbbffc8/bin/swiftshader.tar.br -------------------------------------------------------------------------------- /incrementVersion: -------------------------------------------------------------------------------- 1 | # incrementVersion.sh OLD_VERSION NEW_VERSION 2 | # Example: incrementVersion 16.1.0 16.2.0 3 | 4 | OLD_VERSION=$1 5 | NEW_VERSION=$2 6 | 7 | sed -i "s/$OLD_VERSION/$NEW_VERSION/" _/ansible/inventory.ini 8 | sed -i "s/\"puppeteer-core\": \"$OLD_VERSION\"/\"puppeteer-core\": \"$NEW_VERSION\"/g" package.json 9 | sed -i "s/puppeteer-core@$OLD_VERSION/puppeteer-core@$NEW_VERSION/" Makefile 10 | 11 | echo "Version number incremented $OLD_VERSION -> $NEW_VERSION. 12 | 13 | 1) Check for a new version of 'chromium' included with 'puppeteer': 14 | a) https://github.com/puppeteer/puppeteer/blob/main/src/revisions.ts 15 | b) https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/####### 16 | c) https://omahaproxy.appspot.com/ 17 | 18 | 2) If the 'chromium' version has been incremented, please compile a new version of 'chromium': 19 | a) cd _/ansible && make chromium 20 | b) Rename the new chromium binary 21 | 22 | 3) Please also update README.md#Versioning 23 | 24 | 4) Merge the PR and deploy to npm 25 | a) Test the new version using 'npm run test' 26 | b) Push the PR to Github and merge it 27 | c) Checkout the main branch 28 | d) Run 'npm version $NEW_VERSION' to publish the package to npm." 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sparticuz/chrome-aws-lambda", 3 | "version": "17.1.3", 4 | "author": { 5 | "name": "Kyle McNally" 6 | }, 7 | "license": "MIT", 8 | "description": "Chromium Binary for AWS Lambda and Google Cloud Functions, forked from @alixaxel/chrome-aws-lambda", 9 | "main": "build/index.js", 10 | "types": "build/index.d.ts", 11 | "files": [ 12 | "bin", 13 | "build", 14 | "typings" 15 | ], 16 | "engines": { 17 | "node": ">= 14" 18 | }, 19 | "scripts": { 20 | "test": "make clean && make && make pretest && make test", 21 | "build": "rm -rf build && tsc -p tsconfig.json", 22 | "postversion": "git push && git push --tags && npm publish", 23 | "prepack": "npm run build", 24 | "preversion": "npm run build" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^16.11.49", 28 | "@types/tar-fs": "^2.0.1", 29 | "clean-modules": "^2.0.6", 30 | "puppeteer-core": "17.1.3", 31 | "typescript": "^4.6.4" 32 | }, 33 | "peerDependencies": { 34 | "puppeteer-core": "17.1.3" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/Sparticuz/chrome-aws-lambda/issues" 38 | }, 39 | "homepage": "https://github.com/Sparticuz/chrome-aws-lambda", 40 | "repository": { 41 | "type": "git", 42 | "url": "git://github.com/Sparticuz/chrome-aws-lambda.git" 43 | }, 44 | "keywords": [ 45 | "aws", 46 | "browser", 47 | "chrome", 48 | "chromium", 49 | "lambda", 50 | "puppeteer", 51 | "serverless" 52 | ], 53 | "prettier": { 54 | "arrowParens": "always", 55 | "bracketSpacing": true, 56 | "jsxBracketSameLine": false, 57 | "printWidth": 140, 58 | "semi": true, 59 | "singleQuote": true, 60 | "tabWidth": 2, 61 | "trailingComma": "es5", 62 | "useTabs": false 63 | }, 64 | "dependencies": { 65 | "tar-fs": "^2.1.1" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /source/hooks/adblock.ts: -------------------------------------------------------------------------------- 1 | import { promises } from 'fs'; 2 | import { get } from 'https'; 3 | import { Page } from 'puppeteer-core'; 4 | 5 | let adblocker: any = null; 6 | 7 | /** 8 | * Enables ad blocking in page. 9 | * Requires `@cliqz/adblocker-puppeteer` package. 10 | * 11 | * @param page - Page to hook to. 12 | */ 13 | export = async function (page: Page): Promise { 14 | if (adblocker == null) { 15 | const { fullLists, PuppeteerBlocker } = require('@cliqz/adblocker-puppeteer'); 16 | 17 | adblocker = await PuppeteerBlocker.fromLists( 18 | (url: string) => { 19 | return new Promise((resolve, reject) => { 20 | return get(url, (response) => { 21 | if (response.statusCode !== 200) { 22 | return reject(`Unexpected status code: ${response.statusCode}.`); 23 | } 24 | 25 | let result = ''; 26 | 27 | response.on('data', (chunk) => { 28 | result += chunk; 29 | }); 30 | 31 | response.on('end', () => { 32 | return resolve({ text: () => result }); 33 | }); 34 | }); 35 | }); 36 | }, 37 | fullLists, 38 | { enableCompression: false }, 39 | { 40 | path: '/tmp/adblock.bin', 41 | read: promises.readFile, 42 | write: promises.writeFile, 43 | } 44 | ); 45 | } 46 | 47 | return await adblocker.enableBlockingInPage(page).then(() => page); 48 | } 49 | -------------------------------------------------------------------------------- /source/hooks/agent.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | 3 | /** 4 | * Removes `Headless` from the User Agent string, if present. 5 | * 6 | * @param page - Page to hook to. 7 | */ 8 | export = async function (page: Page): Promise { 9 | let result = await page.browser().userAgent(); 10 | 11 | if (result.includes('Headless') === true) { 12 | await page.setUserAgent(result.replace('Headless', '')); 13 | } 14 | 15 | return page; 16 | }; 17 | -------------------------------------------------------------------------------- /source/hooks/chrome.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | import { Writeable } from '../../typings/chrome-aws-lambda'; 3 | 4 | /** 5 | * Mocks the global `chrome` property to mimic headful Chrome. 6 | * 7 | * @param page - Page to hook to. 8 | */ 9 | export = async function (page: Page): Promise { 10 | const handler = () => { 11 | let alpha = Date.now(); 12 | let delta = Math.floor(500 * Math.random()); 13 | 14 | if ((window as any).chrome === undefined) { 15 | Object.defineProperty(window, 'chrome', { 16 | configurable: false, 17 | enumerable: true, 18 | value: {}, 19 | writable: true, 20 | }); 21 | } 22 | 23 | /** 24 | * https://github.com/berstend/puppeteer-extra/blob/master/packages/puppeteer-extra-plugin-stealth/evasions/chrome.app/index.js 25 | */ 26 | if ((window as any).chrome.app === undefined) { 27 | const InvocationError = (callback: string) => { 28 | /** 29 | * Truncates every line of the stack trace (with the exception of the first), until `search` is found. 30 | */ 31 | const truncateStackTrace = (error: Error, search: string) => { 32 | const stack = error.stack.split('\n'); 33 | const index = stack.findIndex((value: string) => value.trim().startsWith(search)); 34 | 35 | if (index > 0) { 36 | error.stack = [stack[0], ...stack.slice(index + 1)].join('\n'); 37 | } 38 | 39 | return error; 40 | }; 41 | 42 | return truncateStackTrace(new TypeError(`Error in invocation of app.${callback}()`), `at ${callback} (eval at `); 43 | }; 44 | 45 | Object.defineProperty((window as any).chrome, 'app', { 46 | value: { 47 | InstallState: { 48 | DISABLED: 'disabled', 49 | INSTALLED: 'installed', 50 | NOT_INSTALLED: 'not_installed', 51 | }, 52 | RunningState: { 53 | CANNOT_RUN: 'cannot_run', 54 | READY_TO_RUN: 'ready_to_run', 55 | RUNNING: 'running', 56 | }, 57 | get isInstalled() { 58 | return false; 59 | }, 60 | getDetails: function getDetails(): null { 61 | if (arguments.length > 0) { 62 | throw InvocationError('getDetails'); 63 | } 64 | 65 | return null; 66 | }, 67 | getIsInstalled: function getIsInstalled() { 68 | if (arguments.length > 0) { 69 | throw InvocationError('getIsInstalled'); 70 | } 71 | 72 | return false; 73 | }, 74 | runningState: function runningState() { 75 | if (arguments.length > 0) { 76 | throw InvocationError('runningState'); 77 | } 78 | 79 | return 'cannot_run'; 80 | }, 81 | }, 82 | }); 83 | } 84 | 85 | let timing: Partial = { 86 | navigationStart: alpha + 1 * delta, 87 | domContentLoadedEventEnd: alpha + 4 * delta, 88 | responseStart: alpha + 2 * delta, 89 | loadEventEnd: alpha + 5 * delta, 90 | }; 91 | 92 | if (window.performance?.timing !== undefined) { 93 | timing = window.performance.timing; 94 | } 95 | 96 | /** 97 | * https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/evasions/chrome.csi 98 | */ 99 | if ((window as any).chrome.csi === undefined) { 100 | Object.defineProperty((window as any).chrome, 'csi', { 101 | value: function csi() { 102 | return { 103 | startE: timing.navigationStart, 104 | onloadT: timing.domContentLoadedEventEnd, 105 | pageT: Date.now() - timing.navigationStart + Math.random().toFixed(3), 106 | tran: 15, 107 | }; 108 | }, 109 | }); 110 | } 111 | 112 | /** 113 | * https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/evasions/chrome.loadTimes 114 | */ 115 | if ((window as any).chrome.loadTimes === undefined) { 116 | let navigation: Writeable> = { 117 | nextHopProtocol: 'h2', 118 | startTime: 3 * delta, 119 | type: 'other' as any, 120 | }; 121 | 122 | if (typeof window.performance?.getEntriesByType === 'function') { 123 | let entries = { 124 | navigation: window.performance.getEntriesByType('navigation') as PerformanceNavigationTiming[], 125 | paint: window.performance.getEntriesByType('paint') as PerformanceNavigationTiming[], 126 | }; 127 | 128 | if (entries.navigation.length > 0) { 129 | navigation = entries.navigation.shift(); 130 | } 131 | 132 | if (entries.paint.length > 0) { 133 | navigation.startTime = entries.paint.shift().startTime; 134 | } 135 | } 136 | 137 | Object.defineProperty((window as any).chrome, 'loadTimes', { 138 | value: function loadTimes() { 139 | return { 140 | get commitLoadTime() { 141 | return timing.responseStart / 1000; 142 | }, 143 | get connectionInfo() { 144 | return navigation.nextHopProtocol; 145 | }, 146 | get finishDocumentLoadTime() { 147 | return timing.domContentLoadedEventEnd / 1000; 148 | }, 149 | get finishLoadTime() { 150 | return timing.loadEventEnd / 1000; 151 | }, 152 | get firstPaintAfterLoadTime() { 153 | return 0; 154 | }, 155 | get firstPaintTime() { 156 | return parseFloat(((navigation.startTime + (window.performance?.timeOrigin ?? timing.navigationStart)) / 1000).toFixed(3)); 157 | }, 158 | get navigationType() { 159 | return navigation.type; 160 | }, 161 | get npnNegotiatedProtocol() { 162 | return ['h2', 'hq'].includes(navigation.nextHopProtocol) ? navigation.nextHopProtocol : 'unknown'; 163 | }, 164 | get requestTime() { 165 | return timing.navigationStart / 1000; 166 | }, 167 | get startLoadTime() { 168 | return timing.navigationStart / 1000; 169 | }, 170 | get wasAlternateProtocolAvailable() { 171 | return false; 172 | }, 173 | get wasFetchedViaSpdy() { 174 | return ['h2', 'hq'].includes(navigation.nextHopProtocol); 175 | }, 176 | get wasNpnNegotiated() { 177 | return ['h2', 'hq'].includes(navigation.nextHopProtocol); 178 | }, 179 | }; 180 | }, 181 | }); 182 | }; 183 | } 184 | 185 | await page.evaluate(handler); 186 | await page.evaluateOnNewDocument(handler); 187 | 188 | return page; 189 | } 190 | -------------------------------------------------------------------------------- /source/hooks/languages.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | 3 | /** 4 | * Emulates `en-US` language. 5 | * 6 | * @param page - Page to hook to. 7 | */ 8 | export = async function (page: Page): Promise { 9 | const handler = () => { 10 | Object.defineProperty(Object.getPrototypeOf(navigator), 'language', { 11 | get: () => 'en-US', 12 | }); 13 | 14 | Object.defineProperty(Object.getPrototypeOf(navigator), 'languages', { 15 | get: () => ['en-US', 'en'], 16 | }); 17 | }; 18 | 19 | await page.evaluate(handler); 20 | await page.evaluateOnNewDocument(handler); 21 | 22 | return page; 23 | } 24 | -------------------------------------------------------------------------------- /source/hooks/permissions.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | 3 | /** 4 | * Emulates `denied` state for all permission queries. 5 | * 6 | * @param page - Page to hook to. 7 | */ 8 | export = async function (page: Page): Promise { 9 | const handler = () => { 10 | let query = window.navigator.permissions.query; 11 | 12 | (Permissions as any).prototype.query = function (parameters: PermissionDescriptor) { 13 | if (parameters?.name?.length > 0) { 14 | return Promise.resolve({ 15 | onchange: null, 16 | state: 'denied', 17 | }); 18 | } 19 | 20 | return query(parameters); 21 | }; 22 | }; 23 | 24 | await page.evaluate(handler); 25 | await page.evaluateOnNewDocument(handler); 26 | 27 | return page; 28 | } 29 | -------------------------------------------------------------------------------- /source/hooks/timezone.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | 3 | /** 4 | * Emulates UTC timezone. 5 | * 6 | * @param page - Page to hook to. 7 | */ 8 | export = function (page: Page): Promise { 9 | return page.emulateTimezone('UTC').then(() => page); 10 | } 11 | -------------------------------------------------------------------------------- /source/hooks/webdriver.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | 3 | /** 4 | * Removes global `webdriver` property to mimic headful Chrome. 5 | * 6 | * @param page - Page to hook to. 7 | */ 8 | export = async function (page: Page): Promise { 9 | const handler = () => { 10 | Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', { 11 | get: () => false, 12 | }); 13 | }; 14 | 15 | await page.evaluate(handler); 16 | await page.evaluateOnNewDocument(handler); 17 | 18 | return page; 19 | } 20 | -------------------------------------------------------------------------------- /source/hooks/window.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer-core'; 2 | 3 | /** 4 | * Patches window outer dimentions to mimic headful Chrome. 5 | * 6 | * @param page - Page to hook to. 7 | */ 8 | export = async function (page: Page): Promise { 9 | const handler = () => { 10 | if (window.outerWidth === 0) { 11 | Object.defineProperty(window, 'outerWidth', { 12 | get: () => screen.availWidth, 13 | }); 14 | } 15 | 16 | if (window.outerHeight === 0) { 17 | Object.defineProperty(window, 'outerHeight', { 18 | get: () => screen.availHeight, 19 | }); 20 | } 21 | 22 | if (window.screenX === 0) { 23 | Object.defineProperty(window, 'screenX', { 24 | get: () => screen.width - screen.availWidth, 25 | }); 26 | 27 | Object.defineProperty(window, 'screenLeft', { 28 | get: () => screenX, 29 | }); 30 | } 31 | 32 | if (window.screenY === 0) { 33 | Object.defineProperty(window, 'screenY', { 34 | get: () => screen.height - screen.availHeight, 35 | }); 36 | 37 | Object.defineProperty(window, 'screenTop', { 38 | get: () => screenY, 39 | }); 40 | } 41 | }; 42 | 43 | await page.evaluate(handler); 44 | await page.evaluateOnNewDocument(handler); 45 | 46 | return page; 47 | } 48 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { access, createWriteStream, existsSync, mkdirSync, readdirSync, symlink, unlinkSync } from 'fs'; 4 | import { IncomingMessage } from 'http'; 5 | import LambdaFS from './lambdafs'; 6 | import { join } from 'path'; 7 | import { PuppeteerNode, Viewport } from 'puppeteer-core'; 8 | import { URL } from 'url'; 9 | 10 | if (/^AWS_Lambda_nodejs(?:10|12|14|16)[.]x$/.test(process.env.AWS_EXECUTION_ENV) === true) { 11 | if (process.env.FONTCONFIG_PATH === undefined) { 12 | process.env.FONTCONFIG_PATH = '/tmp/aws'; 13 | } 14 | 15 | if (process.env.LD_LIBRARY_PATH === undefined) { 16 | process.env.LD_LIBRARY_PATH = '/tmp/aws/lib'; 17 | } else if (process.env.LD_LIBRARY_PATH.startsWith('/tmp/aws/lib') !== true) { 18 | process.env.LD_LIBRARY_PATH = [...new Set(['/tmp/aws/lib', ...process.env.LD_LIBRARY_PATH.split(':')])].join(':'); 19 | } 20 | } 21 | 22 | class Chromium { 23 | /** 24 | * Downloads or symlinks a custom font and returns its basename, patching the environment so that Chromium can find it. 25 | * If not running on AWS Lambda nor Google Cloud Functions, `null` is returned instead. 26 | */ 27 | static font(input: string): Promise { 28 | if (Chromium.headless !== true) { 29 | return null; 30 | } 31 | 32 | if (process.env.HOME === undefined) { 33 | process.env.HOME = '/tmp'; 34 | } 35 | 36 | if (existsSync(`${process.env.HOME}/.fonts`) !== true) { 37 | mkdirSync(`${process.env.HOME}/.fonts`); 38 | } 39 | 40 | return new Promise((resolve, reject) => { 41 | if (/^https?:[/][/]/i.test(input) !== true) { 42 | input = `file://${input}`; 43 | } 44 | 45 | const url = new URL(input); 46 | const output = `${process.env.HOME}/.fonts/${url.pathname.split('/').pop()}`; 47 | 48 | if (existsSync(output) === true) { 49 | return resolve(output.split('/').pop()); 50 | } 51 | 52 | if (url.protocol === 'file:') { 53 | access(url.pathname, (error) => { 54 | if (error != null) { 55 | return reject(error); 56 | } 57 | 58 | symlink(url.pathname, output, (error) => { 59 | return error != null ? reject(error) : resolve(url.pathname.split('/').pop()); 60 | }); 61 | }); 62 | } else { 63 | let handler = url.protocol === 'http:' ? require('http').get : require('https').get; 64 | 65 | handler(input, (response: IncomingMessage) => { 66 | if (response.statusCode !== 200) { 67 | return reject(`Unexpected status code: ${response.statusCode}.`); 68 | } 69 | 70 | const stream = createWriteStream(output); 71 | 72 | stream.once('error', (error) => { 73 | return reject(error); 74 | }); 75 | 76 | response.on('data', (chunk) => { 77 | stream.write(chunk); 78 | }); 79 | 80 | response.once('end', () => { 81 | stream.end(() => { 82 | return resolve(url.pathname.split('/').pop()); 83 | }); 84 | }); 85 | }); 86 | } 87 | }); 88 | } 89 | 90 | /** 91 | * Returns a list of additional Chromium flags recommended for serverless environments. 92 | * The canonical list of flags can be found on https://peter.sh/experiments/chromium-command-line-switches/. 93 | */ 94 | static get args(): string[] { 95 | const result = [ 96 | '--allow-running-insecure-content', // https://source.chromium.org/search?q=lang:cpp+symbol:kAllowRunningInsecureContent&ss=chromium 97 | '--autoplay-policy=user-gesture-required', // https://source.chromium.org/search?q=lang:cpp+symbol:kAutoplayPolicy&ss=chromium 98 | '--disable-background-timer-throttling', 99 | '--disable-component-update', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableComponentUpdate&ss=chromium 100 | '--disable-domain-reliability', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableDomainReliability&ss=chromium 101 | '--disable-features=AudioServiceOutOfProcess,IsolateOrigins,site-per-process', // https://source.chromium.org/search?q=file:content_features.cc&ss=chromium 102 | '--disable-ipc-flooding-protection', 103 | '--disable-print-preview', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisablePrintPreview&ss=chromium 104 | '--disable-dev-shm-usage', 105 | '--disable-setuid-sandbox', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSetuidSandbox&ss=chromium 106 | '--disable-site-isolation-trials', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSiteIsolation&ss=chromium 107 | '--disable-speech-api', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSpeechAPI&ss=chromium 108 | '--disable-web-security', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableWebSecurity&ss=chromium 109 | '--disk-cache-size=33554432', // https://source.chromium.org/search?q=lang:cpp+symbol:kDiskCacheSize&ss=chromium 110 | '--enable-features=SharedArrayBuffer', // https://source.chromium.org/search?q=file:content_features.cc&ss=chromium 111 | '--hide-scrollbars', // https://source.chromium.org/search?q=lang:cpp+symbol:kHideScrollbars&ss=chromium 112 | '--ignore-gpu-blocklist', // https://source.chromium.org/search?q=lang:cpp+symbol:kIgnoreGpuBlocklist&ss=chromium 113 | '--in-process-gpu', // https://source.chromium.org/search?q=lang:cpp+symbol:kInProcessGPU&ss=chromium 114 | '--mute-audio', // https://source.chromium.org/search?q=lang:cpp+symbol:kMuteAudio&ss=chromium 115 | '--no-default-browser-check', // https://source.chromium.org/search?q=lang:cpp+symbol:kNoDefaultBrowserCheck&ss=chromium 116 | '--no-first-run', 117 | '--no-pings', // https://source.chromium.org/search?q=lang:cpp+symbol:kNoPings&ss=chromium 118 | '--no-sandbox', // https://source.chromium.org/search?q=lang:cpp+symbol:kNoSandbox&ss=chromium 119 | '--no-zygote', // https://source.chromium.org/search?q=lang:cpp+symbol:kNoZygote&ss=chromium 120 | '--use-gl=angle', // https://chromium.googlesource.com/chromium/src/+/main/docs/gpu/swiftshader.md 121 | '--use-angle=swiftshader', // https://chromium.googlesource.com/chromium/src/+/main/docs/gpu/swiftshader.md 122 | '--window-size=1920,1080', // https://source.chromium.org/search?q=lang:cpp+symbol:kWindowSize&ss=chromium 123 | ]; 124 | 125 | if (Chromium.headless === true) { 126 | result.push('--single-process'); // https://source.chromium.org/search?q=lang:cpp+symbol:kSingleProcess&ss=chromium 127 | } else { 128 | result.push('--start-maximized'); // https://source.chromium.org/search?q=lang:cpp+symbol:kStartMaximized&ss=chromium 129 | } 130 | 131 | return result; 132 | } 133 | 134 | /** 135 | * Returns sensible default viewport settings. 136 | */ 137 | static get defaultViewport(): Required { 138 | return { 139 | deviceScaleFactor: 1, 140 | hasTouch: false, 141 | height: 1080, 142 | isLandscape: true, 143 | isMobile: false, 144 | width: 1920, 145 | }; 146 | } 147 | 148 | /** 149 | * Inflates the current version of Chromium and returns the path to the binary. 150 | * If not running on AWS Lambda nor Google Cloud Functions, `null` is returned instead. 151 | */ 152 | static get executablePath(): Promise { 153 | if (Chromium.headless !== true) { 154 | return Promise.resolve(null); 155 | } 156 | 157 | if (existsSync('/tmp/chromium') === true) { 158 | for (const file of readdirSync('/tmp')) { 159 | if (file.startsWith('core.chromium') === true) { 160 | unlinkSync(`/tmp/${file}`); 161 | } 162 | } 163 | 164 | return Promise.resolve('/tmp/chromium'); 165 | } 166 | 167 | const input = join(__dirname, '..', 'bin'); 168 | const promises = [ 169 | LambdaFS.inflate(`${input}/chromium.br`), 170 | LambdaFS.inflate(`${input}/swiftshader.tar.br`), 171 | ]; 172 | 173 | if (/^AWS_Lambda_nodejs(?:10|12|14|16)[.]x$/.test(process.env.AWS_EXECUTION_ENV) === true) { 174 | promises.push(LambdaFS.inflate(`${input}/aws.tar.br`)); 175 | } 176 | 177 | return Promise.all(promises).then((result) => result.shift()); 178 | } 179 | 180 | /** 181 | * Returns a boolean indicating if we are running on AWS Lambda or Google Cloud Functions. 182 | * False is returned if Serverless environment variables `IS_LOCAL` or `IS_OFFLINE` are set. 183 | */ 184 | static get headless() { 185 | if (process.env.IS_LOCAL !== undefined || process.env.IS_OFFLINE !== undefined) { 186 | return false; 187 | } 188 | 189 | const environments = [ 190 | 'AWS_LAMBDA_FUNCTION_NAME', 191 | 'FUNCTION_NAME', 192 | 'FUNCTION_TARGET', 193 | 'FUNCTIONS_EMULATOR', 194 | ]; 195 | 196 | return environments.some((key) => process.env[key] !== undefined); 197 | } 198 | 199 | /** 200 | * Overloads puppeteer with useful methods and returns the resolved package. 201 | */ 202 | static get puppeteer(): PuppeteerNode { 203 | for (const overload of ['Browser', 'BrowserContext', 'ElementHandle', 'Frame', 'Page']) { 204 | require(`${__dirname}/puppeteer/lib/${overload}`); 205 | } 206 | 207 | try { 208 | return require('puppeteer'); 209 | } catch (error: any) { 210 | if (error.code !== 'MODULE_NOT_FOUND') { 211 | throw error; 212 | } 213 | 214 | return require('puppeteer-core'); 215 | } 216 | } 217 | } 218 | 219 | export = Chromium; 220 | -------------------------------------------------------------------------------- /source/lambdafs.ts: -------------------------------------------------------------------------------- 1 | import { createReadStream, createWriteStream, existsSync } from 'fs'; 2 | import { tmpdir } from 'os'; 3 | import { basename, join } from 'path'; 4 | import { extract } from 'tar-fs'; 5 | import { createBrotliDecompress, createUnzip } from 'zlib'; 6 | 7 | class LambdaFS { 8 | /** 9 | * Decompresses a (tarballed) Brotli or Gzip compressed file and returns the path to the decompressed file/folder. 10 | * 11 | * @param filePath Path of the file to decompress. 12 | */ 13 | static inflate(filePath: string): Promise { 14 | const output = filePath.includes("swiftshader") ? tmpdir() : join(tmpdir(), basename(filePath).replace(/[.](?:t(?:ar(?:[.](?:br|gz))?|br|gz)|br|gz)$/i, '')); 15 | 16 | return new Promise((resolve, reject) => { 17 | if (filePath.includes("swiftshader")) { 18 | if (existsSync(`${output}/libGLESv2.so`)) { 19 | return resolve(output); 20 | } 21 | } else { 22 | if (existsSync(output) === true) { 23 | return resolve(output); 24 | } 25 | } 26 | 27 | let source = createReadStream(filePath, { highWaterMark: 2 ** 23 }); 28 | let target = null; 29 | 30 | if (/[.](?:t(?:ar(?:[.](?:br|gz))?|br|gz))$/i.test(filePath) === true) { 31 | target = extract(output); 32 | 33 | target.once('finish', () => { 34 | return resolve(output); 35 | }); 36 | } else { 37 | target = createWriteStream(output, { mode: 0o700 }); 38 | } 39 | 40 | source.once('error', (error: Error) => { 41 | return reject(error); 42 | }); 43 | 44 | target.once('error', (error: Error) => { 45 | return reject(error); 46 | }); 47 | 48 | target.once('close', () => { 49 | return resolve(output); 50 | }); 51 | 52 | if (/(?:br|gz)$/i.test(filePath) === true) { 53 | source.pipe(/br$/i.test(filePath) ? createBrotliDecompress({ chunkSize: 2 ** 21 }) : createUnzip({ chunkSize: 2 ** 21 })).pipe(target); 54 | } else { 55 | source.pipe(target); 56 | } 57 | }); 58 | } 59 | } 60 | 61 | export = LambdaFS; 62 | -------------------------------------------------------------------------------- /source/puppeteer/lib/Browser.ts: -------------------------------------------------------------------------------- 1 | import { Browser, Page } from 'puppeteer-core'; 2 | import { Hook, Prototype } from '../../../typings/chrome-aws-lambda'; 3 | 4 | let Super: Prototype = null; 5 | 6 | try { 7 | Super = require('puppeteer/lib/cjs/puppeteer/common/Browser.js').Browser; 8 | } catch (error) { 9 | Super = require('puppeteer-core/lib/cjs/puppeteer/common/Browser.js').Browser; 10 | } 11 | 12 | Super.prototype.defaultPage = async function (...hooks: Hook[]) { 13 | let page: Page = null; 14 | let pages: Page[] = await this.pages(); 15 | 16 | if (pages.length === 0) { 17 | pages = [await this.newPage()]; 18 | } 19 | 20 | page = pages.shift(); 21 | 22 | if (hooks != null && Array.isArray(hooks) === true) { 23 | for (let hook of hooks) { 24 | page = await hook(page); 25 | } 26 | } 27 | 28 | return page; 29 | }; 30 | 31 | let newPage: any = Super.prototype.newPage; 32 | 33 | Super.prototype.newPage = async function (...hooks: Hook[]) { 34 | let page: Page = await newPage.apply(this, arguments); 35 | 36 | if (hooks != null && Array.isArray(hooks) === true) { 37 | for (let hook of hooks) { 38 | page = await hook(page); 39 | } 40 | } 41 | 42 | return page; 43 | }; 44 | -------------------------------------------------------------------------------- /source/puppeteer/lib/BrowserContext.ts: -------------------------------------------------------------------------------- 1 | import { BrowserContext, Page } from 'puppeteer-core'; 2 | import { Hook, Prototype } from '../../../typings/chrome-aws-lambda'; 3 | 4 | let Super: Prototype = null; 5 | 6 | try { 7 | Super = require('puppeteer/lib/cjs/puppeteer/common/Browser.js').BrowserContext; 8 | } catch (error) { 9 | Super = require('puppeteer-core/lib/cjs/puppeteer/common/Browser.js').BrowserContext; 10 | } 11 | 12 | Super.prototype.defaultPage = async function (...hooks: Hook[]) { 13 | let page: Page = null; 14 | let pages: Page[] = await this.pages(); 15 | 16 | if (pages.length === 0) { 17 | pages = [await this.newPage()]; 18 | } 19 | 20 | page = pages.shift(); 21 | 22 | if (hooks != null && Array.isArray(hooks) === true) { 23 | for (let hook of hooks) { 24 | page = await hook(page); 25 | } 26 | } 27 | 28 | return page; 29 | }; 30 | 31 | let newPage: any = Super.prototype.newPage; 32 | 33 | Super.prototype.newPage = async function (...hooks: Hook[]) { 34 | let page: Page = await newPage.apply(this, arguments); 35 | 36 | if (hooks != null && Array.isArray(hooks) === true) { 37 | for (let hook of hooks) { 38 | page = await hook(page); 39 | } 40 | } 41 | 42 | return page; 43 | }; 44 | -------------------------------------------------------------------------------- /source/puppeteer/lib/ElementHandle.ts: -------------------------------------------------------------------------------- 1 | import { ElementHandle, EvaluateFunc, HTTPRequest, HTTPResponse, Page, WaitForOptions, WaitTimeoutOptions } from 'puppeteer-core'; 2 | import { Prototype } from '../../../typings/chrome-aws-lambda'; 3 | 4 | let Super: Prototype = null; 5 | 6 | try { 7 | Super = require('puppeteer/lib/cjs/puppeteer/common/ElementHandle.js').ElementHandle; 8 | } catch (error) { 9 | Super = require('puppeteer-core/lib/cjs/puppeteer/common/ElementHandle.js').ElementHandle; 10 | } 11 | 12 | Super.prototype.clear = function () { 13 | return this.click({ clickCount: 3 }).then(() => this.press('Backspace')); 14 | }; 15 | 16 | Super.prototype.clickAndWaitForNavigation = function (options?: WaitForOptions) { 17 | options = options ?? { 18 | waitUntil: [ 19 | 'load', 20 | ], 21 | }; 22 | 23 | let promises: [Promise, Promise] = [ 24 | ((this as any)._page as Page).waitForNavigation(options), 25 | this.click(), 26 | ]; 27 | 28 | return Promise.all(promises).then((value) => value.shift() as HTTPResponse); 29 | }; 30 | 31 | Super.prototype.clickAndWaitForRequest = function (predicate: string | RegExp | ((request: HTTPRequest) => boolean | Promise), options?: WaitTimeoutOptions) { 32 | let callback = (request: HTTPRequest) => { 33 | let url = request.url(); 34 | 35 | if (typeof predicate === 'string' && predicate.includes('*') === true) { 36 | predicate = new RegExp(predicate.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/[*]+/g, '.*?'), 'g'); 37 | } 38 | 39 | if (predicate instanceof RegExp) { 40 | return predicate.test(url); 41 | } 42 | 43 | return predicate === url; 44 | }; 45 | 46 | let promises: [Promise, Promise] = [ 47 | ((this as any)._page as Page).waitForRequest((typeof predicate === 'function') ? predicate : callback, options), 48 | this.click(), 49 | ]; 50 | 51 | return Promise.all(promises).then((value) => value.shift() as HTTPRequest); 52 | }; 53 | 54 | Super.prototype.clickAndWaitForResponse = function (predicate: string | RegExp | ((request: HTTPResponse) => boolean | Promise), options?: WaitTimeoutOptions) { 55 | let callback = (request: HTTPResponse) => { 56 | let url = request.url(); 57 | 58 | if (typeof predicate === 'string' && predicate.includes('*') === true) { 59 | predicate = new RegExp(predicate.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/[*]+/g, '.*?'), 'g'); 60 | } 61 | 62 | if (predicate instanceof RegExp) { 63 | return predicate.test(url); 64 | } 65 | 66 | return predicate === url; 67 | }; 68 | 69 | let promises: [Promise, Promise] = [ 70 | ((this as any)._page as Page).waitForResponse((typeof predicate === 'function') ? predicate : callback, options), 71 | this.click(), 72 | ]; 73 | 74 | return Promise.all(promises).then((value) => value.shift() as HTTPResponse); 75 | }; 76 | 77 | Super.prototype.fillFormByLabel = function >(data: T) { 78 | let callback = (node: HTMLFormElement, data: T) => { 79 | if (node.nodeName.toLowerCase() !== 'form') { 80 | throw new Error('Element is not a
element.'); 81 | } 82 | 83 | let result: Record = {}; 84 | 85 | for (let [key, value] of Object.entries(data)) { 86 | let selector = [ 87 | `id(string(//label[normalize-space(.) = "${key}"]/@for))`, 88 | `//label[normalize-space(.) = "${key}"]//*[self::input or self::select or self::textarea]`, 89 | ].join(' | '); 90 | 91 | if (result.hasOwnProperty(key) !== true) { 92 | result[key] = []; 93 | } 94 | 95 | let element: Node = null; 96 | let elements: HTMLInputElement[] = []; 97 | let iterator = document.evaluate(selector, node, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); 98 | 99 | while ((element = iterator.iterateNext()) != null) { 100 | elements.push(element as HTMLInputElement); 101 | } 102 | 103 | if (elements.length === 0) { 104 | throw new Error(`No elements match the selector '${selector}' for '${key}'.`); 105 | } 106 | 107 | let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase(); 108 | let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[]; 109 | 110 | if (type === 'file') { 111 | throw new Error(`Input element of type 'file' is not supported.`); 112 | } 113 | 114 | for (let element of elements) { 115 | try { 116 | element.focus(); 117 | element.dispatchEvent(new Event('focus')); 118 | } catch (error) { 119 | } 120 | 121 | if (type === 'select') { 122 | element.value = undefined; 123 | 124 | for (let index of ['value', 'label'] as ['value', 'label']) { 125 | if (result[key].length > 0) { 126 | break; 127 | } 128 | 129 | for (let option of Array.from((element as unknown as HTMLSelectElement).options)) { 130 | option.selected = values.includes(option[index]); 131 | 132 | if (option.selected === true) { 133 | result[key].push(option.value); 134 | 135 | if (element.multiple !== true) { 136 | break; 137 | } 138 | } 139 | } 140 | } 141 | } else if (type === 'checkbox' || type === 'radio') { 142 | element.checked = (value === true) || values.includes(element.value); 143 | 144 | if (element.checked === true) { 145 | result[key].push(element.value); 146 | } 147 | } else if (typeof value === 'string') { 148 | if (element.isContentEditable === true) { 149 | result[key].push(element.textContent = value); 150 | } else { 151 | result[key].push(element.value = value); 152 | } 153 | } 154 | 155 | for (let trigger of ['input', 'change']) { 156 | element.dispatchEvent(new Event(trigger, { 'bubbles': true })); 157 | } 158 | 159 | try { 160 | element.blur(); 161 | element.dispatchEvent(new Event('blur')); 162 | } catch (error) { 163 | } 164 | 165 | if (type === 'checkbox' || type === 'radio') { 166 | break; 167 | } 168 | } 169 | } 170 | 171 | return result; 172 | }; 173 | 174 | return this.evaluate(callback as unknown as EvaluateFunc<[ElementHandle, T]>, data) as any; 175 | }; 176 | 177 | Super.prototype.fillFormByName = function >(data: T) { 178 | let callback = (node: HTMLFormElement, data: T, heuristic: 'css' | 'label' | 'name' | 'xpath' = 'css') => { 179 | if (node.nodeName.toLowerCase() !== 'form') { 180 | throw new Error('Element is not a element.'); 181 | } 182 | 183 | let result: Record = {}; 184 | 185 | for (let [key, value] of Object.entries(data)) { 186 | let selector = `[name="${key}"]`; 187 | 188 | if (result.hasOwnProperty(key) !== true) { 189 | result[key] = []; 190 | } 191 | 192 | let elements: HTMLInputElement[] = Array.from(node.querySelectorAll(selector)); 193 | 194 | if (elements.length === 0) { 195 | throw new Error(`No elements match the selector '${selector}' for '${key}'.`); 196 | } 197 | 198 | let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase(); 199 | let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[]; 200 | 201 | if (type === 'file') { 202 | throw new Error(`Input element of type 'file' is not supported.`); 203 | } 204 | 205 | for (let element of elements) { 206 | try { 207 | element.focus(); 208 | element.dispatchEvent(new Event('focus')); 209 | } catch (error) { 210 | } 211 | 212 | if (type === 'select') { 213 | element.value = undefined; 214 | 215 | for (let index of ['value', 'label'] as ['value', 'label']) { 216 | if (result[key].length > 0) { 217 | break; 218 | } 219 | 220 | for (let option of Array.from((element as unknown as HTMLSelectElement).options)) { 221 | option.selected = values.includes(option[index]); 222 | 223 | if (option.selected === true) { 224 | result[key].push(option.value); 225 | 226 | if (element.multiple !== true) { 227 | break; 228 | } 229 | } 230 | } 231 | } 232 | } else if (type === 'checkbox' || type === 'radio') { 233 | element.checked = (value === true) || values.includes(element.value); 234 | 235 | if (element.checked === true) { 236 | result[key].push(element.value); 237 | } 238 | } else if (typeof value === 'string') { 239 | if (element.isContentEditable === true) { 240 | result[key].push(element.textContent = value); 241 | } else { 242 | result[key].push(element.value = value); 243 | } 244 | } 245 | 246 | for (let trigger of ['input', 'change']) { 247 | element.dispatchEvent(new Event(trigger, { 'bubbles': true })); 248 | } 249 | 250 | try { 251 | element.blur(); 252 | element.dispatchEvent(new Event('blur')); 253 | } catch (error) { 254 | } 255 | 256 | if (type === 'checkbox' || type === 'radio') { 257 | break; 258 | } 259 | } 260 | } 261 | 262 | return result; 263 | }; 264 | 265 | return this.evaluate(callback as unknown as EvaluateFunc<[ElementHandle, T]>, data) as any; 266 | }; 267 | 268 | Super.prototype.fillFormBySelector = function >(data: T) { 269 | let callback = (node: HTMLFormElement, data: T, heuristic: 'css' | 'label' | 'name' | 'xpath' = 'css') => { 270 | if (node.nodeName.toLowerCase() !== 'form') { 271 | throw new Error('Element is not a element.'); 272 | } 273 | 274 | let result: Record = {}; 275 | 276 | for (let [key, value] of Object.entries(data)) { 277 | let selector = key; 278 | 279 | if (result.hasOwnProperty(key) !== true) { 280 | result[key] = []; 281 | } 282 | 283 | let elements: HTMLInputElement[] = Array.from(node.querySelectorAll(selector)); 284 | 285 | if (elements.length === 0) { 286 | throw new Error(`No elements match the selector '${selector}' for '${key}'.`); 287 | } 288 | 289 | let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase(); 290 | let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[]; 291 | 292 | if (type === 'file') { 293 | throw new Error(`Input element of type 'file' is not supported.`); 294 | } 295 | 296 | for (let element of elements) { 297 | try { 298 | element.focus(); 299 | element.dispatchEvent(new Event('focus')); 300 | } catch (error) { 301 | } 302 | 303 | if (type === 'select') { 304 | element.value = undefined; 305 | 306 | for (let index of ['value', 'label'] as ['value', 'label']) { 307 | if (result[key].length > 0) { 308 | break; 309 | } 310 | 311 | for (let option of Array.from((element as unknown as HTMLSelectElement).options)) { 312 | option.selected = values.includes(option[index]); 313 | 314 | if (option.selected === true) { 315 | result[key].push(option.value); 316 | 317 | if (element.multiple !== true) { 318 | break; 319 | } 320 | } 321 | } 322 | } 323 | } else if (type === 'checkbox' || type === 'radio') { 324 | element.checked = (value === true) || values.includes(element.value); 325 | 326 | if (element.checked === true) { 327 | result[key].push(element.value); 328 | } 329 | } else if (typeof value === 'string') { 330 | if (element.isContentEditable === true) { 331 | result[key].push(element.textContent = value); 332 | } else { 333 | result[key].push(element.value = value); 334 | } 335 | } 336 | 337 | for (let trigger of ['input', 'change']) { 338 | element.dispatchEvent(new Event(trigger, { 'bubbles': true })); 339 | } 340 | 341 | try { 342 | element.blur(); 343 | element.dispatchEvent(new Event('blur')); 344 | } catch (error) { 345 | } 346 | 347 | if (type === 'checkbox' || type === 'radio') { 348 | break; 349 | } 350 | } 351 | } 352 | 353 | return result; 354 | }; 355 | 356 | return this.evaluate(callback as unknown as EvaluateFunc<[ElementHandle, T]>, data) as any; 357 | }; 358 | 359 | Super.prototype.fillFormByXPath = function >(data: T) { 360 | let callback = (node: HTMLFormElement, data: T) => { 361 | if (node.nodeName.toLowerCase() !== 'form') { 362 | throw new Error('Element is not a element.'); 363 | } 364 | 365 | let result: Record = {}; 366 | 367 | for (let [key, value] of Object.entries(data)) { 368 | let selector = key; 369 | 370 | if (result.hasOwnProperty(key) !== true) { 371 | result[key] = []; 372 | } 373 | 374 | let element: Node = null; 375 | let elements: HTMLInputElement[] = []; 376 | let iterator = document.evaluate(selector, node, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); 377 | 378 | while ((element = iterator.iterateNext()) != null) { 379 | elements.push(element as HTMLInputElement); 380 | } 381 | 382 | if (elements.length === 0) { 383 | throw new Error(`No elements match the selector '${selector}' for '${key}'.`); 384 | } 385 | 386 | let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase(); 387 | let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[]; 388 | 389 | if (type === 'file') { 390 | throw new Error(`Input element of type 'file' is not supported.`); 391 | } 392 | 393 | for (let element of elements) { 394 | try { 395 | element.focus(); 396 | element.dispatchEvent(new Event('focus')); 397 | } catch (error) { 398 | } 399 | 400 | if (type === 'select') { 401 | element.value = undefined; 402 | 403 | for (let index of ['value', 'label'] as ['value', 'label']) { 404 | if (result[key].length > 0) { 405 | break; 406 | } 407 | 408 | for (let option of Array.from((element as unknown as HTMLSelectElement).options)) { 409 | option.selected = values.includes(option[index]); 410 | 411 | if (option.selected === true) { 412 | result[key].push(option.value); 413 | 414 | if (element.multiple !== true) { 415 | break; 416 | } 417 | } 418 | } 419 | } 420 | } else if (type === 'checkbox' || type === 'radio') { 421 | element.checked = (value === true) || values.includes(element.value); 422 | 423 | if (element.checked === true) { 424 | result[key].push(element.value); 425 | } 426 | } else if (typeof value === 'string') { 427 | if (element.isContentEditable === true) { 428 | result[key].push(element.textContent = value); 429 | } else { 430 | result[key].push(element.value = value); 431 | } 432 | } 433 | 434 | for (let trigger of ['input', 'change']) { 435 | element.dispatchEvent(new Event(trigger, { 'bubbles': true })); 436 | } 437 | 438 | try { 439 | element.blur(); 440 | element.dispatchEvent(new Event('blur')); 441 | } catch (error) { 442 | } 443 | 444 | if (type === 'checkbox' || type === 'radio') { 445 | break; 446 | } 447 | } 448 | } 449 | 450 | return result; 451 | }; 452 | 453 | return this.evaluate(callback as unknown as EvaluateFunc<[ElementHandle, T]>, data) as any; 454 | }; 455 | 456 | Super.prototype.getInnerHTML = function () { 457 | return this.evaluate((node: Element) => { 458 | return (node as HTMLElement).innerHTML; 459 | }); 460 | }; 461 | 462 | Super.prototype.getInnerText = function () { 463 | return this.evaluate((node: Element) => { 464 | return (node as HTMLElement).innerText; 465 | }); 466 | }; 467 | 468 | Super.prototype.number = function (decimal: string = '.', property: any) { 469 | let callback = (node: any, decimal: string, property: any) => { 470 | let data = (node[property] as unknown) as string; 471 | 472 | if (typeof data === 'string') { 473 | decimal = decimal ?? '.'; 474 | 475 | if (typeof decimal === 'string') { 476 | decimal = decimal.replace(/[.]/g, '\\$&'); 477 | } 478 | 479 | let matches = data.match(/((?:[-+]|\b)[0-9]+(?:[ ,.'`´]*[0-9]+)*)\b/g); 480 | 481 | if (matches != null) { 482 | return matches.map((value) => parseFloat(value.replace(new RegExp(`[^-+0-9${decimal}]+`, 'g'), '').replace(decimal, '.'))); 483 | } 484 | } 485 | 486 | return null; 487 | }; 488 | 489 | return this.evaluate(callback, decimal, property as any); 490 | }; 491 | 492 | Super.prototype.selectByLabel = function (...values: string[]) { 493 | for (let value of values) { 494 | console.assert(typeof value === 'string', `Values must be strings. Found value '${value}' of type '${typeof value}'.`); 495 | } 496 | 497 | let callback = (node: HTMLSelectElement, values: string[]) => { 498 | if (node.nodeName.toLowerCase() !== 'select') { 499 | throw new Error('Element is not a