Initial Fork from chrome-aws-lambda

For repo history, please see: abdc140008
This commit is contained in:
Sparticuz 2022-09-26 15:05:08 -04:00
commit 034e9e0d56
42 changed files with 3436 additions and 0 deletions

19
.editorconfig Normal file
View File

@ -0,0 +1,19 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.json]
indent_size = 2
indent_style = space
[Makefile]
indent_size = 4
indent_style = tab

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github:Sparticuz

69
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@ -0,0 +1,69 @@
---
name: Bug Report
about: Standard Bug Report
title: "[BUG]"
labels: bug
---
<!---
For Chromium-specific bugs, please refer to: https://bugs.chromium.org/p/chromium
For Puppeteer-specific bugs, please refer to: https://github.com/GoogleChrome/puppeteer/issues
-->
## Environment
* `chrome-aws-lambda` Version:
* `puppeteer` / `puppeteer-core` Version:
* OS: <!-- Linux | Mac | Windows -->
* Node.js Version: <!-- 8.x | 10.x | 12.x | 14.x -->
* Lambda / GCF Runtime: <!-- `nodejs8.10` | `nodejs10.x` | `nodejs12.x` -->
## Expected Behavior
<!-- What should have happened. -->
## Current Behavior
<!-- What happened instead. -->
## Steps to Reproduce
<!-- Include code and/or URLs to reproduce this issue. -->
<!--
```js
const chromium = require('chrome-aws-lambda');
exports.handler = async (event, context, callback) => {
let result = null;
let browser = null;
try {
browser = await chromium.puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath,
headless: chromium.headless,
ignoreHTTPSErrors: true,
});
let page = await browser.newPage();
await page.goto(event.url || 'https://example.com');
result = await page.title();
} catch (error) {
return callback(error);
} finally {
if (browser !== null) {
await browser.close();
}
}
return callback(null, result);
};
```
-->
## Possible Solution
<!-- Not mandatory, but you can suggest a fix or reason for the bug. -->

View File

@ -0,0 +1,14 @@
---
name: Feature Request
about: Suggest an Idea or Improvement
title: "[REQUEST]"
labels: enhancement
---
## What would you like to have implemented?
<!--- Clearly describe what feature you'd like to see implemented and why. -->
## Why would it be useful?
<!--- Optional: Provide any use cases, code or screenshots that support this feature request. -->

66
.github/workflows/aws.yml vendored Normal file
View File

@ -0,0 +1,66 @@
name: AWS Lambda CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
name: Build Lambda Layer
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16.x
- name: Install Packages
run: npm install
- name: Create Lambda Layer
run: make chrome_aws_lambda.zip
- name: Upload Layer Artifact
uses: actions/upload-artifact@v3
with:
name: chrome_aws_lambda
path: chrome_aws_lambda.zip
execute:
name: Lambda (Node ${{ matrix.version }}.x)
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
event:
- example.com
version:
- 14
- 16
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Setup AWS SAM CLI
uses: aws-actions/setup-sam@v2
- name: Download Layer Artifact
uses: actions/download-artifact@v3
with:
name: chrome_aws_lambda
- name: Provision Layer
run: unzip chrome_aws_lambda.zip -d _/amazon/code
- name: Invoke Lambda on SAM
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)

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
.fonts
.idea
*.log
*.pem
*.pem.pub
*.zip
bin/chromium-*.br
build
node_modules
nodejs
package-lock.json
_/amazon/samconfig.toml
_/amazon/.aws-sam

5
.npmignore Normal file
View File

@ -0,0 +1,5 @@
_
.fonts
.idea
*.zip
Dockerfile

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2018 Alix Axel
Copyright (c) 2022 Kyle McNally
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

27
Makefile Normal file
View File

@ -0,0 +1,27 @@
.PHONY: clean
clean:
rm -rf chrome_aws_lambda.zip _/amazon/code/nodejs
pretest:
unzip chrome_aws_lambda.zip -d _/amazon/code
test:
sam local invoke --template _/amazon/template.yml --event _/amazon/events/example.com.json node16
.fonts.zip:
zip -9 --filesync --move --recurse-paths .fonts.zip .fonts/
%.zip:
npm install --fund=false --package-lock=false
mkdir -p nodejs
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
npm pack
mkdir -p nodejs/node_modules/@sparticuz/chrome-aws-lambda/
tar --directory nodejs/node_modules/@sparticuz/chrome-aws-lambda/ --extract --file sparticuz-chrome-aws-lambda-*.tgz --strip-components=1
npx clean-modules --directory nodejs --include "**/*.d.ts" "**/@types/**" "**/*.@(yaml|yml)" --yes
rm sparticuz-chrome-aws-lambda-*.tgz
mkdir -p $(dir $@)
zip -9 --filesync --move --recurse-paths $@ nodejs
.DEFAULT_GOAL := chrome_aws_lambda.zip

379
README.md Normal file
View File

@ -0,0 +1,379 @@
# chrome-aws-lambda
[![@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)
[![TypeScript](https://img.shields.io/npm/types/chrome-aws-lambda?style=for-the-badge)](https://www.typescriptlang.org/dt/search?search=chrome-aws-lambda)
[![Chromium](https://img.shields.io/badge/chromium-48_MB-brightgreen.svg?style=for-the-badge)](bin/)
[![Donate](https://img.shields.io/badge/donate-paypal-orange.svg?style=for-the-badge)](https://paypal.me/sparticuz)
Chromium Binary for AWS Lambda and Google Cloud Functions
### Difference from alixaxel/chrome-aws-lambda
This fork was born out of [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.
## Install
```shell
npm install @sparticuz/chrome-aws-lambda --save-prod
```
This will ship with appropriate binary for the latest stable release of [`puppeteer`](https://github.com/GoogleChrome/puppeteer) (usually updated within a few days).
You also need to install the corresponding version of `puppeteer-core` (or `puppeteer`):
```shell
npm install puppeteer-core --save-prod
```
If you wish to install an older version of Chromium, take a look at [Versioning](https://github.com/Sparticuz/chrome-aws-lambda#versioning).
## Usage
This package works with all the currently supported AWS Lambda Node.js runtimes out of the box.
```javascript
const chromium = require('@sparticuz/chrome-aws-lambda');
exports.handler = async (event, context, callback) => {
let result = null;
let browser = null;
try {
browser = await chromium.puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath,
headless: chromium.headless,
ignoreHTTPSErrors: true,
});
let page = await browser.newPage();
await page.goto(event.url || 'https://example.com');
result = await page.title();
} catch (error) {
return callback(error);
} finally {
if (browser !== null) {
await browser.close();
}
}
return callback(null, result);
};
```
### Usage with Playwright
```javascript
const chromium = require('@sparticuz/chrome-aws-lambda');
const playwright = require('playwright-core');
(async () => {
const browser = await playwright.chromium.launch({
args: chromium.args,
executablePath: await chromium.executablePath,
headless: chromium.headless,
});
// ...
await browser.close();
})();
```
You should allocate at least 512 MB of RAM to your Lambda, however 1600 MB (or more) is recommended.
### Running Locally
Please refer to the [Local Development Wiki page](https://github.com/alixaxel/chrome-aws-lambda/wiki/HOWTO:-Local-Development) for instructions and troubleshooting.
## API
| Method / Property | Returns | Description |
| ----------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `font(url)` | `{?Promise<string>}` | Provisions a custom font and returns its basename. |
| `args` | `{!Array<string>}` | Provides a list of recommended additional [Chromium flags](https://github.com/GoogleChrome/chrome-launcher/blob/master/docs/chrome-flags-for-tools.md). |
| `defaultViewport` | `{!Object}` | Returns more sensible default viewport settings. |
| `executablePath` | `{?Promise<string>}` | Returns the path the Chromium binary was extracted to. |
| `headless` | `{!boolean}` | Returns `true` if we are running on AWS Lambda or GCF. |
| `puppeteer` | `{!Object}` | Overloads `puppeteer` and returns the resolved package. |
## Fonts
The Amazon Linux 2 AWS Lambda runtime is no longer provisioned with any font faces.
Because of this, this package ships with [Open Sans](https://fonts.google.com/specimen/Open+Sans), which supports the following scripts:
* Latin
* Greek
* Cyrillic
To provision additional fonts, simply call the `font()` method with an absolute path or URL:
```typescript
await chromium.font('/var/task/fonts/NotoColorEmoji.ttf');
// or
await chromium.font('https://raw.githack.com/googlei18n/noto-emoji/master/fonts/NotoColorEmoji.ttf');
```
> `Noto Color Emoji` (or similar) is needed if you want to [render emojis](https://getemoji.com/).
> For URLs, it's recommended that you use a CDN, like [raw.githack.com](https://raw.githack.com/) or [gitcdn.xyz](https://gitcdn.xyz/).
This method should be invoked _before_ launching Chromium.
> On non-serverless environments, the `font()` method is a no-op to avoid polluting the user space.
---
Alternatively, it's also possible to provision fonts via AWS Lambda Layers.
Simply create a directory named `.fonts` and place any font faces you want there:
```
.fonts
├── NotoColorEmoji.ttf
└── Roboto.ttf
```
Afterwards, you just need to ZIP the directory and upload it as a AWS Lambda Layer:
```shell
zip -9 --filesync --move --recurse-paths .fonts.zip .fonts/
```
## Overloading
Since version `8.0.0`, it's possible to [overload puppeteer](/typings/chrome-aws-lambda.d.ts) with the following convenient API:
```typescript
interface Browser {
defaultPage(...hooks: ((page: Page) => Promise<Page>)[])
newPage(...hooks: ((page: Page) => Promise<Page>)[])
}
interface BrowserContext {
defaultPage(...hooks: ((page: Page) => Promise<Page>)[])
newPage(...hooks: ((page: Page) => Promise<Page>)[])
}
interface Page {
block(patterns: string[])
clear(selector: string)
clickAndWaitForNavigation(selector: string, options?: WaitForOptions)
clickAndWaitForRequest(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions)
clickAndWaitForRequest(selector: string, predicate: ((request: HTTPRequest) => boolean | Promise<boolean>), options?: WaitTimeoutOptions)
clickAndWaitForResponse(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions)
clickAndWaitForResponse(selector: string, predicate: ((request: HTTPResponse) => boolean | Promise<boolean>), options?: WaitTimeoutOptions)
count(selector: string)
exists(selector: string)
fillFormByLabel(selector: string, data: Record<string, boolean | string | string[]>)
fillFormByName(selector: string, data: Record<string, boolean | string | string[]>)
fillFormBySelector(selector: string, data: Record<string, boolean | string | string[]>)
fillFormByXPath(selector: string, data: Record<string, boolean | string | string[]>)
number(selector: string, decimal?: string, property?: string)
selectByLabel(selector: string, ...values: string[])
string(selector: string, property?: string)
waitForInflightRequests(requests?: number, alpha: number, omega: number, options?: WaitTimeoutOptions)
waitForText(predicate: string, options?: WaitTimeoutOptions)
waitUntilVisible(selector: string, options?: WaitTimeoutOptions)
waitWhileVisible(selector: string, options?: WaitTimeoutOptions)
withTracing(options: TracingOptions, callback: (page: Page) => Promise<any>)
}
interface Frame {
clear(selector: string)
clickAndWaitForNavigation(selector: string, options?: WaitForOptions)
clickAndWaitForRequest(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions)
clickAndWaitForRequest(selector: string, predicate: ((request: HTTPRequest) => boolean | Promise<boolean>), options?: WaitTimeoutOptions)
clickAndWaitForResponse(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions)
clickAndWaitForResponse(selector: string, predicate: ((request: HTTPResponse) => boolean | Promise<boolean>), options?: WaitTimeoutOptions)
count(selector: string)
exists(selector: string)
fillFormByLabel(selector: string, data: Record<string, boolean | string | string[]>)
fillFormByName(selector: string, data: Record<string, boolean | string | string[]>)
fillFormBySelector(selector: string, data: Record<string, boolean | string | string[]>)
fillFormByXPath(selector: string, data: Record<string, boolean | string | string[]>)
number(selector: string, decimal?: string, property?: string)
selectByLabel(selector: string, ...values: string[])
string(selector: string, property?: string)
waitForText(predicate: string, options?: WaitTimeoutOptions)
waitUntilVisible(selector: string, options?: WaitTimeoutOptions)
waitWhileVisible(selector: string, options?: WaitTimeoutOptions)
}
interface ElementHandle {
clear()
clickAndWaitForNavigation(options?: WaitForOptions)
clickAndWaitForRequest(predicate: string | RegExp, options?: WaitTimeoutOptions)
clickAndWaitForRequest(predicate: ((request: HTTPRequest) => boolean | Promise<boolean>), options?: WaitTimeoutOptions)
clickAndWaitForResponse(predicate: string | RegExp, options?: WaitTimeoutOptions)
clickAndWaitForResponse(predicate: ((request: HTTPResponse) => boolean | Promise<boolean>), options?: WaitTimeoutOptions)
fillFormByLabel(data: Record<string, boolean | string | string[]>)
fillFormByName(data: Record<string, boolean | string | string[]>)
fillFormBySelector(data: Record<string, boolean | string | string[]>)
fillFormByXPath(data: Record<string, boolean | string | string[]>)
getInnerHTML()
getInnerText()
number(decimal?: string, property?: string)
selectByLabel(...values: string[])
string(property?: string)
}
```
To enable this behavior, simply call the `puppeteer` property exposed by this package.
> Refer to the [TypeScript typings](/typings/chrome-aws-lambda.d.ts) for general documentation.
## Page Hooks
When overloaded, you can specify a list of hooks to automatically apply to pages.
For instance, to remove the `Headless` substring from the user agent:
```typescript
async function replaceUserAgent(page: Page): Promise<Page> {
let value = await page.browser().userAgent();
if (value.includes('Headless') === true) {
await page.setUserAgent(value.replace('Headless', ''));
}
return page;
}
```
And then simply pass that page hook to `defaultPage()` or `newPage()`:
```typescript
let page = await browser.defaultPage(replaceUserAgent);
```
> Additional bundled page hooks can be found on [`/build/hooks`](/source/hooks).
## Versioning
This package is versioned based on the underlying `puppeteer` minor version:
| `puppeteer` Version | `chrome-aws-lambda` Version | Chromium Revision |
| ------------------- | --------------------------------------------- | ------------------------------------------------------- |
| `17.1.*` | `npm i @sparticuz/chrome-aws-lambda@~17.1.1` | [`1036745`](https://crrev.com/1036745) (`106.0.5249.0`) |
| `16.1.*` | `npm i @sparticuz/chrome-aws-lambda@~16.1.0` | [`1022525`](https://crrev.com/1011831) (`105.0.5173.0`) |
| `15.5.*` | `npm i @sparticuz/chrome-aws-lambda@~15.5.0` | [`1022525`](https://crrev.com/1011831) (`105.0.5173.0`) |
| `14.4.*` | `npm i @sparticuz/chrome-aws-lambda@~14.4.1` | [`1002410`](https://crrev.com/1002410) (`103.0.5058.0`) |
| `14.3.*` | `npm i @sparticuz/chrome-aws-lambda@~14.3.0` | [`1002410`](https://crrev.com/1002410) (`103.0.5058.0`) |
| `14.2.*` | `npm i @sparticuz/chrome-aws-lambda@~14.2.0` | [`1002410`](https://crrev.com/1002410) (`103.0.5058.0`) |
| `14.1.*` | `npm i @sparticuz/chrome-aws-lambda@~14.1.1` | [`991974`](https://crrev.com/991974) (`102.0.5002.0`) |
| `10.1.*` | `npm i chrome-aws-lambda@~10.1.0` | [`884014`](https://crrev.com/884014) (`92.0.4512.0`) |
| `10.0.*` | `npm i chrome-aws-lambda@~10.0.0` | [`884014`](https://crrev.com/884014) (`92.0.4512.0`) |
| `9.1.*` | `npm i chrome-aws-lambda@~9.1.0` | [`869685`](https://crrev.com/869685) (`91.0.4469.0`) |
| `9.0.*` | `npm i chrome-aws-lambda@~9.0.0` | [`869685`](https://crrev.com/869685) (`91.0.4469.0`) |
| `8.0.*` | `npm i chrome-aws-lambda@~8.0.2` | [`856583`](https://crrev.com/856583) (`90.0.4427.0`) |
| `7.0.*` | `npm i chrome-aws-lambda@~7.0.0` | [`848005`](https://crrev.com/848005) (`90.0.4403.0`) |
| `6.0.*` | `npm i chrome-aws-lambda@~6.0.0` | [`843427`](https://crrev.com/843427) (`89.0.4389.0`) |
| `5.5.*` | `npm i chrome-aws-lambda@~5.5.0` | [`818858`](https://crrev.com/818858) (`88.0.4298.0`) |
| `5.4.*` | `npm i chrome-aws-lambda@~5.4.0` | [`809590`](https://crrev.com/809590) (`87.0.4272.0`) |
| `5.3.*` | `npm i chrome-aws-lambda@~5.3.1` | [`800071`](https://crrev.com/800071) (`86.0.4240.0`) |
| `5.2.*` | `npm i chrome-aws-lambda@~5.2.1` | [`782078`](https://crrev.com/782078) (`85.0.4182.0`) |
| `5.1.*` | `npm i chrome-aws-lambda@~5.1.0` | [`768783`](https://crrev.com/768783) (`84.0.4147.0`) |
| `5.0.*` | `npm i chrome-aws-lambda@~5.0.0` | [`756035`](https://crrev.com/756035) (`83.0.4103.0`) |
| `3.1.*` | `npm i chrome-aws-lambda@~3.1.1` | [`756035`](https://crrev.com/756035) (`83.0.4103.0`) |
| `3.0.*` | `npm i chrome-aws-lambda@~3.0.4` | [`737027`](https://crrev.com/737027) (`81.0.4044.0`) |
| `2.1.*` | `npm i chrome-aws-lambda@~2.1.1` | [`722234`](https://crrev.com/722234) (`80.0.3987.0`) |
| `2.0.*` | `npm i chrome-aws-lambda@~2.0.2` | [`705776`](https://crrev.com/705776) (`79.0.3945.0`) |
| `1.20.*` | `npm i chrome-aws-lambda@~1.20.4` | [`686378`](https://crrev.com/686378) (`78.0.3882.0`) |
| `1.19.*` | `npm i chrome-aws-lambda@~1.19.0` | [`674921`](https://crrev.com/674921) (`77.0.3844.0`) |
| `1.18.*` | `npm i chrome-aws-lambda@~1.18.1` | [`672088`](https://crrev.com/672088) (`77.0.3835.0`) |
| `1.18.*` | `npm i chrome-aws-lambda@~1.18.0` | [`669486`](https://crrev.com/669486) (`77.0.3827.0`) |
| `1.17.*` | `npm i chrome-aws-lambda@~1.17.1` | [`662092`](https://crrev.com/662092) (`76.0.3803.0`) |
| `1.16.*` | `npm i chrome-aws-lambda@~1.16.1` | [`656675`](https://crrev.com/656675) (`76.0.3786.0`) |
| `1.15.*` | `npm i chrome-aws-lambda@~1.15.1` | [`650583`](https://crrev.com/650583) (`75.0.3765.0`) |
| `1.14.*` | `npm i chrome-aws-lambda@~1.14.0` | [`641577`](https://crrev.com/641577) (`75.0.3738.0`) |
| `1.13.*` | `npm i chrome-aws-lambda@~1.13.0` | [`637110`](https://crrev.com/637110) (`74.0.3723.0`) |
| `1.12.*` | `npm i chrome-aws-lambda@~1.12.2` | [`624492`](https://crrev.com/624492) (`73.0.3679.0`) |
| `1.11.*` | `npm i chrome-aws-lambda@~1.11.2` | [`609904`](https://crrev.com/609904) (`72.0.3618.0`) |
| `1.10.*` | `npm i chrome-aws-lambda@~1.10.1` | [`604907`](https://crrev.com/604907) (`72.0.3582.0`) |
| `1.9.*` | `npm i chrome-aws-lambda@~1.9.1` | [`594312`](https://crrev.com/594312) (`71.0.3563.0`) |
| `1.8.*` | `npm i chrome-aws-lambda@~1.8.0` | [`588429`](https://crrev.com/588429) (`71.0.3542.0`) |
| `1.7.*` | `npm i chrome-aws-lambda@~1.7.0` | [`579032`](https://crrev.com/579032) (`70.0.3508.0`) |
| `1.6.*` | `npm i chrome-aws-lambda@~1.6.3` | [`575458`](https://crrev.com/575458) (`69.0.3494.0`) |
| `1.5.*` | `npm i chrome-aws-lambda@~1.5.0` | [`564778`](https://crrev.com/564778) (`69.0.3452.0`) |
| `1.4.*` | `npm i chrome-aws-lambda@~1.4.0` | [`555668`](https://crrev.com/555668) (`68.0.3419.0`) |
| `1.3.*` | `npm i chrome-aws-lambda@~1.3.0` | [`549031`](https://crrev.com/549031) (`67.0.3391.0`) |
| `1.2.*` | `npm i chrome-aws-lambda@~1.2.0` | [`543305`](https://crrev.com/543305) (`67.0.3372.0`) |
| `1.1.*` | `npm i chrome-aws-lambda@~1.1.0` | [`536395`](https://crrev.com/536395) (`66.0.3347.0`) |
| `1.0.*` | `npm i chrome-aws-lambda@~1.0.0` | [`526987`](https://crrev.com/526987) (`65.0.3312.0`) |
| `0.13.*` | `npm i chrome-aws-lambda@~0.13.0` | [`515411`](https://crrev.com/515411) (`64.0.3264.0`) |
Patch versions are reserved for bug fixes in `chrome-aws-lambda` and general maintenance.
## Compiling
To compile your own version of Chromium check the [Ansible playbook instructions](_/ansible).
## AWS Lambda Layer
[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.
The following set of (Linux) commands will create a layer of this package alongside `puppeteer-core`:
```shell
git clone --depth=1 https://github.com/alixaxel/chrome-aws-lambda.git && \
cd chrome-aws-lambda && \
make chrome_aws_lambda.zip
```
The above will create a `chrome-aws-lambda.zip` file, which can be uploaded to your Layers console.
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).
## Google Cloud Functions
Since version `1.11.2`, it's also possible to use this package on Google/Firebase Cloud Functions.
According to our benchmarks, it's 40% to 50% faster than using the off-the-shelf `puppeteer` bundle.
## Compression
The Chromium binary is compressed using the Brotli algorithm.
This allows us to get the best compression ratio and faster decompression times.
| File | Algorithm | Level | Bytes | MiB | % | Inflation |
| ------------- | --------- | ----- | --------- | --------- | ---------- | ---------- |
| `chromium` | - | - | 136964856 | 130.62 | - | - |
| `chromium.gz` | Gzip | 1 | 51662087 | 49.27 | 62.28% | 1.035s |
| `chromium.gz` | Gzip | 2 | 50438352 | 48.10 | 63.17% | 1.016s |
| `chromium.gz` | Gzip | 3 | 49428459 | 47.14 | 63.91% | 0.968s |
| `chromium.gz` | Gzip | 4 | 47873978 | 45.66 | 65.05% | 0.950s |
| `chromium.gz` | Gzip | 5 | 46929422 | 44.76 | 65.74% | 0.938s |
| `chromium.gz` | Gzip | 6 | 46522529 | 44.37 | 66.03% | 0.919s |
| `chromium.gz` | Gzip | 7 | 46406406 | 44.26 | 66.12% | 0.917s |
| `chromium.gz` | Gzip | 8 | 46297917 | 44.15 | 66.20% | 0.916s |
| `chromium.gz` | Gzip | 9 | 46270972 | 44.13 | 66.22% | 0.968s |
| `chromium.gz` | Zopfli | 10 | 45089161 | 43.00 | 67.08% | 0.919s |
| `chromium.gz` | Zopfli | 20 | 45085868 | 43.00 | 67.08% | 0.919s |
| `chromium.gz` | Zopfli | 30 | 45085003 | 43.00 | 67.08% | 0.925s |
| `chromium.gz` | Zopfli | 40 | 45084328 | 43.00 | 67.08% | 0.921s |
| `chromium.gz` | Zopfli | 50 | 45084098 | 43.00 | 67.08% | 0.935s |
| `chromium.br` | Brotli | 0 | 55401211 | 52.83 | 59.55% | 0.778s |
| `chromium.br` | Brotli | 1 | 54429523 | 51.91 | 60.26% | 0.757s |
| `chromium.br` | Brotli | 2 | 46436126 | 44.28 | 66.10% | 0.659s |
| `chromium.br` | Brotli | 3 | 46122033 | 43.99 | 66.33% | 0.616s |
| `chromium.br` | Brotli | 4 | 45050239 | 42.96 | 67.11% | 0.692s |
| `chromium.br` | Brotli | 5 | 40813510 | 38.92 | 70.20% | **0.598s** |
| `chromium.br` | Brotli | 6 | 40116951 | 38.26 | 70.71% | 0.601s |
| `chromium.br` | Brotli | 7 | 39302281 | 37.48 | 71.30% | 0.615s |
| `chromium.br` | Brotli | 8 | 39038303 | 37.23 | 71.50% | 0.668s |
| `chromium.br` | Brotli | 9 | 38853994 | 37.05 | 71.63% | 0.673s |
| `chromium.br` | Brotli | 10 | 36090087 | 34.42 | 73.65% | 0.765s |
| `chromium.br` | Brotli | 11 | 34820408 | **33.21** | **74.58%** | 0.712s |
## License
MIT

0
_/amazon/code/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,23 @@
[
{
"url": "https://example.com",
"expected": {
"title": "Example Domain",
"screenshot": "72e10960dcf78c864f3d3635e3beb5be394daf40"
}
},
{
"url": "https://example.com",
"expected": {
"title": "Example Domain",
"screenshot": "72e10960dcf78c864f3d3635e3beb5be394daf40"
}
},
{
"url": "https://get.webgl.org",
"expected": {
"remove": "logo-container",
"screenshot": "25ac96a4e44f338f5362c18da2b2823ee599c330"
}
}
]

View File

@ -0,0 +1,57 @@
const { ok } = require('assert');
const { createHash } = require('crypto');
const chromium = require('@sparticuz/chrome-aws-lambda');
exports.handler = async (event, context) => {
let browser = null;
try {
const browser = await chromium.puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath,
headless: chromium.headless,
ignoreHTTPSErrors: true,
});
const contexts = [
browser.defaultBrowserContext(),
];
while (contexts.length < event.length) {
contexts.push(await browser.createIncognitoBrowserContext());
}
for (let context of contexts) {
const job = event.shift();
const page = await context.defaultPage();
if (job.hasOwnProperty('url') === true) {
await page.goto(job.url, { waitUntil: ['domcontentloaded', 'load'] });
if (job.hasOwnProperty('expected') === true) {
if (job.expected.hasOwnProperty('title') === true) {
ok(await page.title() === job.expected.title, `Title assertion failed.`);
}
if (job.expected.hasOwnProperty('screenshot') === true) {
if (job.expected.hasOwnProperty('remove') === true ) {
await page.evaluate((selector) => {
document.getElementById(selector).remove();
}, job.expected.remove);
}
ok(createHash('sha1').update((await page.screenshot()).toString('base64')).digest('hex') === job.expected.screenshot, `Screenshot assertion failed.`);
}
}
}
}
} catch (error) {
throw error.message;
} finally {
if (browser !== null) {
await browser.close();
}
}
return true;
};

39
_/amazon/template.yml Normal file
View File

@ -0,0 +1,39 @@
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
MemorySize: 2048
Timeout: 30
Resources:
layer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: sparticuz-chrome-aws-lambda
ContentUri: code/
CompatibleRuntimes:
- nodejs14.x
- nodejs16.x
node14:
Type: AWS::Serverless::Function
Properties:
Layers:
- !Ref layer
Handler: handlers/index.handler
Runtime: nodejs14.x
Policies:
- AWSLambdaBasicExecutionRole
- AWSXRayDaemonWriteAccess
Tracing: Active
node16:
Type: AWS::Serverless::Function
Properties:
Layers:
- !Ref layer
Handler: handlers/index.handler
Runtime: nodejs16.x
Policies:
- AWSLambdaBasicExecutionRole
- AWSXRayDaemonWriteAccess
Tracing: Active

9
_/ansible/Makefile Normal file
View File

@ -0,0 +1,9 @@
.PHONY: ansible chromium
dependencies:
sudo apt install python3-pip zip
pip install ansible boto boto3 aws-sam-cli
echo "Docker is also required in order to test the package, please install docker or Docker Desktop"
chromium:
ansible-playbook plays/chromium.yml -i inventory.ini

31
_/ansible/README.md Normal file
View File

@ -0,0 +1,31 @@
# Chromium Playbook
This Ansible playbook will launch an EC2 `c6a.12xlarge` Spot Instance and compile Chromium statically.
Once the compilation finishes, the binary will be compressed with Brotli and downloaded.
The whole process usually takes around 1 hour to on a `c6a.12xlarge` instance.
## Chromium Version
To compile a specific version of Chromium, update the `puppeteer_version` variable in the Ansible inventory, i.e.:
```shell
puppeteer_version=v1.9.0
```
If not specified, the current `main` will be used.
## Usage
```shell
AWS_REGION=us-east-1 \
AWS_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXX \
AWS_SECRET_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
make chromium
```
## Requirements
- [Ansible](http://docs.ansible.com/ansible/latest/intro_installation.html#latest-releases-via-apt-ubuntu)
- AWS SDK for Python (`boto` and `boto3`)

8
_/ansible/ansible.cfg Normal file
View File

@ -0,0 +1,8 @@
[defaults]
hash_behaviour = merge
host_key_checking = false
retry_files_enabled = false
[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60 -o ServerAliveInterval=30
pipelining = true

17
_/ansible/inventory.ini Normal file
View File

@ -0,0 +1,17 @@
[localhost]
127.0.0.1
[localhost:vars]
ansible_connection=local
ansible_python_interpreter=python
image=ami-0309aede310b9cc1f
region=us-east-1
instance_size=c6a.8xlarge
[aws]
[aws:vars]
ansible_connection=ssh
ansible_python_interpreter=auto_silent
ansible_ssh_private_key_file=ansible.pem
puppeteer_version=v17.1.3

11
_/ansible/plays/.gclient Normal file
View File

@ -0,0 +1,11 @@
solutions = [
{
"name": "src",
"url": "https://chromium.googlesource.com/chromium/src.git",
"managed": False,
"custom_deps": {},
"custom_vars": {
"checkout_pgo_profiles": True,
},
},
]

View File

@ -0,0 +1,383 @@
---
- name: Bootstrap AWS
hosts: localhost
gather_facts: false
tasks:
- name: Creating SSH Key
shell: |
ssh-keygen -b 2048 -t rsa -f ansible.pem -q -N '' && \
chmod 0600 ansible.pem.pub
args:
chdir: ..
creates: ansible.pem
- name: Creating EC2 Key Pair
amazon.aws.ec2_key:
name: ansible
state: present
region: "{{ region }}"
key_material: "{{ item }}"
with_file: ../ansible.pem.pub
- name: Creating Security Group
amazon.aws.ec2_group:
name: Chromium
description: SSH Access
state: present
region: "{{ region }}"
rules:
- proto: tcp
to_port: 22
from_port: 22
cidr_ip: 0.0.0.0/0
rules_egress:
- proto: all
cidr_ip: 0.0.0.0/0
- name: Request EC2 Instance
amazon.aws.ec2_instance:
count: 1
ebs_optimized: yes
image:
id: "{{ image }}"
instance_initiated_shutdown_behavior: terminate
instance_type: "{{ instance_size }}"
key_name: ansible
network:
assign_public_ip: yes
delete_on_termination: yes
groups: Chromium
region: "{{ region }}"
security_group: Chromium
state: present
tags:
Name: Chromium
volumes:
- device_name: /dev/xvda
ebs:
delete_on_termination: true
volume_type: io2
volume_size: 128
iops: 3000
register: ec2
- name: Registering Host
add_host:
hostname: "{{ item.public_ip_address }}"
groupname: aws
with_items: "{{ ec2.instances }}"
- name: Waiting for SSH
wait_for:
host: "{{ item.public_ip_address }}"
port: 22
timeout: 120
state: started
with_items: "{{ ec2.instances }}"
- name: AWS
user: ec2-user
hosts: aws
gather_facts: true
environment:
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
PATH: "{{ ansible_env.PATH }}:/srv/source/depot_tools"
tasks:
- name: Update system
become: true
become_user: root
shell: |
dnf update --releasever=2022.0.20220831 -y
- name: Installing Packages
become: true
become_user: root
dnf:
name:
- "@Development Tools"
- alsa-lib-devel
- atk-devel
- bc
- bluez-libs-devel
- bzip2-devel
- cairo-devel
- cmake
- cups-devel
- dbus-devel
- dbus-glib-devel
- dbus-x11
- expat-devel
- glibc
- glibc-langpack-en
- gperf
- gtk3-devel
- httpd
- java-17-amazon-corretto
- libatomic
- libcap-devel
- libjpeg-devel
- libstdc++
- libXScrnSaver-devel
- libxkbcommon-x11-devel
- mod_ssl
- ncurses-compat-libs
- nspr-devel
- nss-devel
- pam-devel
- pciutils-devel
- perl
- php
- php-cli
- pulseaudio-libs-devel
- python
- python-psutil
- python-setuptools
- ruby
- xorg-x11-server-Xvfb
- zlib
state: latest
update_cache: true
- name: Checking for Directory Structure
stat:
path: /srv/source/chromium
register:
structure
- name: Creating Directory Structure
become: true
become_user: root
file:
path: /srv/{{ item }}/chromium
state: directory
group: ec2-user
owner: ec2-user
recurse: true
with_items:
- build
- source
when: structure.stat.exists != true
- name: Cloning Depot Tools
git:
repo: https://chromium.googlesource.com/chromium/tools/depot_tools.git
dest: /srv/source/depot_tools
force: yes
update: yes
- name: Upload .gclient
copy:
src: .gclient
dest: /srv/source/chromium/.gclient
owner: ec2-user
group: ec2-user
mode: '0664'
- name: Checking for Chromium
stat:
path: /srv/source/chromium/.gclient
register: gclient
- name: Resolving Puppeteer Version
uri:
url: "https://raw.githubusercontent.com/puppeteer/puppeteer/{{ puppeteer_version | default('main') }}/src/revisions.ts"
return_content: yes
register: puppeteer_revisions
- name: Resolving Chromium Revision from Puppeteer Version
set_fact:
chromium_revision: >
{{ puppeteer_revisions.content | regex_search("chromium: [']([0-9]*)[']", '\1') | first }}
- name: Resolving Git Commit from Chromium Revision
uri:
url: "https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/{{ chromium_revision }}"
return_content: yes
register: revision
- name: Parse Result
set_fact:
gitsha: >
{{ revision.content | regex_search('"git_sha":"([a-zA-Z0-9_]*)"', '\1') | trim }}
- name: Checking Out Chromium revision
shell: |
gclient sync --delete_unversioned_trees --revision {{ gitsha | first }} --with_branch_heads
args:
chdir: /srv/source/chromium
- name: Run Chromium hooks
shell: |
gclient runhooks
args:
chdir: /srv/source/chromium
- name: Patching Chromium
lineinfile:
path: "/srv/source/chromium/src/content/browser/{{ item.path }}"
line: "{{ item.line }}"
regexp: "{{ item.regexp }}"
state: present
backrefs: yes
with_items:
- {
path: 'sandbox_ipc_linux.cc',
line: '\1PLOG(WARNING) << "poll"; failed_polls = 0;',
regexp: '^(\s+)PLOG[(]WARNING[)] << "poll";$',
}
- {
path: 'renderer_host/render_process_host_impl.cc',
line: '\1// \2\3',
regexp: '^( )(\s*)(CHECK[(]render_process_host->InSameStoragePartition[(])$',
}
- {
path: 'renderer_host/render_process_host_impl.cc',
line: '\1// \2\3',
regexp: '^( )(\s*)(browser_context->GetStoragePartition[(]site_instance,)$',
}
- {
path: 'renderer_host/render_process_host_impl.cc',
line: '\1// \2\3',
regexp: '^( )(\s*)(false /[*] can_create [*]/[)][)][)];)$',
}
- name: Creating Build Configuration Directory
file:
mode: 0755
path: /srv/source/chromium/src/out/Headless
state: directory
- name: Mounting Build Directory in Memory
become: true
become_user: root
shell: |
mount --types tmpfs --options size=24G,nr_inodes=128k,mode=1777 tmpfs /srv/source/chromium/src/out/Headless
args:
warn: false
- name: Creating Headless Chromium Configuration
copy:
content: |
import("//build/args/headless.gn")
blink_symbol_level = 0
dcheck_always_on = false
disable_histogram_support = false
enable_basic_print_dialog = false
enable_basic_printing = true
enable_keystone_registration_framework = false
enable_linux_installer = false
enable_media_remoting = false
enable_one_click_signin = false
ffmpeg_branding = "Chrome"
is_component_build = false
is_debug = false
is_official_build = true
proprietary_codecs = true
symbol_level = 0
target_cpu = "x64"
target_os = "linux"
use_brlapi = 0
use_sysroot = true
v8_symbol_level = 0
v8_target_cpu = "x64"
dest: /srv/source/chromium/src/out/Headless/args.gn
- name: Generating Headless Chromium Configuration
shell: |
gn gen out/Headless
args:
chdir: /srv/source/chromium/src
- name: Compiling Headless Chromium
shell: |
autoninja -C out/Headless headless_shell
args:
chdir: /srv/source/chromium/src
- name: Getting Chromium Version
shell: |
sed --regexp-extended 's~[^0-9]+~~g' chrome/VERSION | tr '\n' '.' | sed 's~[.]$~~'
args:
chdir: /srv/source/chromium/src
warn: false
register: version
- name: Striping Symbols from Chromium Binary
shell: |
strip -o /srv/build/chromium/chromium-{{ version.stdout | quote }} out/Headless/headless_shell
args:
chdir: /srv/source/chromium/src
- name: Compressing Chromium
shell: |
brotli --best --force {{ item }}
args:
chdir: /srv/build/chromium
with_items:
- "chromium-{{ version.stdout }}"
- name: Downloading Chromium
fetch:
src: "/srv/build/chromium/{{ item }}"
dest: ../../../bin/
flat: yes
fail_on_missing: true
with_items:
- "chromium-{{ version.stdout }}.br"
- name: Archiving OpenGL ES driver
shell: |
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
args:
chdir: /srv/build/chromium
creates: /srv/build/chromium/swiftshader.tar
warn: false
- name: Compressing OpenGL ES driver
shell: |
brotli --best --force swiftshader.tar
args:
chdir: /srv/build/chromium
creates: /srv/build/chromium/swiftshader.tar.br
- name: Downloading OpenGL ES driver
fetch:
src: /srv/build/chromium/swiftshader.tar.br
dest: ../../../bin/
flat: yes
fail_on_missing: true
- name: Teardown AWS
hosts: localhost
gather_facts: false
tasks:
- name: Terminating EC2 Instance
amazon.aws.ec2_instance:
wait: yes
state: absent
instance_ids: '{{ ec2.instance_ids }}'
region: "{{ region }}"
- name: Deleting Security Group
amazon.aws.ec2_group:
name: Chromium
state: absent
region: "{{ region }}"
- name: Deleting EC2 Key Pair
amazon.aws.ec2_key:
name: ansible
state: absent
region: "{{ region }}"
- name: Deleting SSH Key
file:
path: "../{{ item }}"
state: absent
with_items:
- ansible.pem
- ansible.pem.pub

BIN
bin/aws.tar.br Normal file

Binary file not shown.

BIN
bin/chromium.br Normal file

Binary file not shown.

BIN
bin/swiftshader.tar.br Normal file

Binary file not shown.

28
incrementVersion Normal file
View File

@ -0,0 +1,28 @@
# incrementVersion.sh OLD_VERSION NEW_VERSION
# Example: incrementVersion 16.1.0 16.2.0
OLD_VERSION=$1
NEW_VERSION=$2
sed -i "s/$OLD_VERSION/$NEW_VERSION/" _/ansible/inventory.ini
sed -i "s/\"puppeteer-core\": \"$OLD_VERSION\"/\"puppeteer-core\": \"$NEW_VERSION\"/g" package.json
sed -i "s/puppeteer-core@$OLD_VERSION/puppeteer-core@$NEW_VERSION/" Makefile
echo "Version number incremented $OLD_VERSION -> $NEW_VERSION.
1) Check for a new version of 'chromium' included with 'puppeteer':
a) https://github.com/puppeteer/puppeteer/blob/main/src/revisions.ts
b) https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/#######
c) https://omahaproxy.appspot.com/
2) If the 'chromium' version has been incremented, please compile a new version of 'chromium':
a) cd _/ansible && make chromium
b) Rename the new chromium binary
3) Please also update README.md#Versioning
4) Merge the PR and deploy to npm
a) Test the new version using 'npm run test'
b) Push the PR to Github and merge it
c) Checkout the main branch
d) Run 'npm version $NEW_VERSION' to publish the package to npm."

67
package.json Normal file
View File

@ -0,0 +1,67 @@
{
"name": "@sparticuz/chrome-aws-lambda",
"version": "17.1.3",
"author": {
"name": "Kyle McNally"
},
"license": "MIT",
"description": "Chromium Binary for AWS Lambda and Google Cloud Functions, forked from @alixaxel/chrome-aws-lambda",
"main": "build/index.js",
"types": "build/index.d.ts",
"files": [
"bin",
"build",
"typings"
],
"engines": {
"node": ">= 14"
},
"scripts": {
"test": "make clean && make && make pretest && make test",
"build": "rm -rf build && tsc -p tsconfig.json",
"postversion": "git push && git push --tags && npm publish",
"prepack": "npm run build",
"preversion": "npm run build"
},
"devDependencies": {
"@types/node": "^16.11.49",
"@types/tar-fs": "^2.0.1",
"clean-modules": "^2.0.6",
"puppeteer-core": "17.1.3",
"typescript": "^4.6.4"
},
"peerDependencies": {
"puppeteer-core": "17.1.3"
},
"bugs": {
"url": "https://github.com/Sparticuz/chrome-aws-lambda/issues"
},
"homepage": "https://github.com/Sparticuz/chrome-aws-lambda",
"repository": {
"type": "git",
"url": "git://github.com/Sparticuz/chrome-aws-lambda.git"
},
"keywords": [
"aws",
"browser",
"chrome",
"chromium",
"lambda",
"puppeteer",
"serverless"
],
"prettier": {
"arrowParens": "always",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"printWidth": 140,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false
},
"dependencies": {
"tar-fs": "^2.1.1"
}
}

48
source/hooks/adblock.ts Normal file
View File

@ -0,0 +1,48 @@
import { promises } from 'fs';
import { get } from 'https';
import { Page } from 'puppeteer-core';
let adblocker: any = null;
/**
* Enables ad blocking in page.
* Requires `@cliqz/adblocker-puppeteer` package.
*
* @param page - Page to hook to.
*/
export = async function (page: Page): Promise<Page> {
if (adblocker == null) {
const { fullLists, PuppeteerBlocker } = require('@cliqz/adblocker-puppeteer');
adblocker = await PuppeteerBlocker.fromLists(
(url: string) => {
return new Promise((resolve, reject) => {
return get(url, (response) => {
if (response.statusCode !== 200) {
return reject(`Unexpected status code: ${response.statusCode}.`);
}
let result = '';
response.on('data', (chunk) => {
result += chunk;
});
response.on('end', () => {
return resolve({ text: () => result });
});
});
});
},
fullLists,
{ enableCompression: false },
{
path: '/tmp/adblock.bin',
read: promises.readFile,
write: promises.writeFile,
}
);
}
return await adblocker.enableBlockingInPage(page).then(() => page);
}

16
source/hooks/agent.ts Normal file
View File

@ -0,0 +1,16 @@
import { Page } from 'puppeteer-core';
/**
* Removes `Headless` from the User Agent string, if present.
*
* @param page - Page to hook to.
*/
export = async function (page: Page): Promise<Page> {
let result = await page.browser().userAgent();
if (result.includes('Headless') === true) {
await page.setUserAgent(result.replace('Headless', ''));
}
return page;
};

189
source/hooks/chrome.ts Normal file
View File

@ -0,0 +1,189 @@
import { Page } from 'puppeteer-core';
import { Writeable } from '../../typings/chrome-aws-lambda';
/**
* Mocks the global `chrome` property to mimic headful Chrome.
*
* @param page - Page to hook to.
*/
export = async function (page: Page): Promise<Page> {
const handler = () => {
let alpha = Date.now();
let delta = Math.floor(500 * Math.random());
if ((window as any).chrome === undefined) {
Object.defineProperty(window, 'chrome', {
configurable: false,
enumerable: true,
value: {},
writable: true,
});
}
/**
* https://github.com/berstend/puppeteer-extra/blob/master/packages/puppeteer-extra-plugin-stealth/evasions/chrome.app/index.js
*/
if ((window as any).chrome.app === undefined) {
const InvocationError = (callback: string) => {
/**
* Truncates every line of the stack trace (with the exception of the first), until `search` is found.
*/
const truncateStackTrace = (error: Error, search: string) => {
const stack = error.stack.split('\n');
const index = stack.findIndex((value: string) => value.trim().startsWith(search));
if (index > 0) {
error.stack = [stack[0], ...stack.slice(index + 1)].join('\n');
}
return error;
};
return truncateStackTrace(new TypeError(`Error in invocation of app.${callback}()`), `at ${callback} (eval at <anonymous>`);
};
Object.defineProperty((window as any).chrome, 'app', {
value: {
InstallState: {
DISABLED: 'disabled',
INSTALLED: 'installed',
NOT_INSTALLED: 'not_installed',
},
RunningState: {
CANNOT_RUN: 'cannot_run',
READY_TO_RUN: 'ready_to_run',
RUNNING: 'running',
},
get isInstalled() {
return false;
},
getDetails: function getDetails(): null {
if (arguments.length > 0) {
throw InvocationError('getDetails');
}
return null;
},
getIsInstalled: function getIsInstalled() {
if (arguments.length > 0) {
throw InvocationError('getIsInstalled');
}
return false;
},
runningState: function runningState() {
if (arguments.length > 0) {
throw InvocationError('runningState');
}
return 'cannot_run';
},
},
});
}
let timing: Partial<PerformanceTiming> = {
navigationStart: alpha + 1 * delta,
domContentLoadedEventEnd: alpha + 4 * delta,
responseStart: alpha + 2 * delta,
loadEventEnd: alpha + 5 * delta,
};
if (window.performance?.timing !== undefined) {
timing = window.performance.timing;
}
/**
* https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/evasions/chrome.csi
*/
if ((window as any).chrome.csi === undefined) {
Object.defineProperty((window as any).chrome, 'csi', {
value: function csi() {
return {
startE: timing.navigationStart,
onloadT: timing.domContentLoadedEventEnd,
pageT: Date.now() - timing.navigationStart + Math.random().toFixed(3),
tran: 15,
};
},
});
}
/**
* https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/evasions/chrome.loadTimes
*/
if ((window as any).chrome.loadTimes === undefined) {
let navigation: Writeable<Partial<PerformanceNavigationTiming>> = {
nextHopProtocol: 'h2',
startTime: 3 * delta,
type: 'other' as any,
};
if (typeof window.performance?.getEntriesByType === 'function') {
let entries = {
navigation: window.performance.getEntriesByType('navigation') as PerformanceNavigationTiming[],
paint: window.performance.getEntriesByType('paint') as PerformanceNavigationTiming[],
};
if (entries.navigation.length > 0) {
navigation = entries.navigation.shift();
}
if (entries.paint.length > 0) {
navigation.startTime = entries.paint.shift().startTime;
}
}
Object.defineProperty((window as any).chrome, 'loadTimes', {
value: function loadTimes() {
return {
get commitLoadTime() {
return timing.responseStart / 1000;
},
get connectionInfo() {
return navigation.nextHopProtocol;
},
get finishDocumentLoadTime() {
return timing.domContentLoadedEventEnd / 1000;
},
get finishLoadTime() {
return timing.loadEventEnd / 1000;
},
get firstPaintAfterLoadTime() {
return 0;
},
get firstPaintTime() {
return parseFloat(((navigation.startTime + (window.performance?.timeOrigin ?? timing.navigationStart)) / 1000).toFixed(3));
},
get navigationType() {
return navigation.type;
},
get npnNegotiatedProtocol() {
return ['h2', 'hq'].includes(navigation.nextHopProtocol) ? navigation.nextHopProtocol : 'unknown';
},
get requestTime() {
return timing.navigationStart / 1000;
},
get startLoadTime() {
return timing.navigationStart / 1000;
},
get wasAlternateProtocolAvailable() {
return false;
},
get wasFetchedViaSpdy() {
return ['h2', 'hq'].includes(navigation.nextHopProtocol);
},
get wasNpnNegotiated() {
return ['h2', 'hq'].includes(navigation.nextHopProtocol);
},
};
},
});
};
}
await page.evaluate(handler);
await page.evaluateOnNewDocument(handler);
return page;
}

23
source/hooks/languages.ts Normal file
View File

@ -0,0 +1,23 @@
import { Page } from 'puppeteer-core';
/**
* Emulates `en-US` language.
*
* @param page - Page to hook to.
*/
export = async function (page: Page): Promise<Page> {
const handler = () => {
Object.defineProperty(Object.getPrototypeOf(navigator), 'language', {
get: () => 'en-US',
});
Object.defineProperty(Object.getPrototypeOf(navigator), 'languages', {
get: () => ['en-US', 'en'],
});
};
await page.evaluate(handler);
await page.evaluateOnNewDocument(handler);
return page;
}

View File

@ -0,0 +1,28 @@
import { Page } from 'puppeteer-core';
/**
* Emulates `denied` state for all permission queries.
*
* @param page - Page to hook to.
*/
export = async function (page: Page): Promise<Page> {
const handler = () => {
let query = window.navigator.permissions.query;
(Permissions as any).prototype.query = function (parameters: PermissionDescriptor) {
if (parameters?.name?.length > 0) {
return Promise.resolve({
onchange: null,
state: 'denied',
});
}
return query(parameters);
};
};
await page.evaluate(handler);
await page.evaluateOnNewDocument(handler);
return page;
}

10
source/hooks/timezone.ts Normal file
View File

@ -0,0 +1,10 @@
import { Page } from 'puppeteer-core';
/**
* Emulates UTC timezone.
*
* @param page - Page to hook to.
*/
export = function (page: Page): Promise<Page> {
return page.emulateTimezone('UTC').then(() => page);
}

19
source/hooks/webdriver.ts Normal file
View File

@ -0,0 +1,19 @@
import { Page } from 'puppeteer-core';
/**
* Removes global `webdriver` property to mimic headful Chrome.
*
* @param page - Page to hook to.
*/
export = async function (page: Page): Promise<Page> {
const handler = () => {
Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', {
get: () => false,
});
};
await page.evaluate(handler);
await page.evaluateOnNewDocument(handler);
return page;
}

47
source/hooks/window.ts Normal file
View File

@ -0,0 +1,47 @@
import { Page } from 'puppeteer-core';
/**
* Patches window outer dimentions to mimic headful Chrome.
*
* @param page - Page to hook to.
*/
export = async function (page: Page): Promise<Page> {
const handler = () => {
if (window.outerWidth === 0) {
Object.defineProperty(window, 'outerWidth', {
get: () => screen.availWidth,
});
}
if (window.outerHeight === 0) {
Object.defineProperty(window, 'outerHeight', {
get: () => screen.availHeight,
});
}
if (window.screenX === 0) {
Object.defineProperty(window, 'screenX', {
get: () => screen.width - screen.availWidth,
});
Object.defineProperty(window, 'screenLeft', {
get: () => screenX,
});
}
if (window.screenY === 0) {
Object.defineProperty(window, 'screenY', {
get: () => screen.height - screen.availHeight,
});
Object.defineProperty(window, 'screenTop', {
get: () => screenY,
});
}
};
await page.evaluate(handler);
await page.evaluateOnNewDocument(handler);
return page;
}

219
source/index.ts Normal file
View File

@ -0,0 +1,219 @@
/// <reference path="../typings/chrome-aws-lambda.d.ts" />
import { access, createWriteStream, existsSync, mkdirSync, readdirSync, symlink, unlinkSync } from 'fs';
import { IncomingMessage } from 'http';
import LambdaFS from './lambdafs';
import { join } from 'path';
import { PuppeteerNode, Viewport } from 'puppeteer-core';
import { URL } from 'url';
if (/^AWS_Lambda_nodejs(?:10|12|14|16)[.]x$/.test(process.env.AWS_EXECUTION_ENV) === true) {
if (process.env.FONTCONFIG_PATH === undefined) {
process.env.FONTCONFIG_PATH = '/tmp/aws';
}
if (process.env.LD_LIBRARY_PATH === undefined) {
process.env.LD_LIBRARY_PATH = '/tmp/aws/lib';
} else if (process.env.LD_LIBRARY_PATH.startsWith('/tmp/aws/lib') !== true) {
process.env.LD_LIBRARY_PATH = [...new Set(['/tmp/aws/lib', ...process.env.LD_LIBRARY_PATH.split(':')])].join(':');
}
}
class Chromium {
/**
* Downloads or symlinks a custom font and returns its basename, patching the environment so that Chromium can find it.
* If not running on AWS Lambda nor Google Cloud Functions, `null` is returned instead.
*/
static font(input: string): Promise<string> {
if (Chromium.headless !== true) {
return null;
}
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());
}
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());
});
});
} else {
let handler = url.protocol === 'http:' ? require('http').get : require('https').get;
handler(input, (response: IncomingMessage) => {
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());
});
});
});
}
});
}
/**
* 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/.
*/
static get args(): string[] {
const result = [
'--allow-running-insecure-content', // https://source.chromium.org/search?q=lang:cpp+symbol:kAllowRunningInsecureContent&ss=chromium
'--autoplay-policy=user-gesture-required', // https://source.chromium.org/search?q=lang:cpp+symbol:kAutoplayPolicy&ss=chromium
'--disable-background-timer-throttling',
'--disable-component-update', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableComponentUpdate&ss=chromium
'--disable-domain-reliability', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableDomainReliability&ss=chromium
'--disable-features=AudioServiceOutOfProcess,IsolateOrigins,site-per-process', // https://source.chromium.org/search?q=file:content_features.cc&ss=chromium
'--disable-ipc-flooding-protection',
'--disable-print-preview', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisablePrintPreview&ss=chromium
'--disable-dev-shm-usage',
'--disable-setuid-sandbox', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSetuidSandbox&ss=chromium
'--disable-site-isolation-trials', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSiteIsolation&ss=chromium
'--disable-speech-api', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableSpeechAPI&ss=chromium
'--disable-web-security', // https://source.chromium.org/search?q=lang:cpp+symbol:kDisableWebSecurity&ss=chromium
'--disk-cache-size=33554432', // https://source.chromium.org/search?q=lang:cpp+symbol:kDiskCacheSize&ss=chromium
'--enable-features=SharedArrayBuffer', // https://source.chromium.org/search?q=file:content_features.cc&ss=chromium
'--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
'--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-first-run',
'--no-pings', // https://source.chromium.org/search?q=lang:cpp+symbol:kNoPings&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
'--use-gl=angle', // https://chromium.googlesource.com/chromium/src/+/main/docs/gpu/swiftshader.md
'--use-angle=swiftshader', // https://chromium.googlesource.com/chromium/src/+/main/docs/gpu/swiftshader.md
'--window-size=1920,1080', // https://source.chromium.org/search?q=lang:cpp+symbol:kWindowSize&ss=chromium
];
if (Chromium.headless === true) {
result.push('--single-process'); // https://source.chromium.org/search?q=lang:cpp+symbol:kSingleProcess&ss=chromium
} else {
result.push('--start-maximized'); // https://source.chromium.org/search?q=lang:cpp+symbol:kStartMaximized&ss=chromium
}
return result;
}
/**
* Returns sensible default viewport settings.
*/
static get defaultViewport(): Required<Viewport> {
return {
deviceScaleFactor: 1,
hasTouch: false,
height: 1080,
isLandscape: true,
isMobile: false,
width: 1920,
};
}
/**
* Inflates the current version of Chromium and returns the path to the binary.
* If not running on AWS Lambda nor Google Cloud Functions, `null` is returned instead.
*/
static get executablePath(): Promise<string> {
if (Chromium.headless !== true) {
return Promise.resolve(null);
}
if (existsSync('/tmp/chromium') === true) {
for (const file of readdirSync('/tmp')) {
if (file.startsWith('core.chromium') === true) {
unlinkSync(`/tmp/${file}`);
}
}
return Promise.resolve('/tmp/chromium');
}
const input = join(__dirname, '..', 'bin');
const promises = [
LambdaFS.inflate(`${input}/chromium.br`),
LambdaFS.inflate(`${input}/swiftshader.tar.br`),
];
if (/^AWS_Lambda_nodejs(?:10|12|14|16)[.]x$/.test(process.env.AWS_EXECUTION_ENV) === true) {
promises.push(LambdaFS.inflate(`${input}/aws.tar.br`));
}
return Promise.all(promises).then((result) => result.shift());
}
/**
* Returns a boolean indicating if we are running on AWS Lambda or Google Cloud Functions.
* False is returned if Serverless environment variables `IS_LOCAL` or `IS_OFFLINE` are set.
*/
static get headless() {
if (process.env.IS_LOCAL !== undefined || process.env.IS_OFFLINE !== undefined) {
return false;
}
const environments = [
'AWS_LAMBDA_FUNCTION_NAME',
'FUNCTION_NAME',
'FUNCTION_TARGET',
'FUNCTIONS_EMULATOR',
];
return environments.some((key) => process.env[key] !== undefined);
}
/**
* Overloads puppeteer with useful methods and returns the resolved package.
*/
static get puppeteer(): PuppeteerNode {
for (const overload of ['Browser', 'BrowserContext', 'ElementHandle', 'Frame', 'Page']) {
require(`${__dirname}/puppeteer/lib/${overload}`);
}
try {
return require('puppeteer');
} catch (error: any) {
if (error.code !== 'MODULE_NOT_FOUND') {
throw error;
}
return require('puppeteer-core');
}
}
}
export = Chromium;

61
source/lambdafs.ts Normal file
View File

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

View File

@ -0,0 +1,43 @@
import { Browser, Page } from 'puppeteer-core';
import { Hook, Prototype } from '../../../typings/chrome-aws-lambda';
let Super: Prototype<Browser> = null;
try {
Super = require('puppeteer/lib/cjs/puppeteer/common/Browser.js').Browser;
} catch (error) {
Super = require('puppeteer-core/lib/cjs/puppeteer/common/Browser.js').Browser;
}
Super.prototype.defaultPage = async function (...hooks: Hook[]) {
let page: Page = null;
let pages: Page[] = await this.pages();
if (pages.length === 0) {
pages = [await this.newPage()];
}
page = pages.shift();
if (hooks != null && Array.isArray(hooks) === true) {
for (let hook of hooks) {
page = await hook(page);
}
}
return page;
};
let newPage: any = Super.prototype.newPage;
Super.prototype.newPage = async function (...hooks: Hook[]) {
let page: Page = await newPage.apply(this, arguments);
if (hooks != null && Array.isArray(hooks) === true) {
for (let hook of hooks) {
page = await hook(page);
}
}
return page;
};

View File

@ -0,0 +1,43 @@
import { BrowserContext, Page } from 'puppeteer-core';
import { Hook, Prototype } from '../../../typings/chrome-aws-lambda';
let Super: Prototype<BrowserContext> = null;
try {
Super = require('puppeteer/lib/cjs/puppeteer/common/Browser.js').BrowserContext;
} catch (error) {
Super = require('puppeteer-core/lib/cjs/puppeteer/common/Browser.js').BrowserContext;
}
Super.prototype.defaultPage = async function (...hooks: Hook[]) {
let page: Page = null;
let pages: Page[] = await this.pages();
if (pages.length === 0) {
pages = [await this.newPage()];
}
page = pages.shift();
if (hooks != null && Array.isArray(hooks) === true) {
for (let hook of hooks) {
page = await hook(page);
}
}
return page;
};
let newPage: any = Super.prototype.newPage;
Super.prototype.newPage = async function (...hooks: Hook[]) {
let page: Page = await newPage.apply(this, arguments);
if (hooks != null && Array.isArray(hooks) === true) {
for (let hook of hooks) {
page = await hook(page);
}
}
return page;
};

View File

@ -0,0 +1,557 @@
import { ElementHandle, EvaluateFunc, HTTPRequest, HTTPResponse, Page, WaitForOptions, WaitTimeoutOptions } from 'puppeteer-core';
import { Prototype } from '../../../typings/chrome-aws-lambda';
let Super: Prototype<ElementHandle> = null;
try {
Super = require('puppeteer/lib/cjs/puppeteer/common/ElementHandle.js').ElementHandle;
} catch (error) {
Super = require('puppeteer-core/lib/cjs/puppeteer/common/ElementHandle.js').ElementHandle;
}
Super.prototype.clear = function () {
return this.click({ clickCount: 3 }).then(() => this.press('Backspace'));
};
Super.prototype.clickAndWaitForNavigation = function (options?: WaitForOptions) {
options = options ?? {
waitUntil: [
'load',
],
};
let promises: [Promise<HTTPResponse>, Promise<void>] = [
((this as any)._page as Page).waitForNavigation(options),
this.click(),
];
return Promise.all(promises).then((value) => value.shift() as HTTPResponse);
};
Super.prototype.clickAndWaitForRequest = function (predicate: string | RegExp | ((request: HTTPRequest) => boolean | Promise<boolean>), options?: WaitTimeoutOptions) {
let callback = (request: HTTPRequest) => {
let url = request.url();
if (typeof predicate === 'string' && predicate.includes('*') === true) {
predicate = new RegExp(predicate.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/[*]+/g, '.*?'), 'g');
}
if (predicate instanceof RegExp) {
return predicate.test(url);
}
return predicate === url;
};
let promises: [Promise<HTTPRequest>, Promise<void>] = [
((this as any)._page as Page).waitForRequest((typeof predicate === 'function') ? predicate : callback, options),
this.click(),
];
return Promise.all(promises).then((value) => value.shift() as HTTPRequest);
};
Super.prototype.clickAndWaitForResponse = function (predicate: string | RegExp | ((request: HTTPResponse) => boolean | Promise<boolean>), options?: WaitTimeoutOptions) {
let callback = (request: HTTPResponse) => {
let url = request.url();
if (typeof predicate === 'string' && predicate.includes('*') === true) {
predicate = new RegExp(predicate.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/[*]+/g, '.*?'), 'g');
}
if (predicate instanceof RegExp) {
return predicate.test(url);
}
return predicate === url;
};
let promises: [Promise<HTTPResponse>, Promise<void>] = [
((this as any)._page as Page).waitForResponse((typeof predicate === 'function') ? predicate : callback, options),
this.click(),
];
return Promise.all(promises).then((value) => value.shift() as HTTPResponse);
};
Super.prototype.fillFormByLabel = function <T extends Record<string, boolean | string | string[]>>(data: T) {
let callback = (node: HTMLFormElement, data: T) => {
if (node.nodeName.toLowerCase() !== 'form') {
throw new Error('Element is not a <form> element.');
}
let result: Record<string, string[]> = {};
for (let [key, value] of Object.entries(data)) {
let selector = [
`id(string(//label[normalize-space(.) = "${key}"]/@for))`,
`//label[normalize-space(.) = "${key}"]//*[self::input or self::select or self::textarea]`,
].join(' | ');
if (result.hasOwnProperty(key) !== true) {
result[key] = [];
}
let element: Node = null;
let elements: HTMLInputElement[] = [];
let iterator = document.evaluate(selector, node, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
while ((element = iterator.iterateNext()) != null) {
elements.push(element as HTMLInputElement);
}
if (elements.length === 0) {
throw new Error(`No elements match the selector '${selector}' for '${key}'.`);
}
let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase();
let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[];
if (type === 'file') {
throw new Error(`Input element of type 'file' is not supported.`);
}
for (let element of elements) {
try {
element.focus();
element.dispatchEvent(new Event('focus'));
} catch (error) {
}
if (type === 'select') {
element.value = undefined;
for (let index of ['value', 'label'] as ['value', 'label']) {
if (result[key].length > 0) {
break;
}
for (let option of Array.from((element as unknown as HTMLSelectElement).options)) {
option.selected = values.includes(option[index]);
if (option.selected === true) {
result[key].push(option.value);
if (element.multiple !== true) {
break;
}
}
}
}
} else if (type === 'checkbox' || type === 'radio') {
element.checked = (value === true) || values.includes(element.value);
if (element.checked === true) {
result[key].push(element.value);
}
} else if (typeof value === 'string') {
if (element.isContentEditable === true) {
result[key].push(element.textContent = value);
} else {
result[key].push(element.value = value);
}
}
for (let trigger of ['input', 'change']) {
element.dispatchEvent(new Event(trigger, { 'bubbles': true }));
}
try {
element.blur();
element.dispatchEvent(new Event('blur'));
} catch (error) {
}
if (type === 'checkbox' || type === 'radio') {
break;
}
}
}
return result;
};
return this.evaluate(callback as unknown as EvaluateFunc<[ElementHandle<Element>, T]>, data) as any;
};
Super.prototype.fillFormByName = function <T extends Record<string, boolean | string | string[]>>(data: T) {
let callback = (node: HTMLFormElement, data: T, heuristic: 'css' | 'label' | 'name' | 'xpath' = 'css') => {
if (node.nodeName.toLowerCase() !== 'form') {
throw new Error('Element is not a <form> element.');
}
let result: Record<string, string[]> = {};
for (let [key, value] of Object.entries(data)) {
let selector = `[name="${key}"]`;
if (result.hasOwnProperty(key) !== true) {
result[key] = [];
}
let elements: HTMLInputElement[] = Array.from(node.querySelectorAll(selector));
if (elements.length === 0) {
throw new Error(`No elements match the selector '${selector}' for '${key}'.`);
}
let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase();
let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[];
if (type === 'file') {
throw new Error(`Input element of type 'file' is not supported.`);
}
for (let element of elements) {
try {
element.focus();
element.dispatchEvent(new Event('focus'));
} catch (error) {
}
if (type === 'select') {
element.value = undefined;
for (let index of ['value', 'label'] as ['value', 'label']) {
if (result[key].length > 0) {
break;
}
for (let option of Array.from((element as unknown as HTMLSelectElement).options)) {
option.selected = values.includes(option[index]);
if (option.selected === true) {
result[key].push(option.value);
if (element.multiple !== true) {
break;
}
}
}
}
} else if (type === 'checkbox' || type === 'radio') {
element.checked = (value === true) || values.includes(element.value);
if (element.checked === true) {
result[key].push(element.value);
}
} else if (typeof value === 'string') {
if (element.isContentEditable === true) {
result[key].push(element.textContent = value);
} else {
result[key].push(element.value = value);
}
}
for (let trigger of ['input', 'change']) {
element.dispatchEvent(new Event(trigger, { 'bubbles': true }));
}
try {
element.blur();
element.dispatchEvent(new Event('blur'));
} catch (error) {
}
if (type === 'checkbox' || type === 'radio') {
break;
}
}
}
return result;
};
return this.evaluate(callback as unknown as EvaluateFunc<[ElementHandle<Element>, T]>, data) as any;
};
Super.prototype.fillFormBySelector = function <T extends Record<string, boolean | string | string[]>>(data: T) {
let callback = (node: HTMLFormElement, data: T, heuristic: 'css' | 'label' | 'name' | 'xpath' = 'css') => {
if (node.nodeName.toLowerCase() !== 'form') {
throw new Error('Element is not a <form> element.');
}
let result: Record<string, string[]> = {};
for (let [key, value] of Object.entries(data)) {
let selector = key;
if (result.hasOwnProperty(key) !== true) {
result[key] = [];
}
let elements: HTMLInputElement[] = Array.from(node.querySelectorAll(selector));
if (elements.length === 0) {
throw new Error(`No elements match the selector '${selector}' for '${key}'.`);
}
let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase();
let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[];
if (type === 'file') {
throw new Error(`Input element of type 'file' is not supported.`);
}
for (let element of elements) {
try {
element.focus();
element.dispatchEvent(new Event('focus'));
} catch (error) {
}
if (type === 'select') {
element.value = undefined;
for (let index of ['value', 'label'] as ['value', 'label']) {
if (result[key].length > 0) {
break;
}
for (let option of Array.from((element as unknown as HTMLSelectElement).options)) {
option.selected = values.includes(option[index]);
if (option.selected === true) {
result[key].push(option.value);
if (element.multiple !== true) {
break;
}
}
}
}
} else if (type === 'checkbox' || type === 'radio') {
element.checked = (value === true) || values.includes(element.value);
if (element.checked === true) {
result[key].push(element.value);
}
} else if (typeof value === 'string') {
if (element.isContentEditable === true) {
result[key].push(element.textContent = value);
} else {
result[key].push(element.value = value);
}
}
for (let trigger of ['input', 'change']) {
element.dispatchEvent(new Event(trigger, { 'bubbles': true }));
}
try {
element.blur();
element.dispatchEvent(new Event('blur'));
} catch (error) {
}
if (type === 'checkbox' || type === 'radio') {
break;
}
}
}
return result;
};
return this.evaluate(callback as unknown as EvaluateFunc<[ElementHandle<Element>, T]>, data) as any;
};
Super.prototype.fillFormByXPath = function <T extends Record<string, boolean | string | string[]>>(data: T) {
let callback = (node: HTMLFormElement, data: T) => {
if (node.nodeName.toLowerCase() !== 'form') {
throw new Error('Element is not a <form> element.');
}
let result: Record<string, string[]> = {};
for (let [key, value] of Object.entries(data)) {
let selector = key;
if (result.hasOwnProperty(key) !== true) {
result[key] = [];
}
let element: Node = null;
let elements: HTMLInputElement[] = [];
let iterator = document.evaluate(selector, node, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
while ((element = iterator.iterateNext()) != null) {
elements.push(element as HTMLInputElement);
}
if (elements.length === 0) {
throw new Error(`No elements match the selector '${selector}' for '${key}'.`);
}
let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase();
let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[];
if (type === 'file') {
throw new Error(`Input element of type 'file' is not supported.`);
}
for (let element of elements) {
try {
element.focus();
element.dispatchEvent(new Event('focus'));
} catch (error) {
}
if (type === 'select') {
element.value = undefined;
for (let index of ['value', 'label'] as ['value', 'label']) {
if (result[key].length > 0) {
break;
}
for (let option of Array.from((element as unknown as HTMLSelectElement).options)) {
option.selected = values.includes(option[index]);
if (option.selected === true) {
result[key].push(option.value);
if (element.multiple !== true) {
break;
}
}
}
}
} else if (type === 'checkbox' || type === 'radio') {
element.checked = (value === true) || values.includes(element.value);
if (element.checked === true) {
result[key].push(element.value);
}
} else if (typeof value === 'string') {
if (element.isContentEditable === true) {
result[key].push(element.textContent = value);
} else {
result[key].push(element.value = value);
}
}
for (let trigger of ['input', 'change']) {
element.dispatchEvent(new Event(trigger, { 'bubbles': true }));
}
try {
element.blur();
element.dispatchEvent(new Event('blur'));
} catch (error) {
}
if (type === 'checkbox' || type === 'radio') {
break;
}
}
}
return result;
};
return this.evaluate(callback as unknown as EvaluateFunc<[ElementHandle<Element>, T]>, data) as any;
};
Super.prototype.getInnerHTML = function () {
return this.evaluate((node: Element) => {
return (node as HTMLElement).innerHTML;
});
};
Super.prototype.getInnerText = function () {
return this.evaluate((node: Element) => {
return (node as HTMLElement).innerText;
});
};
Super.prototype.number = function (decimal: string = '.', property: any) {
let callback = (node: any, decimal: string, property: any) => {
let data = (node[property] as unknown) as string;
if (typeof data === 'string') {
decimal = decimal ?? '.';
if (typeof decimal === 'string') {
decimal = decimal.replace(/[.]/g, '\\$&');
}
let matches = data.match(/((?:[-+]|\b)[0-9]+(?:[ ,.'`´]*[0-9]+)*)\b/g);
if (matches != null) {
return matches.map((value) => parseFloat(value.replace(new RegExp(`[^-+0-9${decimal}]+`, 'g'), '').replace(decimal, '.')));
}
}
return null;
};
return this.evaluate(callback, decimal, property as any);
};
Super.prototype.selectByLabel = function (...values: string[]) {
for (let value of values) {
console.assert(typeof value === 'string', `Values must be strings. Found value '${value}' of type '${typeof value}'.`);
}
let callback = (node: HTMLSelectElement, values: string[]) => {
if (node.nodeName.toLowerCase() !== 'select') {
throw new Error('Element is not a <select> element.');
}
node.value = undefined;
let result = [];
let options = Array.from(node.options);
for (let option of options) {
option.selected = values.includes(option.label);
if (option.selected === true) {
result.push(option.value);
if (node.multiple !== true) {
break;
}
}
}
for (let trigger of ['input', 'change']) {
node.dispatchEvent(new Event(trigger, { bubbles: true }));
}
return result;
};
return this.evaluate(callback as any, values);
};
Super.prototype.string = function (property: any) {
let callback = (node: any, property: any) => {
let data = (node[property] as unknown) as string;
if (typeof data === 'string') {
let patterns = {
' ': /[\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000]/g,
'-': /[\u2013\u2014]/g,
'...': /[\u2026]/g,
'': /[\u200B\uFEFF]/g,
'"': /[\u201C\u201D]/g,
'<': /[\u00AB\u2039]/g,
'>': /[\u00BB\u203A]/g,
'|': /[\u007C\u00A6\u01C0\u2223\u2758]/g,
"'": /[\u2018\u2019\u201A\u201B\u2032]/g,
};
for (let [key, value] of Object.entries(patterns)) {
data = data.replace(value, key);
}
return data.replace(/[\s]+/g, ' ').trim();
}
return null;
};
return this.evaluate(callback, property as any);
};

View File

@ -0,0 +1,148 @@
import { Frame, HTTPRequest, HTTPResponse, WaitForOptions, WaitTimeoutOptions } from 'puppeteer-core';
import { KeysOfType, Prototype } from '../../../typings/chrome-aws-lambda';
let Super: Prototype<Frame> = null;
try {
Super = require('puppeteer/lib/cjs/puppeteer/common/Frame.js').Frame;
} catch (error) {
Super = require('puppeteer-core/lib/cjs/puppeteer/common/Frame.js').Frame;
}
Super.prototype.clear = function (selector: string) {
return this.$(selector).then((element) => element?.clear());
};
Super.prototype.clickAndWaitForNavigation = function (selector: string, options?: WaitForOptions) {
options = options ?? {
waitUntil: [
'load',
],
};
let promises: [Promise<HTTPResponse>, Promise<void>] = [
this.waitForNavigation(options),
this.waitForSelector(selector, { timeout: options.timeout }).then((element) => element.click()),
];
return Promise.all(promises).then((value) => value.shift() as HTTPResponse);
};
Super.prototype.clickAndWaitForRequest = function (selector: string, predicate: string | RegExp | ((request: HTTPRequest) => boolean | Promise<boolean>), options?: WaitTimeoutOptions) {
let callback = (request: HTTPRequest) => {
let url = request.url();
if (typeof predicate === 'string' && predicate.includes('*') === true) {
predicate = new RegExp(predicate.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/[*]+/g, '.*?'), 'g');
}
if (predicate instanceof RegExp) {
return predicate.test(url);
}
return predicate === url;
};
let promises: [Promise<HTTPRequest>, Promise<void>] = [
this.page().waitForRequest((typeof predicate === 'function') ? predicate : callback, options),
this.click(selector),
];
return Promise.all(promises).then((value) => value.shift() as HTTPRequest);
};
Super.prototype.clickAndWaitForResponse = function (selector: string, predicate: string | RegExp | ((request: HTTPResponse) => boolean | Promise<boolean>), options?: WaitTimeoutOptions) {
let callback = (request: HTTPResponse) => {
let url = request.url();
if (typeof predicate === 'string' && predicate.includes('*') === true) {
predicate = new RegExp(predicate.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/[*]+/g, '.*?'), 'g');
}
if (predicate instanceof RegExp) {
return predicate.test(url);
}
return predicate === url;
};
let promises: [Promise<HTTPResponse>, Promise<void>] = [
this.page().waitForResponse((typeof predicate === 'function') ? predicate : callback, options),
this.click(selector),
];
return Promise.all(promises).then((value) => value.shift() as HTTPResponse);
};
Super.prototype.count = function (selector: string) {
let callback = (selector: string) => {
return document.querySelectorAll(selector).length;
};
return this.evaluate(callback, selector);
};
Super.prototype.exists = function (selector: string) {
let callback = (selector: string) => {
return document.querySelector(selector) !== null;
};
return this.evaluate(callback, selector);
};
Super.prototype.fillFormByLabel = function <T extends Record<string, boolean | string | string[]>>(selector: string, data: T) {
return this.$(selector).then((element) => element?.fillFormByLabel(data) ?? null);
};
Super.prototype.fillFormByName = function <T extends Record<string, boolean | string | string[]>>(selector: string, data: T) {
return this.$(selector).then((element) => element?.fillFormByName(data) ?? null);
};
Super.prototype.fillFormBySelector = function <T extends Record<string, boolean | string | string[]>>(selector: string, data: T) {
return this.$(selector).then((element) => element?.fillFormBySelector(data) ?? null);
};
Super.prototype.fillFormByXPath = function <T extends Record<string, boolean | string | string[]>>(selector: string, data: T) {
return this.$(selector).then((element) => element?.fillFormByXPath(data) ?? null);
};
Super.prototype.number = function <T = HTMLElement>(selector: string, decimal: string = '.', property: KeysOfType<T, string> = 'textContent' as any) {
return this.$(selector).then((element) => element?.number(decimal, property) ?? null);
};
Super.prototype.selectByLabel = function (selector: string, ...values: string[]) {
return this.$(selector).then((element) => element?.selectByLabel(...values) ?? null);
};
Super.prototype.string = function <T = HTMLElement>(selector: string, property: KeysOfType<T, string> = 'textContent' as any) {
return this.$(selector).then((element) => element?.string(property) ?? null);
};
Super.prototype.waitForText = function (predicate: string, options?: WaitTimeoutOptions) {
if (predicate.includes(`"`) !== true) {
predicate = `"${predicate}"`;
} else if (predicate.includes(`'`) !== true) {
predicate = `'${predicate}'`;
} else {
throw new Error('Predicate cannot include both single and double quotes.');
}
return this.waitForXPath(`//*[contains(concat(' ', normalize-space(text()), ' '), ${predicate})]`, {
...options,
visible: true,
});
};
Super.prototype.waitUntilVisible = function (selector: string, options?: WaitTimeoutOptions) {
return this.waitForSelector(selector, {
...options,
visible: true,
});
};
Super.prototype.waitWhileVisible = function (selector: string, options?: WaitTimeoutOptions) {
return this.waitForSelector(selector, {
...options,
hidden: true,
});
};

View File

@ -0,0 +1,184 @@
import { CDPSession, HTTPRequest, HTTPResponse, Page, TracingOptions, WaitForOptions, WaitTimeoutOptions } from 'puppeteer-core';
import { KeysOfType, Prototype } from '../../../typings/chrome-aws-lambda';
let Super: Prototype<Page> = null;
try {
Super = require('puppeteer/lib/cjs/puppeteer/common/Page.js').Page;
} catch (error) {
Super = require('puppeteer-core/lib/cjs/puppeteer/common/Page.js').Page;
}
Super.prototype.block = function (predicates: string[]) {
return ((this as any)._client as CDPSession).send('Network.setBlockedURLs', {
urls: predicates
});
};
Super.prototype.clear = function (selector: string) {
return this.mainFrame().clear(selector);
};
Super.prototype.clickAndWaitForNavigation = function (selector: string, options?: WaitForOptions) {
return this.mainFrame().clickAndWaitForNavigation(selector, options);
};
Super.prototype.clickAndWaitForRequest = function (selector: string, predicate: string | RegExp | ((request: HTTPRequest) => boolean | Promise<boolean>), options?: WaitTimeoutOptions) {
return this.mainFrame().clickAndWaitForRequest(selector, predicate as any, options);
};
Super.prototype.clickAndWaitForResponse = function (selector: string, predicate: string | RegExp | ((request: HTTPResponse) => boolean | Promise<boolean>), options?: WaitTimeoutOptions) {
return this.mainFrame().clickAndWaitForResponse(selector, predicate as any, options);
};
Super.prototype.count = function (selector: string) {
return this.mainFrame().count(selector);
};
Super.prototype.exists = function (selector: string) {
return this.mainFrame().exists(selector);
};
Super.prototype.fillFormByLabel = function <T extends Record<string, boolean | string | string[]>>(selector: string, data: T) {
return this.mainFrame().fillFormByLabel(selector, data);
};
Super.prototype.fillFormByName = function <T extends Record<string, boolean | string | string[]>>(selector: string, data: T) {
return this.mainFrame().fillFormByName(selector, data);
};
Super.prototype.fillFormBySelector = function <T extends Record<string, boolean | string | string[]>>(selector: string, data: T) {
return this.mainFrame().fillFormBySelector(selector, data);
};
Super.prototype.fillFormByXPath = function <T extends Record<string, boolean | string | string[]>>(selector: string, data: T) {
return this.mainFrame().fillFormByXPath(selector, data);
};
Super.prototype.number = function <T = HTMLElement>(selector: string, decimal: string = '.', property: KeysOfType<T, string> = 'textContent' as any) {
return this.mainFrame().number(selector, decimal, property);
};
Super.prototype.selectByLabel = function (selector: string, ...values: string[]) {
return this.mainFrame().selectByLabel(selector, ...values);
};
Super.prototype.string = function <T = HTMLElement>(selector: string, property: KeysOfType<T, string> = 'textContent' as any) {
return this.mainFrame().string(selector, property);
};
Super.prototype.waitForInflightRequests = function (requests: number = 0, alpha: number = 500, omega: number = 500, options?: WaitTimeoutOptions) {
let result: Record<string, Function> = {
reject: null,
resolve: null,
};
let timeout: NodeJS.Timeout;
let timeoutAlpha: NodeJS.Timeout;
let timeoutOmega: NodeJS.Timeout;
if (options == null) {
options = {
timeout: (this as any)._timeoutSettings.navigationTimeout(),
};
}
let inflight = 0;
const check = () => {
if (inflight <= Math.max(0, requests)) {
if (timeoutOmega !== undefined) {
clearTimeout(timeoutOmega);
}
timeoutOmega = setTimeout(onTimeoutOmega, omega);
}
};
const clear = () => {
if (timeout !== undefined) {
clearTimeout(timeout);
}
if (timeoutAlpha !== undefined) {
clearTimeout(timeoutAlpha);
}
if (timeoutOmega !== undefined) {
clearTimeout(timeoutOmega);
}
this.off('request', onRequestStarted);
this.off('requestfailed', onRequestSettled);
this.off('requestfinished', onRequestSettled);
};
function onRequestStarted() {
if (timeoutAlpha !== undefined) {
clearTimeout(timeoutAlpha);
}
if (timeoutOmega !== undefined) {
clearTimeout(timeoutOmega);
}
++inflight;
}
function onRequestSettled() {
if (inflight > 0) {
--inflight;
}
check();
}
function onTimeout() {
clear(); return result.reject(new Error(`Navigation timeout of ${options.timeout} ms exceeded.`));
}
function onTimeoutAlpha() {
clear(); return result.resolve();
}
function onTimeoutOmega() {
clear(); return result.resolve();
}
this.on('request', onRequestStarted);
this.on('requestfailed', onRequestSettled);
this.on('requestfinished', onRequestSettled);
if (options.timeout !== 0) {
timeout = setTimeout(onTimeout, options.timeout);
}
timeoutAlpha = setTimeout(onTimeoutAlpha, alpha);
return new Promise((resolve, reject) => {
result.reject = reject;
result.resolve = resolve;
});
};
Super.prototype.waitForText = function (predicate: string, options?: WaitTimeoutOptions) {
return this.mainFrame().waitForText(predicate, options);
};
Super.prototype.waitUntilVisible = function (selector: string, options?: WaitTimeoutOptions) {
return this.mainFrame().waitUntilVisible(selector, options);
};
Super.prototype.waitWhileVisible = function (selector: string, options?: WaitTimeoutOptions) {
return this.mainFrame().waitWhileVisible(selector, options);
};
Super.prototype.withTracing = function (options: TracingOptions, callback: (page: Page) => Promise<any>): Promise<Buffer> {
return this.tracing.start(options).then(async () => {
if (typeof callback === 'function') {
await callback(this);
}
return await this.tracing.stop();
});
};

42
tsconfig.json Normal file
View File

@ -0,0 +1,42 @@
{
"compileOnSave": false,
"compilerOptions": {
"alwaysStrict": true,
"declaration": true,
"declarationDir": "build",
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": false,
"inlineSourceMap": false,
"lib": ["dom", "es2020"],
"listEmittedFiles": false,
"listFiles": false,
"module": "commonjs",
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"outDir": "build",
"pretty": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"strictFunctionTypes": true,
"strictNullChecks": false,
"target": "es2020",
"traceResolution": false,
"typeRoots": ["node_modules/@types", "typings"],
"types": ["node"]
},
"exclude": ["build", "node_modules", "tmp"],
"include": ["source"],
"typeAcquisition": {
"enable": true
}
}

471
typings/chrome-aws-lambda.d.ts vendored Normal file
View File

@ -0,0 +1,471 @@
import { Page } from 'puppeteer-core';
export type Hook = (page: Page) => Promise<Page>;
export type KeysOfType<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T]
export type Prototype<T = any> = T & { prototype: T };
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
declare module 'puppeteer-core' {
interface Browser {
/**
* Returns the browser default page.
*
* @param hooks - Optional hooks to apply on the page.
*/
defaultPage(...hooks: Hook[]): Promise<Page>;
/**
* Returns a new page overloaded with browser-context methods.
*
* @param hooks - Optional hooks to apply on the new page.
*/
newPage(...hooks: Hook[]): Promise<Page>;
}
interface BrowserContext {
/**
* Returns the browser context default page.
*
* @param hooks - Optional hooks to apply on the page.
*/
defaultPage(...hooks: Hook[]): Promise<Page>;
/**
* Returns a new page overloaded with browser-context methods.
*
* @param hooks - Optional hooks to apply on the new page.
*/
newPage(...hooks: Hook[]): Promise<Page>;
}
interface ElementHandle {
/**
* Selects all text in a editable element and clears it.
*/
clear(): Promise<void>;
/**
* Clicks an element and waits for navigation to finish.
*
* @param options - Options to configure when the navigation is consided finished.
*/
clickAndWaitForNavigation(options?: WaitForOptions): Promise<HTTPResponse>;
/**
* Clicks an element and waits for a request to be initiated.
*
* @param predicate - URL pattern to wait for, wildcards `*` are allowed.
* @param options - Optional waiting parameters.
*/
clickAndWaitForRequest(predicate: string | RegExp, options?: WaitTimeoutOptions): Promise<HTTPRequest>;
/**
* Clicks an element and waits for a request to be initiated.
*
* @param predicate - Predicate to wait for.
* @param options - Optional waiting parameters.
*/
clickAndWaitForRequest(predicate: ((request: HTTPRequest) => boolean | Promise<boolean>), options?: WaitTimeoutOptions): Promise<HTTPRequest>;
/**
* Clicks an element and waits for a request to be finalized.
*
* @param predicate - URL pattern to wait for, wildcards `*` are allowed.
* @param options - Optional waiting parameters.
*/
clickAndWaitForResponse(predicate: string | RegExp, options?: WaitTimeoutOptions): Promise<HTTPResponse>;
/**
* Clicks an element and waits for a request to be finalized.
*
* @param predicate - Predicate to wait for.
* @param options - Optional waiting parameters.
*/
clickAndWaitForResponse(predicate: ((request: HTTPResponse) => boolean | Promise<boolean>), options?: WaitTimeoutOptions): Promise<HTTPResponse>;
/**
* Fills a `form` with a variable number of inputs and returns its actual filled state.
*
* @param data - Data to fill the form, as a label-value[s] map.
*/
fillFormByLabel<T extends Record<string, boolean | string | string[]>>(data: T): Promise<Record<keyof T, string[]>>;
/**
* Fills a `form` with a variable number of inputs and returns its actual filled state.
*
* @param data - Data to fill the form, as a name-value[s] map.
*/
fillFormByName<T extends Record<string, boolean | string | string[]>>(data: T): Promise<Record<keyof T, string[]>>;
/**
* Fills a `form` with a variable number of inputs and returns its actual filled state.
*
* @param data - Data to fill the form, as a selector-value[s] map.
*/
fillFormBySelector<T extends Record<string, boolean | string | string[]>>(data: T): Promise<Record<keyof T, string[]>>;
/**
* Fills a `form` with a variable number of inputs and returns its actual filled state.
*
* @param data - Data to fill the form, as a XPath selector-value[s] map.
*/
fillFormByXPath<T extends Record<string, boolean | string | string[]>>(data: T): Promise<Record<keyof T, string[]>>;
/**
* Returns the `innerHTML` property of the element.
*/
getInnerHTML(): Promise<string>;
/**
* Returns the `innerText` property of the element.
*/
getInnerText(): Promise<string>;
/**
* Returns normalized number(s) found in the given element.
*
* @param decimal - Decimal separator to use, defaults to `.`.
* @param property - Element property to extract content from, defaults to `textContent`.
*/
number(decimal?: string, property?: any): Promise<number[]>;
/**
* Selects multiple `select` options by label and returns the values of the actual selection.
*
* @param values - Option labels to select.
*/
selectByLabel(...values: string[]): Promise<string[]>;
/**
* Returns normalized text found in the given element.
*
* @param property - Element property to extract content from, defaults to `textContent`.
*/
string(property?: any): Promise<string>;
}
interface Frame {
/**
* Selects all text in a editable element and clears it.
*
* @param selector - Selector to query for.
*/
clear(selector: string): Promise<void>;
/**
* Clicks an element and waits for navigation to finish.
*
* @param selector - Selector to query for.
* @param options - Options to configure when the navigation is consided finished.
*/
clickAndWaitForNavigation(selector: string, options?: WaitForOptions): Promise<HTTPResponse>;
/**
* Clicks an element and waits for a request to be initiated.
*
* @param selector - Selector to query for.
* @param pattern - URL pattern to wait for, wildcards `*` are allowed.
* @param options - Optional waiting parameters.
*/
clickAndWaitForRequest(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions): Promise<HTTPRequest>;
/**
* Clicks an element and waits for a request to be initiated.
*
* @param selector - Selector to query for.
* @param pattern - Predicate to wait for.
* @param options - Optional waiting parameters.
*/
clickAndWaitForRequest(selector: string, predicate: ((request: HTTPRequest) => boolean | Promise<boolean>), options?: WaitTimeoutOptions): Promise<HTTPRequest>;
/**
* Clicks an element and waits for a request to be finalized.
*
* @param selector - Selector to query for.
* @param predicate - URL pattern to wait for, wildcards `*` are allowed.
* @param options - Optional waiting parameters.
*/
clickAndWaitForResponse(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions): Promise<HTTPResponse>;
/**
* Clicks an element and waits for a request to be finalized.
*
* @param selector - Selector to query for.
* @param predicate - Predicate to wait for.
* @param options - Optional waiting parameters.
*/
clickAndWaitForResponse(selector: string, predicate: ((request: HTTPResponse) => boolean | Promise<boolean>), options?: WaitTimeoutOptions): Promise<HTTPResponse>;
/**
* Returns the total number of elements that match the selector.
*
* @param selector - Selector to query for.
*/
count(selector: string): Promise<number>;
/**
* Checks whether at least one element matching the selector exists.
*
* @param selector - Selector to query for.
*/
exists(selector: string): Promise<boolean>;
/**
* Fills a `form` with a variable number of inputs and returns its actual filled state.
*
* @param selector - Selector to query the `form` element for.
* @param data - Data to fill the form, as a label-value[s] map.
*/
fillFormByLabel<T extends Record<string, boolean | string | string[]>>(selector: string, data: T): Promise<Record<keyof T, string[]>>;
/**
* Fills a `form` with a variable number of inputs and returns its actual filled state.
*
* @param selector - Selector to query the `form` element for.
* @param data - Data to fill the form, as a name-value[s] map.
*/
fillFormByName<T extends Record<string, boolean | string | string[]>>(selector: string, data: T): Promise<Record<keyof T, string[]>>;
/**
* Fills a `form` with a variable number of inputs and returns its actual filled state.
*
* @param selector - Selector to query the `form` element for.
* @param data - Data to fill the form, as a selector-value[s] map.
*/
fillFormBySelector<T extends Record<string, boolean | string | string[]>>(selector: string, data: T): Promise<Record<keyof T, string[]>>;
/**
* Fills a `form` with a variable number of inputs and returns its actual filled state.
*
* @param selector - Selector to query the `form` element for.
* @param data - Data to fill the form, as a XPath selector-value[s] map.
*/
fillFormByXPath<T extends Record<string, boolean | string | string[]>>(selector: string, data: T): Promise<Record<keyof T, string[]>>;
/**
* Returns normalized number(s) found in the given element.
*
* @param selector - Selector to query for.
* @param decimal - Decimal separator to use, defaults to `.`.
* @param property - Element property to extract content from, defaults to `textContent`.
*/
number<T = HTMLElement>(selector: string, decimal?: string, property?: KeysOfType<T, string>): Promise<number[]>;
/**
* Selects multiple `select` options by label and returns the values of the actual selection.
*
* @param selector - Selector to query the `select` element for.
* @param values - Option labels to select.
*/
selectByLabel(selector: string, ...values: string[]): Promise<string[]>;
/**
* Returns normalized text found in the given selector.
*
* @param selector - Selector to query for.
* @param property - Element property to extract content from, defaults to `textContent`.
*/
string<T = HTMLElement>(selector: string, property?: KeysOfType<T, string>): Promise<string>;
/**
* Wait for a string to be present and visible.
*
* @param predicate - String to wait for.
* @param options - Optional waiting parameters.
*/
waitForText(predicate: string, options?: WaitTimeoutOptions): Promise<ElementHandle<Node>>;
/**
* Waits for element to be present in DOM and to be visible.
*
* @param selector - Selector to query for.
* @param options - Optional waiting parameters.
*/
waitUntilVisible(selector: string, options?: WaitTimeoutOptions): Promise<ElementHandle<Node>>;
/**
* Waits for element to not be found in the DOM or to be hidden.
*
* @param selector - Selector to query for.
* @param options - Optional waiting parameters.
*/
waitWhileVisible(selector: string, options?: WaitTimeoutOptions): Promise<ElementHandle<Node>>;
}
interface Page {
/**
* Blocks URLs from loading without initializing request interception.
* Experimental: https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-setBlockedURLs
*
* @param patterns - URL patterns to block, wildcards `*` are allowed.
*/
block(patterns: string[]): Promise<void>;
/**
* Selects all text in a editable element and clears it.
*
* @param selector - Selector to query for.
*/
clear(selector: string): Promise<void>;
/**
* Clicks an element and waits for navigation to finish.
*
* @param selector - Selector to query for.
* @param options - Options to configure when the navigation is consided finished.
*/
clickAndWaitForNavigation(selector: string, options?: WaitForOptions): Promise<HTTPResponse>;
/**
* Clicks an element and waits for a request to be initiated.
*
* @param selector - Selector to query for.
* @param predicate - URL pattern to wait for, wildcards `*` are allowed.
* @param options - Optional waiting parameters.
*/
clickAndWaitForRequest(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions): Promise<HTTPRequest>;
/**
* Clicks an element and waits for a request to be initiated.
*
* @param selector - Selector to query for.
* @param predicate - Predicate to wait for.
* @param options - Optional waiting parameters.
*/
clickAndWaitForRequest(selector: string, predicate: ((request: HTTPRequest) => boolean | Promise<boolean>), options?: WaitTimeoutOptions): Promise<HTTPRequest>;
/**
* Clicks an element and waits for a request to be finalized.
*
* @param selector - Selector to query for.
* @param predicate - URL pattern to wait for, wildcards `*` are allowed.
* @param options - Optional waiting parameters.
*/
clickAndWaitForResponse(selector: string, predicate: string | RegExp, options?: WaitTimeoutOptions): Promise<HTTPResponse>;
/**
* Clicks an element and waits for a request to be finalized.
*
* @param selector - Selector to query for.
* @param predicate - Predicate to wait for.
* @param options - Optional waiting parameters.
*/
clickAndWaitForResponse(selector: string, predicate: ((request: HTTPResponse) => boolean | Promise<boolean>), options?: WaitTimeoutOptions): Promise<HTTPResponse>;
/**
* Returns the total number of elements that match the selector.
*
* @param selector - Selector to query for.
*/
count(selector: string): Promise<number>;
/**
* Checks whether at least one element matching the selector exists.
*
* @param selector - Selector to query for.
*/
exists(selector: string): Promise<boolean>;
/**
* Fills a `form` with a variable number of inputs and returns its actual filled state.
*
* @param selector - Selector to query the `form` element for.
* @param data - Data to fill the form, as a label-value[s] map.
*/
fillFormByLabel<T extends Record<string, boolean | string | string[]>>(selector: string, data: T): Promise<Record<keyof T, string[]>>;
/**
* Fills a `form` with a variable number of inputs and returns its actual filled state.
*
* @param selector - Selector to query the `form` element for.
* @param data - Data to fill the form, as a name-value[s] map.
*/
fillFormByName<T extends Record<string, boolean | string | string[]>>(selector: string, data: T): Promise<Record<keyof T, string[]>>;
/**
* Fills a `form` with a variable number of inputs and returns its actual filled state.
*
* @param selector - Selector to query the `form` element for.
* @param data - Data to fill the form, as a selector-value[s] map.
*/
fillFormBySelector<T extends Record<string, boolean | string | string[]>>(selector: string, data: T): Promise<Record<keyof T, string[]>>;
/**
* Fills a `form` with a variable number of inputs and returns its actual filled state.
*
* @param selector - Selector to query the `form` element for.
* @param data - Data to fill the form, as a XPath selector-value[s] map.
*/
fillFormByXPath<T extends Record<string, boolean | string | string[]>>(selector: string, data: T): Promise<Record<keyof T, string[]>>;
/**
* Returns normalized number(s) found in the given element.
*
* @param selector - Selector to query for.
* @param decimal - Decimal separator to use, defaults to `.`.
* @param property - Element property to extract content from, defaults to `textContent`.
*/
number<T = HTMLElement>(selector: string, decimal?: string, property?: KeysOfType<T, string>): Promise<number[]>;
/**
* Selects multiple `select` options by label and returns the values of the actual selection.
*
* @param selector - Selector to query the `select` element for.
* @param values - Option labels to select.
*/
selectByLabel(selector: string, ...values: string[]): Promise<string[]>;
/**
* Returns normalized text found in the given selector.
*
* @param selector - Selector to query for.
* @param property - Element property to extract content from, defaults to `textContent`.
*/
string<T = HTMLElement>(selector: string, property?: KeysOfType<T, string>): Promise<string>;
/**
* Wait for the total number of inflight requests to not exceed a specific threshold.
*
* @param requests Maximum number of inflight requests, defaults to 0.
* @param alpha The number of milliseconds to wait for any requests to be issued, defaults to `500` ms.
* @param omega The number of milliseconds to wait for any outstanding inflight requests to settle, defaults to `500` ms.
* @param options Optional waiting parameters. Defaults to the navigation timeout, pass 0 to disable.
*
* @author [mifi](https://github.com/puppeteer/puppeteer/issues/1353#issuecomment-629271737)
* @author [DevBrent](https://github.com/puppeteer/puppeteer/issues/1353#issuecomment-648299486)
*/
waitForInflightRequests(requests?: number, alpha?: number, omega?: number, options?: WaitTimeoutOptions): Promise<void>;
/**
* Wait for a string to be present and visible.
*
* @param predicate - String to wait for.
* @param options - Optional waiting parameters.
*/
waitForText(predicate: string, options?: WaitTimeoutOptions): Promise<ElementHandle<Node>>;
/**
* Waits for element to be present in DOM and to be visible.
*
* @param selector - Selector to query for.
* @param options - Optional waiting parameters.
*/
waitUntilVisible(selector: string, options?: WaitTimeoutOptions): Promise<ElementHandle<Node>>;
/**
* Waits for element to not be found in the DOM or to be hidden.
*
* @param selector - Selector to query for.
* @param options - Optional waiting parameters.
*/
waitWhileVisible(selector: string, options?: WaitTimeoutOptions): Promise<ElementHandle<Node>>;
/**
* Encapsulates the callback execution in a tracing session.
*
* @param options Tracing options.
* @param callback Callback to execute.
*/
withTracing(options: TracingOptions, callback: (page: Page) => Promise<any>): Promise<Buffer>;
}
}