├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── publish.yml ├── .gitignore ├── .nvmrc ├── Dockerfile ├── LICENSE.md ├── README.md ├── SECURITY.md ├── docker ├── Dockerfile └── docker-custom-entrypoint.sh ├── gulpfile.js ├── images ├── content.png ├── screenshot.png └── script.png ├── index.js ├── nodes └── Puppeteer │ ├── Puppeteer.node.options.ts │ ├── Puppeteer.node.ts │ ├── puppeteer.svg │ └── types.d.ts ├── package-lock.json ├── package.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [package.json] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.yml] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: '22.x' 13 | registry-url: 'https://registry.npmjs.org' 14 | - run: npm ci 15 | - run: npm run build 16 | - run: npm publish 17 | env: 18 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .tmp 4 | tmp 5 | dist 6 | npm-debug.log* 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.n8n.io/n8nio/n8n 2 | 3 | USER root 4 | 5 | # Install Chrome dependencies and Chrome 6 | RUN apk add --no-cache \ 7 | chromium \ 8 | nss \ 9 | glib \ 10 | freetype \ 11 | freetype-dev \ 12 | harfbuzz \ 13 | ca-certificates \ 14 | ttf-freefont \ 15 | udev \ 16 | ttf-liberation \ 17 | font-noto-emoji 18 | 19 | # Tell Puppeteer to use installed Chrome instead of downloading it 20 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \ 21 | PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser 22 | 23 | # Install n8n-nodes-puppeteer in a permanent location 24 | COPY . /opt/n8n-custom-nodes/node_modules/n8n-nodes-puppeteer 25 | RUN cd /opt/n8n-custom-nodes/node_modules/n8n-nodes-puppeteer && \ 26 | npm install && \ 27 | npm run build && \ 28 | chown -R node:node /opt/n8n-custom-nodes 29 | 30 | # Copy our custom entrypoint 31 | COPY docker/docker-custom-entrypoint.sh /docker-custom-entrypoint.sh 32 | RUN chmod +x /docker-custom-entrypoint.sh && \ 33 | chown node:node /docker-custom-entrypoint.sh 34 | 35 | USER node 36 | 37 | ENTRYPOINT ["/docker-custom-entrypoint.sh"] 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nicholas Penree 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # n8n-nodes-puppeteer 2 | 3 | ![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png) 4 | 5 | [n8n](https://www.n8n.io) node for browser automation using [Puppeteer](https://pptr.dev/). Execute custom scripts, capture screenshots and PDFs, scrape content, and automate web interactions using Chrome/Chromium's DevTools Protocol. Full access to Puppeteer's API plus n8n's Code node capabilities makes this node powerful for any browser automation task. 6 | 7 | ## How to install 8 | 9 | ### Community Nodes (Recommended) 10 | 11 | For n8n version 0.187 and later, you can install this node through the Community Nodes panel: 12 | 13 | 1. Go to **Settings > Community Nodes** 14 | 2. Select **Install** 15 | 3. Enter `n8n-nodes-puppeteer` in **Enter npm package name** 16 | 4. Agree to the [risks](https://docs.n8n.io/integrations/community-nodes/risks/) of using community nodes 17 | 5. Select **Install** 18 | 19 | ### Docker Installation (Recommended for Production) 20 | 21 | We provide a ready-to-use Docker setup in the `docker/` directory that includes all necessary dependencies and configurations: 22 | 23 | 1. Clone this repository or copy the following files to your project: 24 | - `docker/Dockerfile` 25 | - `docker/docker-custom-entrypoint.sh` 26 | 27 | 2. Build your Docker image: 28 | ```bash 29 | docker build -t n8n-puppeteer -f docker/Dockerfile docker/ 30 | ``` 31 | 32 | 3. Run the container: 33 | ```bash 34 | docker run -it \ 35 | -p 5678:5678 \ 36 | -v ~/.n8n:/home/node/.n8n \ 37 | n8n-puppeteer 38 | ``` 39 | 40 | ### Manual Installation 41 | 42 | For a standard installation without Docker: 43 | 44 | ```bash 45 | # Navigate to your n8n root directory 46 | cd /path/to/n8n 47 | 48 | # Install the package 49 | npm install n8n-nodes-puppeteer 50 | ``` 51 | 52 | Note: By default, when Puppeteer is installed, it downloads a compatible version of Chromium. While this works, it increases installation size and may not include necessary system dependencies. For production use, we recommend either using the Docker setup above or installing system Chrome/Chromium and setting the `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true` environment variable. 53 | 54 | ## Browser Setup Options 55 | 56 | ### 1. Local Browser (Docker Setup - Recommended) 57 | 58 | The included Docker setup provides the most reliable way to run Chrome/Chromium with all necessary dependencies. It uses Alpine Linux's Chromium package and includes all required fonts and libraries. 59 | 60 | ### 2. Remote Browser (Alternative for Cloud) 61 | 62 | You can also connect to an external Chrome instance using the "Browser WebSocket Endpoint" option. This approach: 63 | - Eliminates the need for Chrome dependencies in your n8n environment 64 | - Simplifies deployment and maintenance 65 | - Provides better resource isolation 66 | - Works great for cloud and containerized deployments 67 | 68 | Options include: 69 | - **Managed Services**: Use [browserless](https://browserless.io) or [browsercloud](https://browsercloud.io) 70 | - **Self-Hosted**: Run your own [browser container](https://docs.browserless.io/docker/config): 71 | ```bash 72 | docker run -p 3000:3000 -e "TOKEN=6R0W53R135510" ghcr.io/browserless/chromium 73 | ``` 74 | 75 | To use a remote browser, enable "Browser WebSocket Endpoint" in any Puppeteer node and enter your WebSocket URL (e.g., `ws://browserless:3000?token=6R0W53R135510`). 76 | 77 | ## Troubleshooting 78 | 79 | If you see errors about missing shared libraries (like `libgobject-2.0.so.0` or `libnss3.so`), either: 80 | 81 | 1. Install the missing Chrome dependencies 82 | 2. Switch to using a remote browser with the WebSocket endpoint option 83 | 84 | For additional help, see [Puppeteer's troubleshooting guide](https://pptr.dev/troubleshooting). 85 | 86 | ## Node Reference 87 | 88 | - **Operations** 89 | 90 | - Get the full HTML contents of the page 91 | - Capture the contents of a page as a PDF document 92 | - Capture screenshot of all or part of the page 93 | - Execute custom script to interact with the page 94 | 95 | - **Options** 96 | 97 | - All Operations 98 | 99 | - **Batch Size**: Maximum number of pages to open simultaneously. More pages will consume more memory and CPU. 100 | - **Browser WebSocket Endpoint**: The WebSocket URL of the browser to connect to. When configured, puppeteer will skip the browser launch and connect to the browser instance. 101 | - **Emulate Device**: Allows you to specify a [device](https://github.com/puppeteer/puppeteer/blob/main/src/common/DeviceDescriptors.ts) to emulate when requesting the page. 102 | - **Executable Path**: A path where Puppeteer expects to find the bundled browser. Has no effect when 'Browser WebSocket Endpoint' is set. 103 | - **Extra Headers**: Allows you add additional headers when requesting the page. 104 | - **Timeout**: Allows you to specify the maximum navigation time in milliseconds. You can pass 0 to disable the timeout entirely. 105 | - **Wait Until**: Allows you to change how Puppeteer considers navigation completed. 106 | - `load`: The load event is fired. 107 | - `domcontentloaded`: The DOMContentLoaded event is fired. 108 | - `networkidle0`: No more than 0 connections for at least 500 ms. 109 | - `networkidle2`: No more than 2 connections for at least 500 ms. 110 | - **Page Caching**: Allows you to toggle whether pages should be cached when requesting. 111 | - **Headless mode**: Allows you to change whether to run browser runs in headless mode or not. 112 | - **Use Chrome Headless Shell**: Whether to run browser in headless shell mode. Defaults to false. Headless mode must be enabled. chrome-headless-shell must be in $PATH. 113 | - **Stealth mode**: When enabled, applies various techniques to make detection of headless Puppeteer harder. Powered by [puppeteer-extra-plugin-stealth](https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth). 114 | - **Launch Arguments**: Allows you to specify additional command line arguments passed to the browser instance. 115 | - **Proxy Server**: Allows Puppeteer to use a custom proxy configuration. You can specify a custom proxy configuration in three ways: 116 | By providing a semi-colon-separated mapping of list scheme to url/port pairs. 117 | For example, you can specify: 118 | 119 | http=foopy:80;ftp=foopy2 120 | 121 | to use HTTP proxy "foopy:80" for http URLs and HTTP proxy "foopy2:80" for ftp URLs. 122 | 123 | By providing a single uri with optional port to use for all URLs. 124 | For example: 125 | 126 | foopy:8080 127 | 128 | will use the proxy at foopy:8080 for all traffic. 129 | 130 | By using the special "direct://" value. 131 | 132 | direct://" will cause all connections to not use a proxy. 133 | 134 | - Get PDF 135 | - **File Name**: Allows you to specify the filename of the output file. 136 | - **Page Ranges** field: Allows you to specify paper ranges to print, e.g. 1-5, 8, 11-13. 137 | - **Scale**: Allows you to scale the rendering of the web page. Amount must be between 0.1 and 2 138 | - **Prefer CSS Page Size**: Give any CSS @page size declared in the page priority over what is declared in the width or height or format option. 139 | - **Format**: Allows you to specify the paper format types when printing a PDF. eg: Letter, A4. 140 | - **Height**: Allows you to set the height of paper. You can pass in a number or a string with a unit. 141 | - **Width**: Allows you to set the width of paper. You can pass in a number or a string with a unit. 142 | - **Landscape**: Allows you to control whether to show the header and footer 143 | - **Margin**: Allows you to specify top, left, right, and bottom margin. 144 | - **Display Header/Footer**: Allows you to specify whether to show the header and footer. 145 | - **Header Template**: Allows you to specify the HTML template for the print header. Should be valid HTML with the following classes used to inject values into them: 146 | - `date`: Formatted print date 147 | - `title`: Document title 148 | - `url`: Document location 149 | - `pageNumber` Current page number 150 | - `totalPages` Total pages in the document 151 | - **Footer Template**: Allows you to specify the HTML template for the print footer. Should be valid HTML with the following classes used to inject values into them: 152 | - `date`: Formatted print date 153 | - `title`: Document title 154 | - `url`: Document location 155 | - `pageNumber` Current page number 156 | - `totalPages` Total pages in the document 157 | - **Transparent Background**: Allows you to hide the default white background and allows generate PDFs with transparency. 158 | - **Background Graphic**: Allows you to include background graphics. 159 | - Get Screenshot 160 | - **File Name**: Allows you to specify the filename of the output file. 161 | - **Type** field: Allows you to specify the image format of the output file: 162 | - JPEG 163 | - PNG 164 | - WebP 165 | - **Quality**: Allows you to specify the quality of the image. 166 | - Accepts a value between 0-100. 167 | - Not applicable to PNG images. 168 | - **Full Page**: Allows you to capture a screen of the full scrollable content. 169 | 170 | ## Custom Scripts 171 | 172 | The Custom Script operation gives you complete control over Puppeteer to automate complex browser interactions, scrape data, generate PDFs/screenshots, and more. Scripts run in a sandboxed environment with access to the full Puppeteer API and n8n's Code node features. 173 | 174 | Before script execution, you can configure browser behavior using the operation's options like: 175 | 176 | - Emulate specific devices 177 | - Set custom headers 178 | - Enable stealth mode to avoid detection 179 | - Configure proxy settings 180 | - Set page load timeouts 181 | - And more 182 | 183 | Access Puppeteer-specific objects using: 184 | 185 | - `$page` - Current page instance 186 | - `$browser` - Browser instance 187 | - `$puppeteer` - Puppeteer library 188 | 189 | Plus all special variables and methods from the Code node are available. For a complete reference, see the [n8n documentation](https://docs.n8n.io/code-examples/methods-variables-reference/). Just like n8n's Code node, anything you `console.log` will be shown in the browser's console during test mode or in stdout when configured. 190 | 191 | ### Basic 192 | 193 | ```javascript 194 | // Navigate to an IP lookup service 195 | await $page.goto("https://httpbin.org/ip"); 196 | 197 | // Extract the IP address from the page content 198 | const ipData = await $page.evaluate(() => { 199 | const response = document.body.innerText; 200 | const parsed = JSON.parse(response); 201 | return parsed.origin; // Extract the 'origin' field, which typically contains the IP address 202 | }); 203 | 204 | console.log("Hello, world!"); 205 | 206 | console.log("IP Address", ipData); 207 | 208 | // Return the result in the required format (array) 209 | return [{ ip: ipData, ...$json }]; 210 | ``` 211 | 212 | ### Storing and re-using cookies 213 | 214 | #### Node 1 215 | 216 | ```javascript 217 | await $page.goto("https://www.example.com/login"); 218 | 219 | // Perform login 220 | await $page.type("#login-username", "user"); 221 | await $page.type("#login-password", "pass"); 222 | await $page.click("#login-button"); 223 | 224 | // Store cookies for later use 225 | const cookies = await $page.cookies(); 226 | 227 | return [{ cookies }]; 228 | ``` 229 | 230 | #### Node 2 231 | 232 | ```javascript 233 | const { cookies } = $input.first().json; 234 | 235 | // Restore cookies 236 | await $page.setCookie(...cookies); 237 | 238 | // Navigate to authenticated page 239 | await $page.goto("https://example.com/protected-page"); 240 | 241 | // Perform authenticated operations 242 | const data = await $page.evaluate(() => { 243 | return document.querySelector(".protected-content").textContent; 244 | }); 245 | 246 | return [{ data }]; 247 | ``` 248 | 249 | ### Working with Binary Data 250 | 251 | ```javascript 252 | await $page.goto("https://www.google.com"); 253 | const imageData = await $page.screenshot({ type: "png", encoding: "base64" }); 254 | return [ 255 | { 256 | binary: { 257 | screenshot: { 258 | data: imageData, 259 | mimeType: "image/png", 260 | fileName: "screenshot.png", 261 | }, 262 | }, 263 | }, 264 | ]; 265 | ``` 266 | 267 | ## Screenshots 268 | 269 | ### Run Custom Script 270 | 271 | ![](images/script.png) 272 | 273 | ### Get Page Content 274 | 275 | ![](images/content.png) 276 | 277 | ### Get Screenshot 278 | 279 | ![](images/screenshot.png) 280 | 281 | ## License 282 | 283 | MIT License 284 | 285 | Copyright (c) 2022-2024 Nicholas Penree 286 | 287 | Permission is hereby granted, free of charge, to any person obtaining a copy 288 | of this software and associated documentation files (the "Software"), to deal 289 | in the Software without restriction, including without limitation the rights 290 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 291 | copies of the Software, and to permit persons to whom the Software is 292 | furnished to do so, subject to the following conditions: 293 | 294 | The above copyright notice and this permission notice shall be included in all 295 | copies or substantial portions of the Software. 296 | 297 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 298 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 299 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 300 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 301 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 302 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 303 | SOFTWARE. 304 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following versions of `n8n-nodes-puppeteer` are currently supported with security updates: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.1.x | :white_check_mark: | 10 | | 1.0.x | :white_check_mark: | 11 | | 0.8.x | :x: | 12 | | < 0.8 | :x: | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | We take security vulnerabilities seriously. If you discover a security issue in n8n-nodes-puppeteer, please report it by: 17 | 18 | 1. **Email**: Send details to nick@penree.com 19 | 2. **GitHub**: For less sensitive issues, open a GitHub issue 20 | 21 | When reporting, please provide: 22 | - A description of the vulnerability 23 | - Steps to reproduce the issue 24 | - Possible impact of the vulnerability 25 | - Any suggested fixes (if available) 26 | 27 | ### What to Expect 28 | 29 | - **Initial Response**: You will receive an acknowledgment within 48 hours 30 | - **Updates**: We will keep you informed of our progress every 3-5 days 31 | - **Resolution Timeline**: We aim to resolve critical issues within 7 days 32 | 33 | ### Process 34 | 35 | 1. Once received, we will validate and assess the reported vulnerability 36 | 2. We will work on a fix and conduct testing 37 | 3. A security release will be issued if needed 38 | 4. Public disclosure will be coordinated with the reporter 39 | 40 | Your efforts to responsibly disclose your findings are appreciated and will be taken into account to help protect our users. 41 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.n8n.io/n8nio/n8n 2 | 3 | USER root 4 | 5 | # Install Chrome dependencies and Chrome 6 | RUN apk add --no-cache \ 7 | chromium \ 8 | nss \ 9 | glib \ 10 | freetype \ 11 | freetype-dev \ 12 | harfbuzz \ 13 | ca-certificates \ 14 | ttf-freefont \ 15 | udev \ 16 | ttf-liberation \ 17 | font-noto-emoji 18 | 19 | # Tell Puppeteer to use installed Chrome instead of downloading it 20 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \ 21 | PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser 22 | 23 | # Install n8n-nodes-puppeteer in a permanent location 24 | RUN mkdir -p /opt/n8n-custom-nodes && \ 25 | cd /opt/n8n-custom-nodes && \ 26 | npm install n8n-nodes-puppeteer && \ 27 | chown -R node:node /opt/n8n-custom-nodes 28 | 29 | # Copy our custom entrypoint 30 | COPY docker-custom-entrypoint.sh /docker-custom-entrypoint.sh 31 | RUN chmod +x /docker-custom-entrypoint.sh && \ 32 | chown node:node /docker-custom-entrypoint.sh 33 | 34 | USER node 35 | 36 | ENTRYPOINT ["/docker-custom-entrypoint.sh"] 37 | -------------------------------------------------------------------------------- /docker/docker-custom-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | print_banner() { 4 | echo "----------------------------------------" 5 | echo "n8n Puppeteer Node - Environment Details" 6 | echo "----------------------------------------" 7 | echo "Node.js version: $(node -v)" 8 | echo "n8n version: $(n8n --version)" 9 | 10 | # Get Chromium version specifically from the path we're using for Puppeteer 11 | CHROME_VERSION=$("$PUPPETEER_EXECUTABLE_PATH" --version 2>/dev/null || echo "Chromium not found") 12 | echo "Chromium version: $CHROME_VERSION" 13 | 14 | # Get Puppeteer version if installed 15 | PUPPETEER_PATH="/opt/n8n-custom-nodes/node_modules/n8n-nodes-puppeteer" 16 | if [ -f "$PUPPETEER_PATH/package.json" ]; then 17 | PUPPETEER_VERSION=$(node -p "require('$PUPPETEER_PATH/package.json').version") 18 | echo "n8n-nodes-puppeteer version: $PUPPETEER_VERSION" 19 | 20 | # Try to resolve puppeteer package from the n8n-nodes-puppeteer directory 21 | CORE_PUPPETEER_VERSION=$(cd "$PUPPETEER_PATH" && node -e "try { const version = require('puppeteer/package.json').version; console.log(version); } catch(e) { console.log('not found'); }") 22 | echo "Puppeteer core version: $CORE_PUPPETEER_VERSION" 23 | else 24 | echo "n8n-nodes-puppeteer: not installed" 25 | fi 26 | 27 | echo "Puppeteer executable path: $PUPPETEER_EXECUTABLE_PATH" 28 | echo "----------------------------------------" 29 | } 30 | 31 | # Add custom nodes to the NODE_PATH 32 | if [ -n "$N8N_CUSTOM_EXTENSIONS" ]; then 33 | export N8N_CUSTOM_EXTENSIONS="/opt/n8n-custom-nodes:${N8N_CUSTOM_EXTENSIONS}" 34 | else 35 | export N8N_CUSTOM_EXTENSIONS="/opt/n8n-custom-nodes" 36 | fi 37 | 38 | print_banner 39 | 40 | # Execute the original n8n entrypoint script 41 | exec /docker-entrypoint.sh "$@" 42 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const { src, dest } = require('gulp'); 2 | 3 | function copyIcons() { 4 | return src('nodes/**/*.{png,svg}') 5 | .pipe(dest('dist/nodes')); 6 | 7 | // src('nodes/**/*.{png,svg}') 8 | // .pipe(dest('dist/nodes')) 9 | 10 | // return src('credentials/**/*.{png,svg}') 11 | // .pipe(dest('dist/credentials')); 12 | } 13 | 14 | exports.default = copyIcons; 15 | -------------------------------------------------------------------------------- /images/content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drudge/n8n-nodes-puppeteer/023789e64f80abb65e22d3f13e4628a991c4e552/images/content.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drudge/n8n-nodes-puppeteer/023789e64f80abb65e22d3f13e4628a991c4e552/images/screenshot.png -------------------------------------------------------------------------------- /images/script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drudge/n8n-nodes-puppeteer/023789e64f80abb65e22d3f13e4628a991c4e552/images/script.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drudge/n8n-nodes-puppeteer/023789e64f80abb65e22d3f13e4628a991c4e552/index.js -------------------------------------------------------------------------------- /nodes/Puppeteer/Puppeteer.node.options.ts: -------------------------------------------------------------------------------- 1 | import { type INodeTypeDescription, NodeConnectionType } from 'n8n-workflow'; 2 | import { existsSync, readFileSync } from 'node:fs'; 3 | 4 | function isRunningInContainer(): boolean { 5 | try { 6 | // Method 1: Check for .dockerenv file 7 | if (existsSync('/.dockerenv')) { 8 | console.log('Puppeteer node: Container detected via .dockerenv file'); 9 | return true; 10 | } 11 | 12 | // Method 2: Check cgroup (Linux only) 13 | if (process.platform === 'linux') { 14 | try { 15 | const cgroupContent = readFileSync('/proc/1/cgroup', 'utf8'); 16 | if (cgroupContent.includes('docker') || cgroupContent.includes('kubepods')) { 17 | console.log('Puppeteer node: Container detected via cgroup content'); 18 | return true; 19 | } 20 | } catch (error) { 21 | console.log('Puppeteer node: cgroup check skipped'); 22 | } 23 | } 24 | 25 | // Method 3: Check common container environment variables 26 | if (process.env.KUBERNETES_SERVICE_HOST || 27 | process.env.DOCKER_CONTAINER || 28 | process.env.DOCKER_HOST) { 29 | console.log('Puppeteer node: Container detected via environment variables'); 30 | return true; 31 | } 32 | 33 | return false; 34 | } catch (error) { 35 | // If any error occurs during checks, log and return false 36 | console.log('Puppeteer node: Container detection failed:', (error as Error).message); 37 | return false; 38 | } 39 | } 40 | 41 | /** 42 | * Options to be displayed 43 | */ 44 | export const nodeDescription: INodeTypeDescription = { 45 | displayName: 'Puppeteer', 46 | name: 'puppeteer', 47 | group: ['puppeteer'], 48 | version: 1, 49 | description: 'Automate browser interactions using Puppeteer', 50 | defaults: { 51 | name: 'Puppeteer', 52 | color: '#125580', 53 | }, 54 | icon: 'file:puppeteer.svg', 55 | inputs: [NodeConnectionType.Main], 56 | outputs: [NodeConnectionType.Main], 57 | usableAsTool: true, 58 | properties: [ 59 | { 60 | displayName: 'URL', 61 | name: 'url', 62 | type: 'string', 63 | required: true, 64 | default: '', 65 | displayOptions: { 66 | show: { 67 | operation: ['getPageContent', 'getScreenshot', 'getPDF'], 68 | }, 69 | }, 70 | }, 71 | { 72 | displayName: 'Operation', 73 | name: 'operation', 74 | type: 'options', 75 | options: [ 76 | { 77 | name: 'Get Page Content', 78 | value: 'getPageContent', 79 | description: 'Gets the full HTML contents of the page', 80 | }, 81 | { 82 | name: 'Get PDF', 83 | value: 'getPDF', 84 | description: 'Captures all or part of the page as a PDF', 85 | }, 86 | { 87 | name: 'Get Screenshot', 88 | value: 'getScreenshot', 89 | description: 'Captures all or part of the page as an image', 90 | }, 91 | { 92 | name: 'Run Custom Script', 93 | value: 'runCustomScript', 94 | description: 'Runs custom code to perform specific actions on the page', 95 | }, 96 | ], 97 | default: 'getPageContent', 98 | }, 99 | { 100 | displayName: 'Script Code', 101 | name: 'scriptCode', 102 | type: 'string', 103 | typeOptions: { 104 | // @ts-ignore 105 | editor: 'codeNodeEditor', 106 | editorLanguage: 'javaScript', 107 | }, 108 | required: true, 109 | default: 110 | '// Navigate to an IP lookup service\nawait $page.goto(\'https://httpbin.org/ip\');\n\n// Extract the IP address from the page content\nconst ipData = await $page.evaluate(() => {\n const response = document.body.innerText;\n const parsed = JSON.parse(response);\n return parsed.origin; // Extract the \'origin\' field, which typically contains the IP address\n});\n\nconsole.log("Hello, world!");\n\nconsole.log("IP Address", ipData);\n\n// Return the result in the required format\nreturn [{ ip: ipData, ...$json }];', 111 | description: 112 | 'JavaScript code to execute with Puppeteer. You have access to the $browser and $page objects, which represent the Puppeteer browser and page.', 113 | noDataExpression: false, 114 | displayOptions: { 115 | show: { 116 | operation: ['runCustomScript'], 117 | }, 118 | }, 119 | }, 120 | { 121 | displayName: 122 | 'Use $page, $browser, or $puppeteer vars to access Puppeteer. Special vars/methods are available.

Debug by using console.log() statements and viewing their output in the browser console.', 123 | name: 'notice', 124 | type: 'notice', 125 | displayOptions: { 126 | show: { 127 | operation: ['runCustomScript'], 128 | }, 129 | }, 130 | default: '', 131 | }, 132 | { 133 | displayName: 'Property Name', 134 | name: 'dataPropertyName', 135 | type: 'string', 136 | required: true, 137 | default: 'data', 138 | description: 139 | 'Name of the binary property in which to store the image or PDF data.', 140 | displayOptions: { 141 | show: { 142 | operation: ['getScreenshot', 'getPDF'], 143 | }, 144 | }, 145 | }, 146 | { 147 | displayName: 'Page Ranges', 148 | name: 'pageRanges', 149 | type: 'string', 150 | required: false, 151 | default: '', 152 | description: 'Paper ranges to print, e.g. 1-5, 8, 11-13.', 153 | displayOptions: { 154 | show: { 155 | operation: ['getPDF'], 156 | }, 157 | }, 158 | }, 159 | { 160 | displayName: 'Scale', 161 | name: 'scale', 162 | type: 'number', 163 | typeOptions: { 164 | minValue: 0.1, 165 | maxValue: 2, 166 | }, 167 | default: 1.0, 168 | required: true, 169 | description: 170 | 'Scales the rendering of the web page. Amount must be between 0.1 and 2.', 171 | displayOptions: { 172 | show: { 173 | operation: ['getPDF'], 174 | }, 175 | }, 176 | }, 177 | { 178 | displayName: 'Prefer CSS Page Size', 179 | name: 'preferCSSPageSize', 180 | type: 'boolean', 181 | required: true, 182 | default: true, 183 | displayOptions: { 184 | show: { 185 | operation: ['getPDF'], 186 | }, 187 | }, 188 | description: 189 | 'Give any CSS @page size declared in the page priority over what is declared in the width or height or format option.', 190 | }, 191 | { 192 | displayName: 'Format', 193 | name: 'format', 194 | type: 'options', 195 | options: [ 196 | { 197 | name: 'Letter', 198 | value: 'Letter', 199 | description: '8.5in x 11in', 200 | }, 201 | { 202 | name: 'Legal', 203 | value: 'Legal', 204 | description: '8.5in x 14in', 205 | }, 206 | { 207 | name: 'Tabloid', 208 | value: 'Tabloid', 209 | description: '11in x 17in', 210 | }, 211 | { 212 | name: 'Ledger', 213 | value: 'Ledger', 214 | description: '17in x 11in', 215 | }, 216 | { 217 | name: 'A0', 218 | 219 | value: 'A0', 220 | description: '33.1in x 46.8in', 221 | }, 222 | { 223 | name: 'A1', 224 | value: 'A1', 225 | description: '23.4in x 33.1in', 226 | }, 227 | { 228 | name: 'A2', 229 | value: 'A2', 230 | description: '16.54in x 23.4in', 231 | }, 232 | { 233 | name: 'A3', 234 | value: 'A3', 235 | description: '11.7in x 16.54in', 236 | }, 237 | { 238 | name: 'A4', 239 | value: 'A4', 240 | description: '8.27in x 11.7in', 241 | }, 242 | { 243 | name: 'A5', 244 | value: 'A5', 245 | description: '5.83in x 8.27in', 246 | }, 247 | { 248 | name: 'A6', 249 | value: 'A6', 250 | description: '4.13in x 5.83in', 251 | }, 252 | ], 253 | default: 'Letter', 254 | description: 255 | 'Valid paper format types when printing a PDF. eg: Letter, A4', 256 | displayOptions: { 257 | show: { 258 | operation: ['getPDF'], 259 | preferCSSPageSize: [false], 260 | }, 261 | }, 262 | }, 263 | { 264 | displayName: 'Height', 265 | name: 'height', 266 | type: 'string', 267 | default: '', 268 | required: false, 269 | description: 270 | 'Sets the height of paper. You can pass in a number or a string with a unit.', 271 | displayOptions: { 272 | show: { 273 | operation: ['getPDF'], 274 | preferCSSPageSize: [false], 275 | }, 276 | }, 277 | }, 278 | { 279 | displayName: 'Width', 280 | name: 'width', 281 | type: 'string', 282 | default: '', 283 | required: false, 284 | description: 285 | 'Sets the width of paper. You can pass in a number or a string with a unit.', 286 | displayOptions: { 287 | show: { 288 | operation: ['getPDF'], 289 | preferCSSPageSize: [false], 290 | }, 291 | }, 292 | }, 293 | { 294 | displayName: 'Landscape', 295 | name: 'landscape', 296 | type: 'boolean', 297 | required: true, 298 | default: true, 299 | displayOptions: { 300 | show: { 301 | operation: ['getPDF'], 302 | }, 303 | }, 304 | description: 'Whether to show the header and footer.', 305 | }, 306 | { 307 | displayName: 'Margin', 308 | name: 'margin', 309 | type: 'collection', 310 | placeholder: 'Add Margin', 311 | default: {}, 312 | description: 'Set the PDF margins.', 313 | displayOptions: { 314 | show: { 315 | operation: ['getPDF'], 316 | }, 317 | }, 318 | options: [ 319 | { 320 | displayName: 'Top', 321 | name: 'top', 322 | type: 'string', 323 | default: '', 324 | required: false, 325 | }, 326 | { 327 | displayName: 'Bottom', 328 | name: 'bottom', 329 | type: 'string', 330 | default: '', 331 | required: false, 332 | }, 333 | { 334 | displayName: 'Left', 335 | name: 'left', 336 | type: 'string', 337 | default: '', 338 | required: false, 339 | }, 340 | { 341 | displayName: 'Right', 342 | name: 'right', 343 | type: 'string', 344 | default: '', 345 | required: false, 346 | }, 347 | ], 348 | }, 349 | { 350 | displayName: 'Display Header/Footer', 351 | name: 'displayHeaderFooter', 352 | type: 'boolean', 353 | required: true, 354 | default: false, 355 | displayOptions: { 356 | show: { 357 | operation: ['getPDF'], 358 | }, 359 | }, 360 | description: 'Whether to show the header and footer.', 361 | }, 362 | { 363 | displayName: 'Header Template', 364 | name: 'headerTemplate', 365 | typeOptions: { 366 | rows: 5, 367 | }, 368 | type: 'string', 369 | default: "", 370 | description: `HTML template for the print header. Should be valid HTML with the following classes used to inject values into them: - date formatted print date 371 | 372 | - title document title 373 | 374 | - url document location 375 | 376 | - pageNumber current page number 377 | 378 | - totalPages total pages in the document`, 379 | noDataExpression: true, 380 | displayOptions: { 381 | show: { 382 | operation: ['getPDF'], 383 | displayHeaderFooter: [true], 384 | }, 385 | }, 386 | }, 387 | { 388 | displayName: 'Footer Template', 389 | name: 'footerTemplate', 390 | typeOptions: { 391 | rows: 5, 392 | }, 393 | type: 'string', 394 | default: "", 395 | description: "HTML template for the print footer. Should be valid HTML with the following classes used to inject values into them: - date formatted print date", 396 | noDataExpression: true, 397 | displayOptions: { 398 | show: { 399 | operation: ['getPDF'], 400 | displayHeaderFooter: [true], 401 | }, 402 | }, 403 | }, 404 | { 405 | displayName: 'Transparent Background', 406 | name: 'omitBackground', 407 | type: 'boolean', 408 | required: true, 409 | default: false, 410 | displayOptions: { 411 | show: { 412 | operation: ['getPDF'], 413 | }, 414 | }, 415 | description: 416 | 'Hides default white background and allows generating pdfs with transparency.', 417 | }, 418 | { 419 | displayName: 'Background Graphics', 420 | name: 'printBackground', 421 | type: 'boolean', 422 | required: true, 423 | default: false, 424 | displayOptions: { 425 | show: { 426 | operation: ['getPDF'], 427 | }, 428 | }, 429 | description: 'Set to true to include background graphics.', 430 | }, 431 | { 432 | displayName: 'Type', 433 | name: 'imageType', 434 | type: 'options', 435 | options: [ 436 | { 437 | name: 'JPEG', 438 | value: 'jpeg', 439 | }, 440 | { 441 | name: 'PNG', 442 | value: 'png', 443 | }, 444 | { 445 | name: 'WebP', 446 | value: 'webp', 447 | }, 448 | ], 449 | displayOptions: { 450 | show: { 451 | operation: ['getScreenshot'], 452 | }, 453 | }, 454 | default: 'png', 455 | description: 'The image type to use. PNG, JPEG, and WebP are supported.', 456 | }, 457 | { 458 | displayName: 'Quality', 459 | name: 'quality', 460 | type: 'number', 461 | typeOptions: { 462 | minValue: 0, 463 | maxValue: 100, 464 | }, 465 | default: 100, 466 | displayOptions: { 467 | show: { 468 | operation: ['getScreenshot'], 469 | imageType: ['jpeg', 'webp'], 470 | }, 471 | }, 472 | description: 473 | 'The quality of the image, between 0-100. Not applicable to png images.', 474 | }, 475 | { 476 | displayName: 'Full Page', 477 | name: 'fullPage', 478 | type: 'boolean', 479 | required: true, 480 | default: false, 481 | displayOptions: { 482 | show: { 483 | operation: ['getScreenshot'], 484 | }, 485 | }, 486 | description: 'When true, takes a screenshot of the full scrollable page.', 487 | }, 488 | { 489 | displayName: 'Query Parameters', 490 | name: 'queryParameters', 491 | placeholder: 'Add Parameter', 492 | type: 'fixedCollection', 493 | typeOptions: { 494 | multipleValues: true, 495 | }, 496 | displayOptions: { 497 | show: { 498 | operation: ['getPageContent', 'getScreenshot', 'getPDF'], 499 | }, 500 | }, 501 | description: 'The query parameter to send.', 502 | default: {}, 503 | options: [ 504 | { 505 | name: 'parameters', 506 | displayName: 'Parameters', 507 | values: [ 508 | { 509 | displayName: 'Name', 510 | name: 'name', 511 | type: 'string', 512 | default: '', 513 | description: 'Name of the parameter.', 514 | }, 515 | { 516 | displayName: 'Value', 517 | name: 'value', 518 | type: 'string', 519 | default: '', 520 | description: 'Value of the parameter.', 521 | }, 522 | ], 523 | }, 524 | ], 525 | }, 526 | { 527 | displayName: 'Options', 528 | name: 'options', 529 | type: 'collection', 530 | placeholder: 'Add Option', 531 | default: {}, 532 | options: [ 533 | { 534 | displayName: 'Batch Size', 535 | name: 'batchSize', 536 | type: 'number', 537 | typeOptions: { 538 | minValue: 1, 539 | }, 540 | default: 1, 541 | description: 542 | 'Maximum number of pages to open simultaneously. More pages will consume more memory and CPU.', 543 | }, 544 | { 545 | displayName: 'Browser WebSocket Endpoint', 546 | name: 'browserWSEndpoint', 547 | type: 'string', 548 | required: false, 549 | default: '', 550 | description: 'The WebSocket URL of the browser to connect to. When configured, puppeteer will skip the browser launch and connect to the browser instance.', 551 | }, 552 | { 553 | displayName: 'Emulate Device', 554 | name: 'device', 555 | type: 'options', 556 | description: 'Emulate a specific device.', 557 | default: '', 558 | typeOptions: { 559 | loadOptionsMethod: 'getDevices', 560 | }, 561 | required: false, 562 | }, 563 | { 564 | displayName: 'Executable path', 565 | name: 'executablePath', 566 | type: 'string', 567 | required: false, 568 | default: '', 569 | description: 570 | 'A path where Puppeteer expects to find the bundled browser. Has no effect when \'Browser WebSocket Endpoint\' is set.', 571 | }, 572 | { 573 | displayName: 'Extra Headers', 574 | name: 'headers', 575 | placeholder: 'Add Header', 576 | type: 'fixedCollection', 577 | typeOptions: { 578 | multipleValues: true, 579 | }, 580 | description: 'The headers to send.', 581 | default: {}, 582 | options: [ 583 | { 584 | name: 'parameter', 585 | displayName: 'Header', 586 | values: [ 587 | { 588 | displayName: 'Name', 589 | name: 'name', 590 | type: 'string', 591 | default: '', 592 | description: 'Name of the header.', 593 | }, 594 | { 595 | displayName: 'Value', 596 | name: 'value', 597 | type: 'string', 598 | default: '', 599 | description: 'Value to set for the header.', 600 | }, 601 | ], 602 | }, 603 | ], 604 | }, 605 | { 606 | displayName: 'File Name', 607 | name: 'fileName', 608 | type: 'string', 609 | default: '', 610 | description: 'File name to set in binary data. Only applies to \'Get PDF\' and \'Get Screenshot\' operations.', 611 | }, 612 | { 613 | displayName: 'Launch Arguments', 614 | name: 'launchArguments', 615 | placeholder: 'Add Argument', 616 | type: 'fixedCollection', 617 | typeOptions: { 618 | multipleValues: true, 619 | }, 620 | description: 621 | 'Additional command line arguments to pass to the browser instance. Has no effect when \'Browser WebSocket Endpoint\' is set.', 622 | default: {}, 623 | options: [ 624 | { 625 | name: 'args', 626 | displayName: '', 627 | values: [ 628 | { 629 | displayName: 'Argument', 630 | name: 'arg', 631 | type: 'string', 632 | default: '', 633 | description: 634 | 'The command line argument to pass to the browser instance.', 635 | }, 636 | ], 637 | }, 638 | ], 639 | }, 640 | { 641 | displayName: 'Timeout', 642 | name: 'timeout', 643 | type: 'number', 644 | typeOptions: { 645 | minValue: 0, 646 | }, 647 | default: 30000, 648 | description: 649 | 'Maximum navigation time in milliseconds. Pass 0 to disable timeout. Has no effect on the \'Run Custom Script\' operation.', 650 | }, 651 | { 652 | displayName: 'Protocol Timeout', 653 | name: 'protocolTimeout', 654 | type: 'number', 655 | typeOptions: { 656 | minValue: 0, 657 | }, 658 | default: 30000, 659 | description: 660 | 'Maximum time in milliseconds to wait for a protocol response. Pass 0 to disable timeout.', 661 | }, 662 | { 663 | displayName: 'Wait Until', 664 | name: 'waitUntil', 665 | type: 'options', 666 | options: [ 667 | { 668 | name: 'load', 669 | value: 'load', 670 | description: 'The load event is fired', 671 | }, 672 | { 673 | name: 'domcontentloaded', 674 | value: 'domcontentloaded', 675 | description: 'The domcontentloaded event is fired', 676 | }, 677 | { 678 | name: 'networkidle0', 679 | value: 'networkidle0', 680 | description: 'No more than 0 connections for at least 500 ms', 681 | }, 682 | { 683 | name: 'networkidle2', 684 | value: 'networkidle2', 685 | description: 'No more than 2 connections for at least 500 ms', 686 | }, 687 | ], 688 | default: 'load', 689 | description: 'When to consider navigation succeeded. Has no effect on the \'Run Custom Script\' operation.', 690 | }, 691 | { 692 | displayName: 'Page Caching', 693 | name: 'pageCaching', 694 | type: 'boolean', 695 | required: false, 696 | default: true, 697 | description: 698 | 'Whether to enable page level caching. Defaults to true.', 699 | }, 700 | { 701 | displayName: 'Headless mode', 702 | name: 'headless', 703 | type: 'boolean', 704 | required: false, 705 | default: true, 706 | description: 707 | 'Whether to run browser in headless mode. Defaults to true.', 708 | }, 709 | { 710 | displayName: 'Use Chrome Headless Shell', 711 | name: 'shell', 712 | type: 'boolean', 713 | required: false, 714 | default: false, 715 | description: 716 | 'Whether to run browser in headless shell mode. Defaults to false. Headless mode must be enabled. chrome-headless-shell must be in $PATH.', 717 | }, 718 | { 719 | displayName: 'Stealth mode', 720 | name: 'stealth', 721 | type: 'boolean', 722 | required: false, 723 | default: false, 724 | description: 725 | 'When enabled, applies various techniques to make detection of headless Puppeteer harder.', 726 | }, 727 | { 728 | displayName: 'Human typing mode', 729 | name: 'humanTyping', 730 | type: 'boolean', 731 | required: false, 732 | default: false, 733 | description: 734 | 'Gives page the function .typeHuman() which "humanizes" the writing of input elements', 735 | }, 736 | { 737 | displayName: 'Human Typing Options', 738 | name: 'humanTypingOptions', 739 | type: 'collection', 740 | placeholder: 'Add Option', 741 | default: {}, 742 | displayOptions: { 743 | show: { 744 | humanTyping: [true], 745 | }, 746 | }, 747 | options: [ 748 | { 749 | displayName: 'Backspace Maximum Delay (ms)', 750 | name: 'backspaceMaximumDelayInMs', 751 | type: 'number', 752 | required: false, 753 | default: 750 * 2, 754 | description: 'Maximum delay for simulating backspaces in milliseconds', 755 | }, 756 | { 757 | displayName: 'Backspace Minimum Delay (ms)', 758 | name: 'backspaceMinimumDelayInMs', 759 | type: 'number', 760 | required: false, 761 | default: 750, 762 | description: 'Minimum delay for simulating backspaces in milliseconds', 763 | }, 764 | { 765 | displayName: 'Maximum Delay (ms)', 766 | name: 'maximumDelayInMs', 767 | type: 'number', 768 | required: false, 769 | default: 650, 770 | description: 'Maximum delay between keystrokes in milliseconds', 771 | }, 772 | { 773 | displayName: 'Minimum Delay (ms)', 774 | name: 'minimumDelayInMs', 775 | type: 'number', 776 | required: false, 777 | default: 150, 778 | description: 'Minimum delay between keystrokes in milliseconds', 779 | }, 780 | { 781 | displayName: 'Chance to Keep a Typo (%)', 782 | name: 'chanceToKeepATypoInPercent', 783 | type: 'number', 784 | required: false, 785 | default: 0, 786 | description: 'Percentage chance to keep a typo', 787 | }, 788 | { 789 | displayName: 'Typo Chance (%)', 790 | name: 'typoChanceInPercent', 791 | type: 'number', 792 | required: false, 793 | default: 15, 794 | description: 'Percentage chance to make a typo', 795 | }, 796 | ], 797 | }, 798 | { 799 | displayName: 'Proxy Server', 800 | name: 'proxyServer', 801 | type: 'string', 802 | required: false, 803 | default: '', 804 | description: 805 | 'This tells Puppeteer to use a custom proxy configuration. Examples: localhost:8080, socks5://localhost:1080, etc.', 806 | }, 807 | { 808 | displayName: 'Add Container Arguments', 809 | name: 'addContainerArgs', 810 | type: 'boolean', 811 | default: isRunningInContainer(), 812 | description: 'Whether to add recommended arguments for container environments (--no-sandbox, --disable-setuid-sandbox, --disable-dev-shm-usage, --disable-gpu)', 813 | required: false, 814 | }, 815 | ], 816 | }, 817 | ], 818 | }; 819 | -------------------------------------------------------------------------------- /nodes/Puppeteer/Puppeteer.node.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type IDataObject, 3 | type IExecuteFunctions, 4 | type ILoadOptionsFunctions, 5 | type INodeExecutionData, 6 | type INodePropertyOptions, 7 | type INodeType, 8 | type INodeTypeDescription, 9 | NodeOperationError, 10 | } from 'n8n-workflow'; 11 | import { makeResolverFromLegacyOptions, NodeVM } from '@n8n/vm2'; 12 | 13 | import puppeteer from 'puppeteer-extra'; 14 | import pluginStealth from 'puppeteer-extra-plugin-stealth'; 15 | //@ts-ignore 16 | import pluginHumanTyping from 'puppeteer-extra-plugin-human-typing'; 17 | import { 18 | type Browser, 19 | type Device, 20 | KnownDevices as devices, 21 | type Page, 22 | type PaperFormat, 23 | type PDFOptions, 24 | type PuppeteerLifeCycleEvent, 25 | type ScreenshotOptions, 26 | } from 'puppeteer'; 27 | 28 | import { nodeDescription } from './Puppeteer.node.options'; 29 | 30 | const { 31 | NODE_FUNCTION_ALLOW_BUILTIN: builtIn, 32 | NODE_FUNCTION_ALLOW_EXTERNAL: external, 33 | CODE_ENABLE_STDOUT, 34 | } = process.env; 35 | 36 | const CONTAINER_LAUNCH_ARGS = [ 37 | '--no-sandbox', 38 | '--disable-setuid-sandbox', 39 | '--disable-dev-shm-usage', 40 | '--disable-gpu' 41 | ]; 42 | 43 | export const vmResolver = makeResolverFromLegacyOptions({ 44 | external: external 45 | ? { 46 | modules: external.split(','), 47 | transitive: false, 48 | } 49 | : false, 50 | builtin: builtIn?.split(',') ?? [], 51 | }); 52 | 53 | interface HeaderObject { 54 | parameter: Record[]; 55 | } 56 | 57 | interface QueryParameter { 58 | name: string; 59 | value: string; 60 | } 61 | 62 | type ErrorResponse = INodeExecutionData & { 63 | json: { 64 | error: string; 65 | url?: string; 66 | headers?: HeaderObject; 67 | statusCode?: number; 68 | body?: string; 69 | }; 70 | pairedItem: { 71 | item: number; 72 | }; 73 | [key: string]: unknown; 74 | error: Error; 75 | }; 76 | 77 | const DEFAULT_USER_AGENT = 78 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36'; 79 | 80 | async function handleError( 81 | this: IExecuteFunctions, 82 | error: Error, 83 | itemIndex: number, 84 | url?: string, 85 | page?: Page, 86 | ): Promise { 87 | if (page) { 88 | try { 89 | await page.close(); 90 | } catch (closeError) { 91 | console.error('Error closing page:', closeError); 92 | } 93 | } 94 | 95 | if (this.continueOnFail()) { 96 | const nodeOperationError = new NodeOperationError(this.getNode(), error.message); 97 | 98 | const errorResponse: ErrorResponse = { 99 | json: { 100 | error: error.message, 101 | }, 102 | pairedItem: { 103 | item: itemIndex, 104 | }, 105 | error: nodeOperationError, 106 | }; 107 | 108 | if (url) { 109 | errorResponse.json.url = url; 110 | } 111 | 112 | return [errorResponse]; 113 | } 114 | 115 | throw new NodeOperationError(this.getNode(), error.message); 116 | } 117 | 118 | async function handleOptions( 119 | this: IExecuteFunctions, 120 | itemIndex: number, 121 | items: INodeExecutionData[], 122 | browser: Browser, 123 | page: Page, 124 | ): Promise { 125 | const options = this.getNodeParameter('options', 0, {}) as IDataObject; 126 | const pageCaching = options.pageCaching !== false; 127 | const headers: HeaderObject = (options.headers || {}) as HeaderObject; 128 | 129 | const requestHeaders = (headers.parameter || []).reduce((acc, header) => { 130 | acc[header.name] = header.value; 131 | return acc; 132 | }, {}); 133 | const device = options.device as string; 134 | 135 | await page.setCacheEnabled(pageCaching); 136 | 137 | if (device) { 138 | const emulatedDevice = devices[device as keyof typeof devices] as Device; 139 | if (emulatedDevice) { 140 | await page.emulate(emulatedDevice); 141 | } 142 | } else { 143 | const userAgent = 144 | requestHeaders['User-Agent'] || 145 | requestHeaders['user-agent'] || 146 | DEFAULT_USER_AGENT; 147 | await page.setUserAgent(userAgent); 148 | } 149 | 150 | await page.setExtraHTTPHeaders(requestHeaders); 151 | } 152 | 153 | async function runCustomScript( 154 | this: IExecuteFunctions, 155 | itemIndex: number, 156 | items: INodeExecutionData[], 157 | browser: Browser, 158 | page: Page, 159 | ): Promise { 160 | const scriptCode = this.getNodeParameter('scriptCode', itemIndex) as string; 161 | const context = { 162 | $getNodeParameter: this.getNodeParameter, 163 | $getWorkflowStaticData: this.getWorkflowStaticData, 164 | helpers: { 165 | ...this.helpers, 166 | httpRequestWithAuthentication: this.helpers.httpRequestWithAuthentication.bind(this), 167 | requestWithAuthenticationPaginated: this.helpers.requestWithAuthenticationPaginated.bind(this), 168 | }, 169 | ...this.getWorkflowDataProxy(itemIndex), 170 | $browser: browser, 171 | $page: page, 172 | $puppeteer: puppeteer, 173 | }; 174 | const vm = new NodeVM({ 175 | console: 'redirect', 176 | sandbox: context, 177 | require: vmResolver, 178 | wasm: false, 179 | }); 180 | 181 | vm.on( 182 | 'console.log', 183 | this.getMode() === 'manual' 184 | ? this.sendMessageToUI 185 | : CODE_ENABLE_STDOUT === 'true' 186 | ? (...args: unknown[]) => 187 | console.log(`[Workflow "${this.getWorkflow().id}"][Node "${this.getNode().name}"]`, ...args) 188 | : () => {}, 189 | ); 190 | 191 | try { 192 | const scriptResult = await vm.run( 193 | `module.exports = async function() { ${scriptCode}\n}()`, 194 | ); 195 | 196 | if (!Array.isArray(scriptResult)) { 197 | return handleError.call( 198 | this, 199 | new Error( 200 | 'Custom script must return an array of items. Please ensure your script returns an array, e.g., return [{ key: value }].', 201 | ), 202 | itemIndex, 203 | undefined, 204 | page, 205 | ); 206 | } 207 | 208 | return this.helpers.normalizeItems(scriptResult); 209 | } catch (error) { 210 | return handleError.call(this, error as Error, itemIndex, undefined, page); 211 | } 212 | } 213 | 214 | async function processPageOperation( 215 | this: IExecuteFunctions, 216 | operation: string, 217 | url: URL, 218 | page: Page, 219 | itemIndex: number, 220 | options: IDataObject, 221 | ): Promise { 222 | const waitUntil = options.waitUntil as PuppeteerLifeCycleEvent; 223 | const timeout = options.timeout as number; 224 | 225 | try { 226 | const response = await page.goto(url.toString(), { 227 | waitUntil, 228 | timeout, 229 | }); 230 | 231 | const headers = await response?.headers(); 232 | const statusCode = response?.status(); 233 | 234 | if (!response || (statusCode && statusCode >= 400)) { 235 | return handleError.call( 236 | this, 237 | new Error(`Request failed with status code ${statusCode || 0}`), 238 | itemIndex, 239 | url.toString(), 240 | page, 241 | ); 242 | } 243 | 244 | if (operation === 'getPageContent') { 245 | const body = await page.content(); 246 | return [{ 247 | json: { 248 | body, 249 | headers, 250 | statusCode, 251 | url: url.toString(), 252 | }, 253 | pairedItem: { 254 | item: itemIndex, 255 | }, 256 | }]; 257 | } 258 | 259 | if (operation === 'getScreenshot') { 260 | try { 261 | const dataPropertyName = this.getNodeParameter( 262 | 'dataPropertyName', 263 | itemIndex, 264 | ) as string; 265 | const fileName = options.fileName as string; 266 | const type = this.getNodeParameter( 267 | 'imageType', 268 | itemIndex, 269 | ) as ScreenshotOptions['type']; 270 | const fullPage = this.getNodeParameter( 271 | 'fullPage', 272 | itemIndex, 273 | ) as boolean; 274 | const screenshotOptions: ScreenshotOptions = { 275 | type, 276 | fullPage, 277 | }; 278 | 279 | if (type !== 'png') { 280 | const quality = this.getNodeParameter( 281 | 'quality', 282 | itemIndex, 283 | ) as number; 284 | screenshotOptions.quality = quality; 285 | } 286 | 287 | if (fileName) { 288 | screenshotOptions.path = fileName; 289 | } 290 | 291 | const screenshot = await page.screenshot(screenshotOptions); 292 | if (screenshot) { 293 | const binaryData = await this.helpers.prepareBinaryData( 294 | Buffer.from(screenshot), 295 | screenshotOptions.path, 296 | `image/${type}`, 297 | ); 298 | return [{ 299 | binary: { [dataPropertyName]: binaryData }, 300 | json: { 301 | headers, 302 | statusCode, 303 | url: url.toString(), 304 | }, 305 | pairedItem: { 306 | item: itemIndex, 307 | }, 308 | }]; 309 | } 310 | } catch (error) { 311 | return handleError.call(this, error as Error, itemIndex, url.toString(), page); 312 | } 313 | } 314 | 315 | if (operation === 'getPDF') { 316 | try { 317 | const dataPropertyName = this.getNodeParameter( 318 | 'dataPropertyName', 319 | itemIndex, 320 | ) as string; 321 | const pageRanges = this.getNodeParameter( 322 | 'pageRanges', 323 | itemIndex, 324 | ) as string; 325 | const displayHeaderFooter = this.getNodeParameter( 326 | 'displayHeaderFooter', 327 | itemIndex, 328 | ) as boolean; 329 | const omitBackground = this.getNodeParameter( 330 | 'omitBackground', 331 | itemIndex, 332 | ) as boolean; 333 | const printBackground = this.getNodeParameter( 334 | 'printBackground', 335 | itemIndex, 336 | ) as boolean; 337 | const landscape = this.getNodeParameter( 338 | 'landscape', 339 | itemIndex, 340 | ) as boolean; 341 | const preferCSSPageSize = this.getNodeParameter( 342 | 'preferCSSPageSize', 343 | itemIndex, 344 | ) as boolean; 345 | const scale = this.getNodeParameter('scale', itemIndex) as number; 346 | const margin = this.getNodeParameter( 347 | 'margin', 348 | 0, 349 | {}, 350 | ) as IDataObject; 351 | 352 | let headerTemplate = ''; 353 | let footerTemplate = ''; 354 | let height = ''; 355 | let width = ''; 356 | let format: PaperFormat = 'A4'; 357 | 358 | if (displayHeaderFooter === true) { 359 | headerTemplate = this.getNodeParameter( 360 | 'headerTemplate', 361 | itemIndex, 362 | ) as string; 363 | footerTemplate = this.getNodeParameter( 364 | 'footerTemplate', 365 | itemIndex, 366 | ) as string; 367 | } 368 | 369 | if (preferCSSPageSize !== true) { 370 | height = this.getNodeParameter('height', itemIndex) as string; 371 | width = this.getNodeParameter('width', itemIndex) as string; 372 | 373 | if (!height || !width) { 374 | format = this.getNodeParameter( 375 | 'format', 376 | itemIndex, 377 | ) as PaperFormat; 378 | } 379 | } 380 | 381 | const pdfOptions: PDFOptions = { 382 | format, 383 | displayHeaderFooter, 384 | omitBackground, 385 | printBackground, 386 | landscape, 387 | headerTemplate, 388 | footerTemplate, 389 | preferCSSPageSize, 390 | scale, 391 | height, 392 | width, 393 | pageRanges, 394 | margin, 395 | }; 396 | const fileName = options.fileName as string; 397 | if (fileName) { 398 | pdfOptions.path = fileName; 399 | } 400 | 401 | const pdf = await page.pdf(pdfOptions); 402 | if (pdf) { 403 | const binaryData = await this.helpers.prepareBinaryData( 404 | Buffer.from(pdf), 405 | pdfOptions.path, 406 | 'application/pdf', 407 | ); 408 | return [{ 409 | binary: { [dataPropertyName]: binaryData }, 410 | json: { 411 | headers, 412 | statusCode, 413 | url: url.toString(), 414 | }, 415 | pairedItem: { 416 | item: itemIndex, 417 | }, 418 | }]; 419 | } 420 | } catch (error) { 421 | return handleError.call(this, error as Error, itemIndex, url.toString(), page); 422 | } 423 | } 424 | 425 | return handleError.call( 426 | this, 427 | new Error(`Unsupported operation: ${operation}`), 428 | itemIndex, 429 | url.toString(), 430 | page, 431 | ); 432 | } catch (error) { 433 | return handleError.call(this, error as Error, itemIndex, url.toString(), page); 434 | } 435 | } 436 | 437 | export class Puppeteer implements INodeType { 438 | description: INodeTypeDescription = nodeDescription; 439 | 440 | methods = { 441 | loadOptions: { 442 | async getDevices( 443 | this: ILoadOptionsFunctions, 444 | ): Promise { 445 | const deviceNames = Object.keys(devices); 446 | const returnData: INodePropertyOptions[] = []; 447 | 448 | for (const name of deviceNames) { 449 | const device = devices[name as keyof typeof devices] as Device; 450 | returnData.push({ 451 | name, 452 | value: name, 453 | description: `${device.viewport.width} x ${device.viewport.height} @ ${device.viewport.deviceScaleFactor}x`, 454 | }); 455 | } 456 | 457 | return returnData; 458 | }, 459 | }, 460 | }; 461 | 462 | async execute(this: IExecuteFunctions): Promise { 463 | const items = this.getInputData(); 464 | const returnData: INodeExecutionData[] = []; 465 | const options = this.getNodeParameter('options', 0, {}) as IDataObject; 466 | const operation = this.getNodeParameter('operation', 0) as string; 467 | let headless: 'shell' | boolean = options.headless !== false; 468 | const headlessShell = options.shell === true; 469 | const executablePath = options.executablePath as string; 470 | const browserWSEndpoint = options.browserWSEndpoint as string; 471 | const stealth = options.stealth === true; 472 | const humanTyping = options.humanTyping === true; 473 | const humanTypingOptions = { 474 | keyboardLayout: "en", 475 | ...((options.humanTypingOptions as IDataObject) || {}) 476 | }; 477 | const launchArguments = (options.launchArguments as IDataObject) || {}; 478 | const launchArgs: IDataObject[] = launchArguments.args as IDataObject[]; 479 | const args: string[] = []; 480 | const device = options.device as string; 481 | const protocolTimeout = options.protocolTimeout as number; 482 | let batchSize = options.batchSize as number; 483 | 484 | if (!Number.isInteger(batchSize) || batchSize < 1) { 485 | batchSize = 1; 486 | } 487 | 488 | // More on launch arguments: https://www.chromium.org/developers/how-tos/run-chromium-with-flags/ 489 | if (launchArgs && launchArgs.length > 0) { 490 | args.push(...launchArgs.map((arg: IDataObject) => arg.arg as string)); 491 | } 492 | 493 | const addContainerArgs = options.addContainerArgs === true; 494 | if (addContainerArgs) { 495 | const missingContainerArgs = CONTAINER_LAUNCH_ARGS.filter(arg => !args.some( 496 | existingArg => existingArg === arg || existingArg.startsWith(`${arg}=`) 497 | )); 498 | 499 | if (missingContainerArgs.length > 0) { 500 | console.log('Puppeteer node: Adding container optimizations:', missingContainerArgs); 501 | args.push(...missingContainerArgs); 502 | } else { 503 | console.log('Puppeteer node: Container optimizations already present in launch arguments'); 504 | } 505 | } 506 | 507 | // More on proxying: https://www.chromium.org/developers/design-documents/network-settings 508 | if (options.proxyServer) { 509 | args.push(`--proxy-server=${options.proxyServer}`); 510 | } 511 | 512 | if (stealth) { 513 | puppeteer.use(pluginStealth()); 514 | } 515 | if (humanTyping) { 516 | puppeteer.use(pluginHumanTyping(humanTypingOptions)); 517 | } 518 | 519 | if (headless && headlessShell) { 520 | headless = 'shell'; 521 | } 522 | 523 | let browser: Browser; 524 | try { 525 | if (browserWSEndpoint) { 526 | browser = await puppeteer.connect({ 527 | browserWSEndpoint, 528 | protocolTimeout, 529 | }); 530 | } else { 531 | browser = await puppeteer.launch({ 532 | headless, 533 | args, 534 | executablePath, 535 | protocolTimeout, 536 | }); 537 | } 538 | } catch (error) { 539 | throw new Error(`Failed to launch/connect to browser: ${(error as Error).message}`); 540 | } 541 | 542 | const processItem = async ( 543 | item: INodeExecutionData, 544 | itemIndex: number, 545 | ): Promise => { 546 | let page: Page | undefined; 547 | try { 548 | page = await browser.newPage(); 549 | await handleOptions.call(this, itemIndex, items, browser, page); 550 | 551 | if (operation === 'runCustomScript') { 552 | console.log( 553 | `Processing ${itemIndex + 1} of ${items.length}: [${operation}]${device ? ` [${device}] ` : ' '} Custom Script`, 554 | ); 555 | return await runCustomScript.call( 556 | this, 557 | itemIndex, 558 | items, 559 | browser, 560 | page, 561 | ); 562 | } 563 | const urlString = this.getNodeParameter('url', itemIndex) as string; 564 | const queryParametersOptions = this.getNodeParameter( 565 | 'queryParameters', 566 | itemIndex, 567 | {}, 568 | ) as IDataObject; 569 | 570 | const queryParameters = (queryParametersOptions.parameters as QueryParameter[]) || []; 571 | let url: URL; 572 | 573 | try { 574 | url = new URL(urlString); 575 | for (const queryParameter of queryParameters) { 576 | url.searchParams.append(queryParameter.name, queryParameter.value); 577 | } 578 | } catch (error) { 579 | return handleError.call( 580 | this, 581 | new Error(`Invalid URL: ${urlString}`), 582 | itemIndex, 583 | urlString, 584 | page, 585 | ); 586 | } 587 | 588 | console.log( 589 | `Processing ${itemIndex + 1} of ${items.length}: [${operation}]${device ? ` [${device}] ` : ' '}${url}`, 590 | ); 591 | 592 | return await processPageOperation.call( 593 | this, 594 | operation, 595 | url, 596 | page, 597 | itemIndex, 598 | options, 599 | ); 600 | } catch (error) { 601 | return handleError.call( 602 | this, 603 | error as Error, 604 | itemIndex, 605 | undefined, 606 | page, 607 | ); 608 | } finally { 609 | if (page) { 610 | try { 611 | await page.close(); 612 | } catch (error) { 613 | console.error('Error closing page:', error); 614 | } 615 | } 616 | } 617 | }; 618 | 619 | try { 620 | for (let i = 0; i < items.length; i += batchSize) { 621 | const batch = items.slice(i, i + batchSize); 622 | const results = await Promise.all( 623 | batch.map((item, idx) => processItem(item, i + idx)), 624 | ); 625 | if (results?.length) { 626 | returnData.push(...results.flat()); 627 | } 628 | } 629 | } finally { 630 | if (browser) { 631 | try { 632 | if (browserWSEndpoint) { 633 | await browser.disconnect(); 634 | } else { 635 | await browser.close(); 636 | } 637 | } catch (error) { 638 | console.error('Error closing browser:', error); 639 | } 640 | } 641 | } 642 | 643 | return this.prepareOutputData(returnData); 644 | } 645 | } 646 | -------------------------------------------------------------------------------- /nodes/Puppeteer/puppeteer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/Puppeteer/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@fye/netsuite-rest-api'; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "n8n-nodes-puppeteer", 3 | "version": "1.4.1", 4 | "description": "n8n node for browser automation using Puppeteer", 5 | "license": "MIT", 6 | "homepage": "https://github.com/drudge/n8n-nodes-puppeteer", 7 | "author": { 8 | "name": "Nicholas Penree", 9 | "email": "nick@penree.com" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/drudge/n8n-nodes-puppeteer.git" 14 | }, 15 | "main": "index.js", 16 | "scripts": { 17 | "dev": "npm run watch", 18 | "build": "tsc && gulp", 19 | "lint": "tslint -p tsconfig.json -c tslint.json", 20 | "lintfix": "tslint --fix -p tsconfig.json -c tslint.json", 21 | "nodelinter": "nodelinter", 22 | "watch": "tsc --watch" 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "keywords": [ 28 | "n8n", 29 | "node", 30 | "puppeteer", 31 | "scraper", 32 | "screenshot", 33 | "script", 34 | "pdf", 35 | "n8n-node", 36 | "n8n-community-node-package" 37 | ], 38 | "n8n": { 39 | "n8nNodesApiVersion": 1, 40 | "credentials": [], 41 | "nodes": [ 42 | "dist/nodes/Puppeteer/Puppeteer.node.js" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "@typescript-eslint/parser": "^8.18.1", 47 | "eslint": "^9.17.0", 48 | "eslint-plugin-n8n-nodes-base": "^1.16.3", 49 | "gulp": "^5.0.0", 50 | "n8n-workflow": "*", 51 | "prettier": "^3.4.2", 52 | "typescript": "^5.7.2" 53 | }, 54 | "peerDependencies": { 55 | "n8n-workflow": "*" 56 | }, 57 | "dependencies": { 58 | "@n8n/vm2": "^3.9.25", 59 | "puppeteer": "^24.1.1", 60 | "puppeteer-extra": "^3.3.6", 61 | "puppeteer-extra-plugin-human-typing": "github:0x7357/puppeteer-extra-plugin-human-typing", 62 | "puppeteer-extra-plugin-stealth": "^2.11.2" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "lib": [ 5 | "es2017", 6 | "es2019.array", 7 | "dom" 8 | ], 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "noImplicitAny": true, 12 | "removeComments": true, 13 | "strictNullChecks": true, 14 | "strict": true, 15 | "preserveConstEnums": true, 16 | "resolveJsonModule": true, 17 | "declaration": true, 18 | "outDir": "./dist/", 19 | "target": "es2017", 20 | "sourceMap": true, 21 | "allowSyntheticDefaultImports": true, 22 | "skipLibCheck": true, 23 | "declarationMap": true, 24 | "esModuleInterop": true, 25 | "emitDecoratorMetadata": true, 26 | "experimentalDecorators": true, 27 | "noFallthroughCasesInSwitch": true, 28 | "noImplicitReturns": false, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": false, 31 | "pretty": true, 32 | "stripInternal": true, 33 | "rootDir": "./", 34 | "forceConsistentCasingInFileNames": true 35 | }, 36 | "include": [ 37 | "nodes/**/*" 38 | ], 39 | "exclude": [ 40 | "node_modules", 41 | "dist" 42 | ] 43 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "linterOptions": { 3 | "exclude": [ 4 | "node_modules/**/*" 5 | ] 6 | }, 7 | "defaultSeverity": "error", 8 | "jsRules": {}, 9 | "rules": { 10 | "array-type": [ 11 | true, 12 | "array-simple" 13 | ], 14 | "arrow-return-shorthand": true, 15 | "ban": [ 16 | true, 17 | { 18 | "name": "Array", 19 | "message": "tsstyle#array-constructor" 20 | } 21 | ], 22 | "ban-types": [ 23 | true, 24 | [ 25 | "Object", 26 | "Use {} instead." 27 | ], 28 | [ 29 | "String", 30 | "Use 'string' instead." 31 | ], 32 | [ 33 | "Number", 34 | "Use 'number' instead." 35 | ], 36 | [ 37 | "Boolean", 38 | "Use 'boolean' instead." 39 | ] 40 | ], 41 | "class-name": true, 42 | "curly": [ 43 | true, 44 | "ignore-same-line" 45 | ], 46 | "forin": true, 47 | "jsdoc-format": true, 48 | "label-position": true, 49 | "indent": [true, "tabs", 2], 50 | "member-access": [ 51 | true, 52 | "no-public" 53 | ], 54 | "new-parens": true, 55 | "no-angle-bracket-type-assertion": true, 56 | "no-any": true, 57 | "no-arg": true, 58 | "no-conditional-assignment": true, 59 | "no-construct": true, 60 | "no-debugger": true, 61 | "no-default-export": true, 62 | "no-duplicate-variable": true, 63 | "no-inferrable-types": true, 64 | "ordered-imports": [true, { 65 | "import-sources-order": "any", 66 | "named-imports-order": "case-insensitive" 67 | }], 68 | "no-namespace": [ 69 | true, 70 | "allow-declarations" 71 | ], 72 | "no-reference": true, 73 | "no-string-throw": true, 74 | "no-unused-expression": true, 75 | "no-var-keyword": true, 76 | "object-literal-shorthand": true, 77 | "only-arrow-functions": [ 78 | true, 79 | "allow-declarations", 80 | "allow-named-functions" 81 | ], 82 | "prefer-const": true, 83 | "radix": true, 84 | "semicolon": [ 85 | true, 86 | "always", 87 | "ignore-bound-class-methods" 88 | ], 89 | "switch-default": true, 90 | "trailing-comma": [ 91 | true, 92 | { 93 | "multiline": { 94 | "objects": "always", 95 | "arrays": "always", 96 | "functions": "always", 97 | "typeLiterals": "ignore" 98 | }, 99 | "esSpecCompliant": true 100 | } 101 | ], 102 | "triple-equals": [ 103 | true, 104 | "allow-null-check" 105 | ], 106 | "use-isnan": true, 107 | "quotemark": [ 108 | true, 109 | "single" 110 | ], 111 | "quotes": [ 112 | "error", 113 | "single" 114 | ], 115 | "variable-name": [ 116 | true, 117 | "check-format", 118 | "ban-keywords", 119 | "allow-leading-underscore", 120 | "allow-trailing-underscore" 121 | ] 122 | }, 123 | "rulesDirectory": [] 124 | } --------------------------------------------------------------------------------