├── .env ├── .gitattributes ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README-en.md ├── README.md ├── __test__ ├── axios-browser.html └── axios-node.js ├── babel.config.ts ├── ecosystem.config.cjs ├── eslint.config.js ├── nodemon.json ├── package.json ├── pnpm-lock.yaml ├── renovate.json ├── src ├── config.ts ├── controllers │ ├── generate-combine-pdf.ts │ ├── generate-image.ts │ ├── generate-pdf.ts │ ├── generate-simple-pdf.ts │ └── home.ts ├── main.ts ├── routes.ts ├── services │ ├── generate-combine-pdf.ts │ ├── generate-image.ts │ ├── generate-pdf.ts │ ├── generate-simple-pdf.ts │ └── home.ts └── utils │ └── index.ts ├── static ├── baidu-cover.png ├── cover-watermark-full-landscape.png ├── cover-watermark-full.png ├── cover-watermark-landscape.png ├── cover-watermark.png ├── google-logo.png └── shinewing-dark.png ├── sync.sh ├── tsconfig.json └── tsup.config.ts /.env: -------------------------------------------------------------------------------- 1 | PORT=5000 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ts linguist-language=TypeScript 2 | *.js linguist-language=TypeScript 3 | *.html linguist-language=TypeScript 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | templates/* 58 | !templates/test.hbs 59 | 60 | dist/ 61 | 62 | .DS_Store 63 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | 4 | // Auto fix 5 | 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": "explicit", 8 | "source.organizeImports": "never", 9 | "source.fixAll.stylelint": "explicit" 10 | }, 11 | "eslint.run": "onType", 12 | "eslint.format.enable": true, 13 | "css.validate": false, 14 | "less.validate": false, 15 | 16 | "files.autoSaveDelay": 500, 17 | 18 | // Enable eslint for all supported languages 19 | "eslint.validate": [ 20 | "javascript", 21 | "javascriptreact", 22 | "typescript", 23 | "typescriptreact", 24 | "vue", 25 | "html", 26 | "markdown", 27 | "json", 28 | "jsonc", 29 | "yaml", 30 | "toml", 31 | "gql", 32 | "graphql" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 (2021-07-05) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * resolve paths alias ([e4fe962](https://github.com/pdsuwwz/puppeteer-server/commit/e4fe962851760d639bdc9ec997af390a55395afd)) 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Wisdom 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. 22 | -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | # Puppeteer Server 2 | 3 | English | [中文](README.md) 4 | 5 | 6 | 7 | 8 | ## 📤 Migrate to Playwright 9 | 10 | To experience enhanced features and broader browser support, the entire codebase of the latest version has seamlessly migrated to Playwright. 11 | 12 | Playwright repo: [koa-playwright-server](https://github.com/pdsuwwz/koa-playwright-server) 13 | 14 | ## Introduction 15 | 16 | 🦩 Koa + ESM + TypeScript + Tsup + Nodemon + Puppeteer + ESLint (v9) 17 | 18 | > * Fast Generate into PDF and images from any webpage. 19 | > 20 | > * Support merge multiple webpages into one PDF file, injection of Cookies, Watermark addition and Header and Footer insertion 21 | 22 | 23 | ## ✨ Features 24 | 25 | * ✅ Built-in ES Module + TypeScript environment 26 | 27 | * 🌈 Separation business logic and Controllers. 28 | 29 | * 🛡 Probably the best practice for Puppeteer project. 30 | 31 | * 🧩 Configured routing. 32 | 33 | * 🚧 Eslint (v9) configuration. 34 | 35 | * ⚡ Fast build with ~~Rollup~~ Tsup. 36 | 37 | * 🔌 Extensible PDF watermark, header and footer. 38 | 39 | * 🧲 Supports merging of multiple PDF files. 40 | 41 | * 🔥 Based on Nodemon HMR. 42 | 43 | 44 | ## Screenshot 45 | 46 | * Merge Combine the two websites into a PDF file 47 | > 📦 See [Merge Test 1](__test__/axios-browser.html), [Merge Test 2](__test__/axios-node.js) 48 | 49 | 50 | ![image](https://user-images.githubusercontent.com/19891724/159743021-e1f9f528-d6d9-4d6b-b63f-4e71c6b72bdb.png) 51 | 52 | 53 | ## 🎯 Prerequisites 54 | 55 | Please make sure that [Node.js](https://nodejs.org/) (>= 20.x) is installed on your operating system. 56 | 57 | ## Project structure 58 | 59 |
 60 | ├── src
 61 | │   ├── controllers/ ---  Server controllers
 62 | │   ├── services/    ---  Server services
 63 | │   ├── config.ts    ---  About Environments variable
 64 | │   ├── main.ts      ---  Entry file
 65 | │   └── routes.ts    ---  Configs for routing controllers 👉 Routing
 66 | 
67 | 68 | ## ⚡ Quick Start 69 | 70 | ### 1. Installation 71 | 72 | ```bash 73 | pnpm install 74 | ``` 75 | 76 | ### 2. Running Development 77 | 78 | ```bash 79 | pnpm dev 80 | ``` 81 | 82 | ### 3. Running Production 83 | 84 | The project has built-in a `pm2`, running the `pnpm start` will automatically manage the process by `pm2`. 85 | 86 | Run `pnpm build` to build, then run `pnpm start` to start the process managed by `pm2`: 87 | 88 | * Build 89 | 90 | ```bash 91 | pnpm build 92 | ``` 93 | 94 | * Run 95 | 96 | ```bash 97 | pnpm start # PORT is 8080 98 | # or 99 | node dist/bundle.esm.js # PORT is 5000 100 | ``` 101 | 102 | ## API 103 | 104 | * `GET /image` 105 | 106 | Generate screenshot. 107 | 108 | ```bash 109 | curl --location --request GET \ 110 | 'http://localhost:5000/image?url=https://www.baidu.com' \ 111 | --output test-image.png 112 | ``` 113 | 114 | * `GET /simple-pdf` 115 | 116 | Generate pdf. 117 | 118 | ```bash 119 | curl --location --request GET \ 120 | 'http://localhost:5000/simple-pdf?url=https://www.google.com/' \ 121 | --output test-simple-pdf.pdf 122 | ``` 123 | 124 | * `POST /pdf` 125 | 126 | Generate pdf with elements such as headers and footers. 127 | 128 | ```bash 129 | curl --location --request POST 'http://localhost:5000/pdf' \ 130 | --header 'Content-Type: application/x-www-form-urlencoded' \ 131 | --data-urlencode 'url=http://www.google.com' \ 132 | --data-urlencode 'cookies[0].name=token' \ 133 | --data-urlencode 'cookies[0].value=9s2d4c16-f072-16eg-b134-0642ap190006' \ 134 | --data-urlencode 'cookies[0].domain=www.google.com' --output test-complex-pdf.pdf 135 | ``` 136 | 137 | 👆 /pdf request parameters 138 | 139 | | Field | Description | Type | Default Value | 140 | | -------- | -------- | -------- | -------- | 141 | | url | Target site url | string | — | 142 | | cookies | Generally used as a website that requires login to access, you can add this field | Array<{ name, value, domain }> | [] | 143 | | hasMargin | If this field is set to true, it means that the generated PDF will contain margins | boolean | true | 144 | | isLandscape | Whether the generated PDF is horizontal | boolean | false | 145 | | hiddenWatermark | Whether to hide watermark | boolean | false | 146 | | attachment | Display the custom header and footer, provided that hasMargin is set to true | { header, footer } | — | 147 | 148 | 149 | * `POST /combine-pdf` 150 | 151 | Merge multiple PDF files into one file. 152 | 153 | See [Merge Test 1](__test__/axios-browser.html), [Merge Test 2](__test__/axios-node.js) 154 | 155 | 👆 /combine-pdf request parameters 156 | 157 | | Field | Description | Type | Default Value | 158 | | -------- | -------- | -------- | -------- | 159 | | pdfList | A collection of target websites, the parameter type is an array, and each item in the array is a parameter required by `/pdf` | Array<{ pdfItem }> | [] | 160 | 161 | 162 | ## Routing 163 | 164 | In order to make the routing information more readable and transparent, the form of configuration is adopted here. 165 | 166 | You can create an `array` and then write the routing meta information into the `array`, and reuse it in the [src/routes.ts](src/routes.ts) 167 | 168 | ```ts 169 | const routes: Array = [ 170 | { 171 | path: '/', 172 | method: 'get', 173 | action: homeController.hello 174 | }, 175 | // here... 176 | ] 177 | ``` 178 | 179 | # License 180 | 181 | :v: 182 | 183 | [MIT](./LICENSE) 184 | 185 | 186 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fpdsuwwz%2Fpuppeteer-server.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fpdsuwwz%2Fpuppeteer-server?ref=badge_large) 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Puppeteer Server 2 | 3 | 中文 | [English](README-en.md) 4 | 5 | 6 | 7 | 8 | ## 📤 迁移到 Playwright 9 | 10 | 为了体验更好的功能和更广泛的浏览器支持,目前新版代码已全部无缝迁移到 Playwright 11 | 12 | Playwright 仓库: [koa-playwright-server](https://github.com/pdsuwwz/koa-playwright-server) 13 | 14 | ## 介绍 15 | 16 | 🦩 Koa + ESM + TypeScript + Tsup + Nodemon + Puppeteer + ESLint (v9) 17 | 18 | > * 能够将任意网页快速生成为 PDF、图片。 19 | > 20 | > * 支持将多个网页合并,并最终生成一个 PDF 文件,支持 Cookie 的注入、PDF 水印的添加和页眉页脚的插入。 21 | 22 | ## ✨ 特性 23 | 24 | * ✅ 自带 TypeScript + ES Module 环境 25 | 26 | * 🌈 解耦了业务层和控制层 27 | 28 | * 🛡 可能是 Puppeteer 项目的最佳实践 29 | 30 | * 🧩 可配置的路由 31 | 32 | * 🚧 内置 Eslint (v9) 语法风格检查 33 | 34 | * ⚡ 使用 ~~Rollup~~ Tsup 快速构建 35 | 36 | * 🔌 内置了 PDF 水印、页眉和页脚,可自行修改及扩展 37 | 38 | * 🧲 支持合并多个 PDF 文件 39 | 40 | * 🔥 支持 Nodemon 开发环境下的模块热更新 41 | 42 | 43 | ## 截图 44 | 45 | * 示例:合并多个网站到一个 PDF 文件 46 | > 📦 详见 [示例代码1](__test__/axios-browser.html)、[示例代码2](__test__/axios-node.js) 47 | 48 | 49 | ![image](https://user-images.githubusercontent.com/19891724/159743021-e1f9f528-d6d9-4d6b-b63f-4e71c6b72bdb.png) 50 | 51 | 52 | ## 🎯 前序准备 53 | 54 | 请确保安装了 [Node.js](https://nodejs.org/)(>= 20.x) 55 | 56 | 57 | ## 项目结构 58 | 59 |
 60 | ├── src
 61 | │   ├── controllers/ ---  控制层,负责调用业务层的接口
 62 | │   ├── services/    ---  业务层,负责编写具体的业务代码
 63 | │   ├── config.ts    ---  用于导出一些全局变量
 64 | │   ├── main.ts      ---  入口文件
 65 | │   └── routes.ts    ---  用于配置路由元信息 👉 路由配置
 66 | 
67 | 68 | ## ⚡ 快速开始 69 | 70 | ### 1. 安装 71 | 72 | ```bash 73 | pnpm install 74 | ``` 75 | 76 | ### 2. 开发环境运行 77 | 78 | ```bash 79 | pnpm dev 80 | ``` 81 | 82 | ### 3. 生产环境运行 83 | 84 | 该项目已内置 `pm2`,在构建完毕后运行 `pnpm start` 即可使用 `pm2` 管理进程。 85 | 86 | 运行 `pnpm build` 进行构建,然后运行 `pnpm start` 启动由 `pm2` 管理的进程: 87 | 88 | * 打包构建 89 | 90 | ```bash 91 | pnpm build 92 | ``` 93 | 94 | * 运行 95 | 96 | ```bash 97 | pnpm start # 端口号为 8080 98 | # 或直接运行 99 | node dist/bundle.esm.js # 端口号为 5000 100 | ``` 101 | 102 | ## 核心接口 103 | 104 | * `GET /image` 105 | 106 | 用于生成一张图片 107 | 108 | ```bash 109 | curl --location --request GET \ 110 | 'http://localhost:5000/image?url=https://www.baidu.com' \ 111 | --output test-image.png 112 | ``` 113 | 114 | * `GET /simple-pdf` 115 | 116 | 用于生成一个 PDF 文件 117 | 118 | ```bash 119 | curl --location --request GET \ 120 | 'http://localhost:5000/simple-pdf?url=https://www.google.com/' \ 121 | --output test-simple-pdf.pdf 122 | ``` 123 | 124 | * `POST /pdf` 125 | 126 | 可生成一个带有自定义页眉和页脚的 PDF(页眉页脚的内容可通过参数来控制是否显示) 127 | 128 | ```bash 129 | curl --location --request POST 'http://localhost:5000/pdf' \ 130 | --header 'Content-Type: application/x-www-form-urlencoded' \ 131 | --data-urlencode 'url=http://www.google.com' \ 132 | --data-urlencode 'cookies[0].name=token' \ 133 | --data-urlencode 'cookies[0].value=9s2d4c16-f072-16eg-b134-0642ap190006' \ 134 | --data-urlencode 'cookies[0].domain=www.google.com' --output test-complex-pdf.pdf 135 | ``` 136 | 137 | 👆 /pdf 请求参数 138 | 139 | | 字段 | 说明 | 类型 | 默认值 | 140 | | -------- | -------- | -------- | -------- | 141 | | url | 目标网站 | string | — | 142 | | cookies | 如果网站需要提前内置 sessionId cookie, 一般用作需要登录才能访问的网站,则添加此字段即可 | Array<{ name, value, domain }> | [] | 143 | | hasMargin | 是否生成出的 PDF 含有内边距空白 | boolean | true | 144 | | isLandscape | 是否生成横向的 PDF | boolean | false | 145 | | hiddenWatermark | 是否隐藏水印 | boolean | false | 146 | | attachment | 展示自定义页眉页脚,前提是需要将 hasMargin 设置为 true | { header, footer } | — | 147 | 148 | 149 | * `POST /combine-pdf` 150 | 151 | 用于将多个 PDF 文件合并到一个文件 152 | 153 | 查看 [示例代码1](__test__/axios-browser.html)、[示例代码2](__test__/axios-node.js) 154 | 155 | 👆 /combine-pdf 请求参数 156 | 157 | | 字段 | 说明 | 类型 | 默认值 | 158 | | -------- | -------- | -------- | -------- | 159 | | pdfList | 目标网站集合,参数类型为数组,数组内每一项即为一个 `/pdf` 所需的参数 | Array<{ pdfItem }> | [] | 160 | 161 | 162 | ## 路由配置 163 | 164 | 为了使路由元数据更具可读性和透明性,这里采用了配置化的方式 165 | 166 | 你可以创建一个 `数组`,然后将路由元信息写入该 `数组` 中,并在 [src/routes.ts](src/routes.ts) 中重用它 167 | 168 | ```ts 169 | const routes: Array = [ 170 | { 171 | path: '/', 172 | method: 'get', 173 | action: homeController.hello 174 | }, 175 | // here... 176 | ] 177 | ``` 178 | 179 | # 许可证 180 | 181 | :v: 182 | 183 | [MIT](./LICENSE) 184 | 185 | 186 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fpdsuwwz%2Fpuppeteer-server.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fpdsuwwz%2Fpuppeteer-server?ref=badge_large) 187 | -------------------------------------------------------------------------------- /__test__/axios-browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Puppeteer Test Server 8 | 26 | 27 | 28 | 29 | [ 30 |
31 | { 32 | url: 'https://github.com/pdsuwwz', 33 | }, 34 |
35 | { 36 | url: 'https://vuejs.org/api/', 37 | }, 38 |
39 | { 40 | url: 'https://www.baidu.com/', 41 | }, 42 |
43 | ] 44 |
45 |

正在爬取👆网站并完成合并pdf的过程,请等待几秒钟,浏览器将自动打开下载窗口

46 |

Please wait a few seconds while we crawl the above websites and complete the PDF merge process. The browser will automatically open the download window.

47 |
48 | 49 | 50 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /__test__/axios-node.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import axios from 'axios' 3 | 4 | const PORT = 5000 5 | // const PORT = 8080 6 | 7 | const service = axios.create({ 8 | baseURL: `http://localhost:${PORT}`, 9 | }) 10 | 11 | 12 | const init = () => { 13 | service.post('/combine-pdf', { 14 | pdfList: [ 15 | { 16 | url: 'https://www.baidu.com/', 17 | isLandscape: true, 18 | attachment: { 19 | header: 'Header 自定义页眉', 20 | footer: 'Footer 自定义页脚', 21 | }, 22 | }, 23 | { 24 | url: 'https://zh-hans.reactjs.org/', 25 | // hasMargin: false, 26 | isLandscape: true, 27 | cookies: [ 28 | { 29 | name: 'token', 30 | value: 'fc83532c-f833-11eb-8526-0242ac130002', 31 | domain: 'localhost', 32 | }, 33 | ], 34 | }, 35 | ], 36 | }, { 37 | responseType: 'arraybuffer', 38 | }).then((res) => { 39 | fs.writeFileSync('combime-test1.pdf', res.data) 40 | }).catch((err) => { 41 | console.log(err) 42 | }) 43 | } 44 | 45 | init() 46 | -------------------------------------------------------------------------------- /babel.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration for development environment only 3 | */ 4 | 5 | export default { 6 | 7 | presets: [ 8 | '@babel/preset-env', { 9 | targets: { 10 | node: 'current', 11 | }, 12 | // https://github.com/babel/babel/issues/10374#issuecomment-597029696 13 | modules: 'auto', 14 | }, 15 | '@babel/preset-typescript', 16 | ], 17 | plugins: [ 18 | [ 19 | 'module-resolver', 20 | { 21 | alias: { 22 | '@': './src', 23 | }, 24 | }, 25 | ], 26 | ], 27 | } 28 | -------------------------------------------------------------------------------- /ecosystem.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('node:path') 2 | const { name } = require('./package.json') 3 | 4 | module.exports = { 5 | apps: [ 6 | { 7 | name, 8 | script: path.resolve(__dirname, './dist/bundle.commonjs.cjs'), 9 | instances: require('node:os').cpus().length, 10 | autorestart: true, 11 | watch: true, 12 | cron_restart: '0 3 * * *', 13 | env_production: { 14 | NODE_ENV: 'production', 15 | PORT: 8080, 16 | }, 17 | }, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | stylistic: { 5 | indent: 2, 6 | quotes: 'single', 7 | semi: false, 8 | }, 9 | 10 | ignores: [ 11 | 'node_modules', 12 | 'dist', 13 | 'public', 14 | ], 15 | rules: { 16 | 'node/prefer-global/process': 'off', 17 | 'node/prefer-global/buffer': 'off', 18 | 'no-async-promise-executor': 'off', 19 | 'prefer-spread': 'off', 20 | 'no-case-declarations': 'off', 21 | 'one-var': 'off', 22 | 'unicorn/prefer-includes': 'off', 23 | 'no-console': 'off', 24 | 'unicorn/no-new-array': 'off', 25 | 'unused-imports/no-unused-vars': 'warn', 26 | 'symbol-description': 'off', 27 | 'prefer-promise-reject-errors': 'off', 28 | 'array-callback-return': 'off', 29 | 'curly': ['error', 'all'], 30 | 'jsdoc/check-alignment': 'off', 31 | 'ts/no-use-before-define': 'off', 32 | 'ts/method-signature-style': 'off', 33 | 'ts/consistent-type-definitions': 'off', 34 | 'style/jsx-curly-brace-presence': 'off', 35 | 'style/jsx-one-expression-per-line': 'off', 36 | 'style/jsx-curly-newline': 'off', 37 | 'style/jsx-closing-tag-location': 'off', 38 | 'style/brace-style': ['error', '1tbs'], 39 | 'style/arrow-parens': ['error', 'always'], 40 | 'style/no-multiple-empty-lines': ['error', { 41 | max: 2, 42 | maxEOF: 0, 43 | }], 44 | 'antfu/consistent-list-newline': 'off', 45 | 'antfu/top-level-function': 'off', 46 | 'antfu/if-newline': 'off', 47 | }, 48 | }) 49 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable": "rs", 3 | "watch": ["src"], 4 | "ext": "ts", 5 | "exec": "pnpm watch", 6 | "events": { 7 | "start": "echo \"\\x1Bc\"" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppeteer-server", 3 | "type": "module", 4 | "version": "0.2.1", 5 | "description": "Koa + TypeScript + Tsup + Puppeteer", 6 | "author": "Wisdom ", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/pdsuwwz/puppeteer-server.git" 11 | }, 12 | "engines": { 13 | "node": ">=20.x" 14 | }, 15 | "scripts": { 16 | "dev": "nodemon", 17 | "watch": "tsx src/main.ts", 18 | "clean:dist": "rm -rf ./dist", 19 | "build": "tsup", 20 | "tsbuild": "pnpm clean:dist && tsc && tsc-alias", 21 | "start": "pm2 start ecosystem.config.cjs --env production", 22 | "restart": "pm2 restart ecosystem.config.cjs --env production", 23 | "stop": "pm2 stop ecosystem.config.cjs", 24 | "delete": "pm2 delete ecosystem.config.cjs", 25 | "delete:all": "pm2 delete all", 26 | "wc": "pm2 list", 27 | "lint": "eslint --fix .", 28 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" 29 | }, 30 | "peerDependencies": { 31 | "puppeteer": "^22.13.1" 32 | }, 33 | "dependencies": { 34 | "@koa/cors": "^5.0.0", 35 | "dotenv": "^16.4.7", 36 | "koa": "^2.16.0", 37 | "koa-bodyparser": "4.4.1", 38 | "koa-router": "^13.0.1", 39 | "pdf-lib": "^1.17.1" 40 | }, 41 | "devDependencies": { 42 | "@antfu/eslint-config": "^4.10.1", 43 | "@babel/core": "^7.26.10", 44 | "@babel/node": "^7.26.0", 45 | "@babel/preset-env": "^7.26.9", 46 | "@babel/preset-typescript": "^7.26.0", 47 | "@eslint/js": "^9.22.0", 48 | "@stylistic/eslint-plugin": "^4.2.0", 49 | "@types/bluebird": "^3.5.42", 50 | "@types/koa": "^2.15.0", 51 | "@types/koa-bodyparser": "4.3.12", 52 | "@types/koa-router": "^7.4.8", 53 | "@types/koa__cors": "^5.0.0", 54 | "@types/node": "^22.13.10", 55 | "@typescript-eslint/eslint-plugin": "^8.26.1", 56 | "@typescript-eslint/parser": "^8.26.1", 57 | "axios": "^1.8.3", 58 | "babel-plugin-module-resolver": "^5.0.2", 59 | "conventional-changelog-cli": "^5.0.0", 60 | "eslint": "^9.22.0", 61 | "esno": "^4.8.0", 62 | "globals": "^16.0.0", 63 | "nodemon": "^3.1.9", 64 | "pm2": "^6.0.5", 65 | "ts-node": "^10.9.2", 66 | "ts-toolbelt": "^9.6.0", 67 | "tsc-alias": "^1.8.11", 68 | "tsconfig-paths": "^4.2.0", 69 | "tslib": "^2.8.1", 70 | "tsup": "^8.4.0", 71 | "tsx": "^4.19.3", 72 | "typescript": "^5.8.2" 73 | }, 74 | "tags": [ 75 | "koa", 76 | "typescript", 77 | "puppeteer", 78 | "pm2", 79 | "nodemon", 80 | "eslint", 81 | "tsup", 82 | "babel" 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":preserveSemverRanges" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | 3 | dotenv.config() 4 | 5 | export const { PORT } = process.env 6 | -------------------------------------------------------------------------------- /src/controllers/generate-combine-pdf.ts: -------------------------------------------------------------------------------- 1 | import type { RequestBody } from '@/controllers/generate-pdf' 2 | import type { Context, Request } from 'koa' 3 | 4 | import GenerateCombinePdfService from '@/services/generate-combine-pdf' 5 | import GeneratePdfService from '@/services/generate-pdf' 6 | 7 | interface RequestCombinePDF extends Request { 8 | body: { 9 | pdfList: RequestBody[] 10 | } 11 | } 12 | 13 | interface ContextCombinePDF extends Context { 14 | request: RequestCombinePDF 15 | } 16 | 17 | 18 | class GenerateCombinePdfController { 19 | private pdfService: GeneratePdfService = new GeneratePdfService() 20 | private service: GenerateCombinePdfService = new GenerateCombinePdfService() 21 | 22 | generate = async (ctx: ContextCombinePDF) => { 23 | let { pdfList = [] } = ctx.request.body 24 | 25 | pdfList = pdfList.filter((pdfItem) => { 26 | return pdfItem.url 27 | }) 28 | 29 | if (!pdfList.length) { 30 | ctx.status = 404 31 | ctx.body = { 32 | status: 'NOT-FOUND', 33 | } 34 | return 35 | } 36 | 37 | 38 | const pdfPromises = pdfList.map((pdfItem) => { 39 | return this.pdfService.generate(pdfItem) 40 | }) 41 | 42 | const totalPdf = await this.service.generate( 43 | ...pdfPromises, 44 | ) 45 | 46 | if (Object.prototype.toString.call(totalPdf) === '[object Uint8Array]') { 47 | ctx.type = 'application/pdf' 48 | } 49 | ctx.body = totalPdf 50 | } 51 | } 52 | 53 | export default new GenerateCombinePdfController() 54 | -------------------------------------------------------------------------------- /src/controllers/generate-image.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'koa' 2 | import GenerateImageService from '@/services/generate-image' 3 | 4 | class GenerateImageController { 5 | private service: GenerateImageService = new GenerateImageService() 6 | 7 | generate = async (ctx: Context) => { 8 | const img = await this.service.generate(ctx) 9 | if (Object.prototype.toString.call(img) === '[object Uint8Array]') { 10 | ctx.type = 'image/png' 11 | } 12 | ctx.body = img 13 | } 14 | } 15 | 16 | export default new GenerateImageController() 17 | -------------------------------------------------------------------------------- /src/controllers/generate-pdf.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Request } from 'koa' 2 | import GeneratePdfService from '@/services/generate-pdf' 3 | 4 | export type Cookies = { 5 | name: string 6 | value: string 7 | domain: string 8 | } 9 | 10 | export type Attachment = { 11 | header: string 12 | footer: string 13 | } 14 | 15 | /** 16 | * 生成 PDF 的请求参数 17 | */ 18 | export interface RequestBody extends Record { 19 | /** 20 | * 目标网站 21 | * 22 | * Target site url 23 | */ 24 | url?: string 25 | /** 26 | * 如果网站需要提前内置 sessionId cookie, 一般用作需要登录才能访问的网站,则添加此字段即可 27 | * 28 | * If the webpage needs to have a sessionId cookie built in advance, 29 | * which is generally used as a website that can only be accessed by logging in, add this field 30 | */ 31 | cookies?: Array 32 | /** 33 | * 设置此字段为 true, 则生成出的 PDF 将含有内边距空白 34 | * 35 | * If this field is set to true, it means that the generated PDF will contain margins 36 | */ 37 | hasMargin?: boolean 38 | /** 39 | * 是否生成横向 PDF 40 | * 41 | * Whether the generated PDF is horizontal 42 | */ 43 | isLandscape?: boolean 44 | /** 45 | * 是否隐藏水印 46 | * 47 | * Whether to hide watermark 48 | */ 49 | hiddenWatermark?: boolean 50 | /** 51 | * 展示自定义页眉页脚,前提是需要将 hasMargin 设置为 true 52 | * 53 | * Display the custom header and footer, provided that hasMargin is set to true 54 | */ 55 | attachment?: Attachment 56 | } 57 | 58 | interface RequestPDF extends Request { 59 | body: RequestBody 60 | } 61 | 62 | interface ContextPDF extends Context { 63 | request: RequestPDF 64 | } 65 | 66 | 67 | class GeneratePdfController { 68 | private service: GeneratePdfService = new GeneratePdfService() 69 | 70 | generate = async (ctx: ContextPDF) => { 71 | const { 72 | url, 73 | cookies, 74 | hasMargin, 75 | } = ctx.request.body 76 | 77 | 78 | if (!url) { 79 | ctx.status = 404 80 | ctx.body = { 81 | status: 'NOT-FOUND', 82 | } 83 | return 84 | } 85 | 86 | 87 | const pdf = await this.service.generate({ 88 | url, 89 | cookies, 90 | hasMargin, 91 | }) 92 | 93 | if (Object.prototype.toString.call(pdf) === '[object Uint8Array]') { 94 | ctx.type = 'application/pdf' 95 | } 96 | ctx.body = pdf 97 | } 98 | } 99 | 100 | export default new GeneratePdfController() 101 | -------------------------------------------------------------------------------- /src/controllers/generate-simple-pdf.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'koa' 2 | import GenerateSimplePdfService from '@/services/generate-simple-pdf' 3 | 4 | export interface RouterQuery { 5 | url?: string 6 | isLandscape?: string 7 | } 8 | 9 | 10 | class GenerateSimplePdfController { 11 | private service: GenerateSimplePdfService = new GenerateSimplePdfService() 12 | 13 | generate = async (ctx: Context) => { 14 | // https://stackoverflow.com/a/39672914/13202554 15 | const { 16 | url, 17 | isLandscape, 18 | }: RouterQuery = ctx.query 19 | 20 | if (!url) { 21 | ctx.status = 404 22 | ctx.body = { 23 | status: 'NOT-FOUND', 24 | } 25 | return 26 | } 27 | 28 | const pdf = await this.service.generate({ 29 | url, 30 | isLandscape, 31 | }) 32 | 33 | if (Object.prototype.toString.call(pdf) === '[object Uint8Array]') { 34 | ctx.type = 'application/pdf' 35 | } 36 | ctx.body = pdf 37 | } 38 | } 39 | 40 | export default new GenerateSimplePdfController() 41 | -------------------------------------------------------------------------------- /src/controllers/home.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'koa' 2 | import HomeService from '@/services/home' 3 | 4 | class HomeController { 5 | private service: HomeService = new HomeService() 6 | 7 | hello = async (ctx: Context) => { 8 | ctx.body = await this.service.hello() 9 | } 10 | } 11 | 12 | export default new HomeController() 13 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { PORT } from '@/config' 2 | import routes from '@/routes' 3 | import { getLocalAddress } from '@/utils' 4 | import Cors from '@koa/cors' 5 | import Koa from 'koa' 6 | import bodyParser from 'koa-bodyparser' 7 | import Router from 'koa-router' 8 | 9 | const app = new Koa() 10 | const router = new Router() 11 | 12 | 13 | // routes 14 | routes.forEach((route) => { 15 | const method = router[route.method] 16 | method.call(router, route.path, route.action) 17 | }) 18 | 19 | 20 | app.use(Cors()) 21 | app.use(bodyParser()) 22 | app.use(router.routes()) 23 | app.use(router.allowedMethods()) 24 | 25 | app.listen(PORT, () => { 26 | console.clear() 27 | 28 | const address = getLocalAddress() 29 | const localhost = address[Object.keys(address)[0]]?.[0] 30 | 31 | const blank1 = ''.padStart(1) 32 | const blank2 = ''.padStart(2) 33 | 34 | console.log( 35 | '\n', 36 | blank1, 37 | '🚀🚀🚀', 38 | '\x1B[32m', 39 | 'Puppeteer Server running at:\n', 40 | '\x1B[0m', 41 | ) 42 | console.log( 43 | blank2, 44 | '> Local: ', 45 | '\x1B[36m', 46 | `http://localhost:${PORT}/`, 47 | '\x1B[0m', 48 | ) 49 | console.log( 50 | blank2, 51 | '> Network:', 52 | '\x1B[36m', 53 | `http://${localhost}:${PORT}/\n`, 54 | '\x1B[0m', 55 | ) 56 | }) 57 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | import type Router from 'koa-router' 2 | import GenerateCombinePdfController from '@/controllers/generate-combine-pdf' 3 | import GenerateImageController from '@/controllers/generate-image' 4 | import GeneratePdfController from '@/controllers/generate-pdf' 5 | import GenerateSimplePdfController from '@/controllers/generate-simple-pdf' 6 | import homeController from '@/controllers/home' 7 | 8 | type HttpMethodKeys = Extract 14 | 15 | interface RouteConfig { 16 | path: string 17 | // method: string | 'get' | 'post' | 'delete' | 'put' 18 | method: HttpMethodKeys 19 | action: Router.IMiddleware 20 | } 21 | 22 | const routes: Array = [ 23 | { 24 | path: '/', 25 | method: 'get', 26 | action: homeController.hello, 27 | }, 28 | { 29 | path: '/image', 30 | method: 'get', 31 | action: GenerateImageController.generate, 32 | }, 33 | { 34 | path: '/pdf', 35 | method: 'post', 36 | action: GeneratePdfController.generate, 37 | }, 38 | { 39 | path: '/simple-pdf', 40 | method: 'get', 41 | action: GenerateSimplePdfController.generate, 42 | }, 43 | { 44 | path: '/combine-pdf', 45 | method: 'post', 46 | action: GenerateCombinePdfController.generate, 47 | }, 48 | ] 49 | export default routes 50 | -------------------------------------------------------------------------------- /src/services/generate-combine-pdf.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PDFPage, 3 | } from 'pdf-lib' 4 | import { 5 | PDFDocument, 6 | } from 'pdf-lib' 7 | 8 | /** 9 | * @example 10 | 11 | curl --location --request GET \ 12 | 'http://localhost:5000/simple-pdf?url=https://www.google.com/' \ 13 | --output test-simple-pdf.pdf 14 | 15 | */ 16 | 17 | export default class GenerateCombinePdfService { 18 | generate = async (...combineList: Promise[]): Promise => { 19 | const mergedPdf = await PDFDocument.create() 20 | 21 | const pdfsToMerge: Uint8Array[] = await Promise.all(combineList) 22 | 23 | for await (const pdfBytes of pdfsToMerge) { 24 | const pdf = await PDFDocument.load(pdfBytes) 25 | const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices()) 26 | 27 | copiedPages.forEach((page: PDFPage) => { 28 | mergedPdf.addPage(page) 29 | }) 30 | } 31 | 32 | const buffer = await mergedPdf.save() 33 | 34 | return Buffer.from(buffer) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/services/generate-image.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'koa' 2 | import puppeteer from 'puppeteer' 3 | 4 | /** 5 | * @example 6 | 7 | curl --location --request GET \ 8 | 'http://localhost:5000/image?url=https://www.baidu.com' \ 9 | --output test-image.png 10 | 11 | */ 12 | 13 | export default class GenerateImageService { 14 | generate = async (ctx: Context): Promise => { 15 | if (!ctx.query.url) { 16 | ctx.status = 404 17 | return { 18 | status: 'NOT-FOUND', 19 | } 20 | } 21 | 22 | const browser = await puppeteer.launch({ 23 | args: [ 24 | '--disable-extensions', 25 | '--no-sandbox', 26 | '--disable-setuid-sandbox', 27 | '--disable-web-security', 28 | ], 29 | }) 30 | 31 | const page = await browser.newPage() 32 | 33 | await page.goto(ctx.query.url as string, { 34 | waitUntil: 'networkidle2', 35 | }) 36 | 37 | return await page.screenshot({ 38 | omitBackground: true, 39 | fullPage: true, 40 | type: 'png', 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/services/generate-pdf.ts: -------------------------------------------------------------------------------- 1 | import type { RequestBody } from '@/controllers/generate-pdf' 2 | import type { PDFOptions } from 'puppeteer' 3 | import fs from 'node:fs' 4 | 5 | import path from 'node:path' 6 | import puppeteer from 'puppeteer' 7 | 8 | const attachmentImage = fs.readFileSync( 9 | path.resolve( 10 | process.cwd(), 11 | 'static/google-logo.png', 12 | ), 13 | { 14 | encoding: 'base64', 15 | }, 16 | ) 17 | 18 | const watermarkImageVertical = fs.readFileSync( 19 | path.resolve( 20 | process.cwd(), 21 | 'static/cover-watermark.png', 22 | ), 23 | { 24 | encoding: 'base64', 25 | }, 26 | ) 27 | const watermarkImageFullVertical = fs.readFileSync( 28 | path.resolve( 29 | process.cwd(), 30 | 'static/cover-watermark-full.png', 31 | ), 32 | { 33 | encoding: 'base64', 34 | }, 35 | ) 36 | 37 | const watermarkImageLandscape = fs.readFileSync( 38 | path.resolve( 39 | process.cwd(), 40 | 'static/cover-watermark-landscape.png', 41 | ), 42 | { 43 | encoding: 'base64', 44 | }, 45 | ) 46 | const watermarkImageFullLandscape = fs.readFileSync( 47 | path.resolve( 48 | process.cwd(), 49 | 'static/cover-watermark-full-landscape.png', 50 | ), 51 | { 52 | encoding: 'base64', 53 | }, 54 | ) 55 | 56 | /** 57 | * @example 58 | 59 | curl --location --request POST 'http://localhost:5000/pdf' \ 60 | --header 'Content-Type: application/x-www-form-urlencoded' \ 61 | --data-urlencode 'url=http://www.google.com' \ 62 | --data-urlencode 'cookies[0].name=token' \ 63 | --data-urlencode 'cookies[0].value=9s2d4c16-f072-16eg-b134-0642ap190006' \ 64 | --data-urlencode 'cookies[0].domain=www.google.com' --output pdf-gen.pdf 65 | 66 | */ 67 | 68 | export default class GeneratePdfService { 69 | generate = async (params: RequestBody): Promise => { 70 | const { 71 | url, 72 | cookies = [], 73 | hasMargin = true, 74 | isLandscape = false, 75 | hiddenWatermark = false, 76 | } = params 77 | 78 | let { attachment } = params 79 | attachment = Object.assign({ 80 | header: '', 81 | footer: '', 82 | }, {}, attachment) 83 | 84 | 85 | const browser = await puppeteer.launch({ 86 | dumpio: true, 87 | defaultViewport: null, 88 | ignoreHTTPSErrors: true, 89 | args: [ 90 | '--disable-extensions', 91 | '--no-sandbox', 92 | '--disable-setuid-sandbox', 93 | '--disable-web-security', 94 | ], 95 | }) 96 | 97 | 98 | const page = await browser.newPage() 99 | 100 | await page.setDefaultNavigationTimeout(100000) 101 | 102 | page.setExtraHTTPHeaders({ 103 | // ... 104 | }) 105 | 106 | await page.setCookie(...cookies) 107 | 108 | await page.goto(encodeURI(url), { 109 | waitUntil: 'networkidle2', 110 | }) 111 | 112 | let waterImage = '' 113 | if (isLandscape) { 114 | waterImage = hasMargin 115 | ? watermarkImageLandscape 116 | : watermarkImageFullLandscape 117 | } else { 118 | waterImage = hasMargin 119 | ? watermarkImageVertical 120 | : watermarkImageFullVertical 121 | } 122 | 123 | // 解决报错:ReferenceError: __name is not defined 124 | // Ref: https://github.com/evanw/esbuild/issues/1031 125 | // Ref: https://github.com/evanw/esbuild/issues/1031 126 | await page.evaluate(() => { 127 | (window as any).__name = (func: any) => func 128 | }) 129 | 130 | // Get the "viewport" of the page, as reported by the page. 131 | await page.evaluate(async ({ watermarkImage, hiddenWatermark }) => { 132 | if (hiddenWatermark) { 133 | return {} 134 | } 135 | 136 | const waitImage = (src: string) => { 137 | return new Promise((resolve) => { 138 | const img = document.createElement('img') 139 | img.onload = () => { 140 | resolve({}) 141 | } 142 | img.src = src 143 | }) 144 | } 145 | // Watermark Image 146 | // const src = 'https://img-prod-cms-rt-microsoft-com.akamaized.net/cms/api/am/imageFileData/RE1Mu3b' 147 | const src = `data:image/png;base64,${watermarkImage}` 148 | 149 | const image = document.createElement('img') 150 | image.style.cssText = ` 151 | position: fixed; 152 | width: 100%; 153 | height: 100%; 154 | top: 0; 155 | left: 0; 156 | right: 0; 157 | bottom: 0; 158 | margin: auto; 159 | opacity: 1; 160 | user-select: none; 161 | pointer-events: none; 162 | z-index: 100000; 163 | ` 164 | image.src = src 165 | await waitImage(src) 166 | 167 | document.body.appendChild(image) 168 | 169 | return { 170 | 171 | } 172 | }, { 173 | watermarkImage: waterImage, 174 | hiddenWatermark, 175 | }) 176 | 177 | // Css for print mode 178 | page.addStyleTag({ 179 | content: ` 180 | @media print { 181 | /* 182 | .xxx-class { 183 | page-break-before: always; 184 | } 185 | */ 186 | } 187 | `, 188 | }) 189 | 190 | const pptrStyleTag = ` 191 | 198 | ` 199 | 200 | const headerTemplate = ` 201 | ${pptrStyleTag} 202 |
216 |
${attachment.header}
217 |
218 | 219 |
220 |
` 221 | 222 | const footerTemplate = ` 223 | ${pptrStyleTag} 224 |
238 |
${attachment.footer}
239 |
240 | 241 | 242 | 243 | 244 | 245 |
246 |
` 247 | 248 | const extraProps: PDFOptions = {} 249 | if (hasMargin) { 250 | extraProps.displayHeaderFooter = true 251 | extraProps.headerTemplate = headerTemplate 252 | extraProps.footerTemplate = footerTemplate 253 | extraProps.margin = { 254 | top: 70, 255 | left: 81, 256 | right: 81, 257 | bottom: 70, 258 | } 259 | } 260 | 261 | const buffer = await page.pdf({ 262 | format: 'a4', 263 | landscape: isLandscape, 264 | printBackground: true, 265 | ...extraProps, 266 | }) 267 | page.close() 268 | return buffer 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/services/generate-simple-pdf.ts: -------------------------------------------------------------------------------- 1 | import type { RouterQuery } from '@/controllers/generate-simple-pdf' 2 | import puppeteer from 'puppeteer' 3 | 4 | /** 5 | * @example 6 | 7 | curl --location --request GET \ 8 | 'http://localhost:5000/simple-pdf?url=https://www.google.com/' \ 9 | --output test-simple-pdf.pdf 10 | 11 | */ 12 | 13 | export default class GenerateSimplePdfService { 14 | generate = async (params: RouterQuery): Promise => { 15 | const { 16 | url, 17 | isLandscape = '1', 18 | } = params 19 | 20 | const browser = await puppeteer.launch({ 21 | dumpio: true, 22 | defaultViewport: null, 23 | ignoreHTTPSErrors: true, 24 | args: [ 25 | '--disable-extensions', 26 | '--no-sandbox', 27 | '--disable-setuid-sandbox', 28 | '--disable-web-security', 29 | ], 30 | }) 31 | 32 | 33 | const page = await browser.newPage() 34 | 35 | await page.setDefaultNavigationTimeout(100000) 36 | 37 | await page.goto(encodeURI(url), { 38 | waitUntil: 'networkidle2', 39 | }) 40 | 41 | const buffer = await page.pdf({ 42 | format: 'a4', 43 | landscape: isLandscape === '1', 44 | printBackground: true, 45 | }) 46 | 47 | await page.close() 48 | 49 | return buffer 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/services/home.ts: -------------------------------------------------------------------------------- 1 | export default class HomeService { 2 | hello = (): Promise => { 3 | return new Promise((resolve) => resolve({ 4 | say: { 5 | hello: 'Hello, Puppeteer Server', 6 | date: new Date(), 7 | }, 8 | })) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { networkInterfaces } from 'node:os' 2 | 3 | 4 | export function getLocalAddress(): any { 5 | const nets = networkInterfaces() 6 | const results = Object.create(null) // Or just '{}', an empty object 7 | 8 | for (const name of Object.keys(nets)) { 9 | for (const net of nets[name]) { 10 | // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses 11 | if (net.family === 'IPv4' && !net.internal) { 12 | if (!results[name]) { 13 | results[name] = [] 14 | } 15 | results[name].push(net.address) 16 | } 17 | } 18 | } 19 | return results 20 | } 21 | -------------------------------------------------------------------------------- /static/baidu-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdsuwwz/puppeteer-server/670ec6f3ba010fa56c15e32a79bb73f65e232cec/static/baidu-cover.png -------------------------------------------------------------------------------- /static/cover-watermark-full-landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdsuwwz/puppeteer-server/670ec6f3ba010fa56c15e32a79bb73f65e232cec/static/cover-watermark-full-landscape.png -------------------------------------------------------------------------------- /static/cover-watermark-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdsuwwz/puppeteer-server/670ec6f3ba010fa56c15e32a79bb73f65e232cec/static/cover-watermark-full.png -------------------------------------------------------------------------------- /static/cover-watermark-landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdsuwwz/puppeteer-server/670ec6f3ba010fa56c15e32a79bb73f65e232cec/static/cover-watermark-landscape.png -------------------------------------------------------------------------------- /static/cover-watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdsuwwz/puppeteer-server/670ec6f3ba010fa56c15e32a79bb73f65e232cec/static/cover-watermark.png -------------------------------------------------------------------------------- /static/google-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdsuwwz/puppeteer-server/670ec6f3ba010fa56c15e32a79bb73f65e232cec/static/google-logo.png -------------------------------------------------------------------------------- /static/shinewing-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdsuwwz/puppeteer-server/670ec6f3ba010fa56c15e32a79bb73f65e232cec/static/shinewing-dark.png -------------------------------------------------------------------------------- /sync.sh: -------------------------------------------------------------------------------- 1 | rsync -avr \ 2 | .env \ 3 | __test__ \ 4 | dist \ 5 | static \ 6 | ecosystem.config.js \ 7 | package.json \ 8 | pnpm-lock.yaml \ 9 | my-company@172.20.30.41:/home/my-company/pdsuwwz-test/. 10 | 11 | # chmod u+x sync.sh 12 | # my-company@172.20.30.41 is your ssh path. 13 | # /home/my-company/pdsuwwz-test/. is your remote directory. 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "esnext"], 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "baseUrl": "./", 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "paths": { 11 | "@/*": [ 12 | "src/*" 13 | ] 14 | }, 15 | "resolveJsonModule": true, 16 | "typeRoots": ["puppeteer", "./node_modules/@types"], 17 | "types": ["node"], 18 | "noImplicitAny": true, 19 | "declaration": false, 20 | "outDir": "./dist", 21 | "removeComments": true, 22 | "sourceMap": false, 23 | "allowSyntheticDefaultImports": true, 24 | "esModuleInterop": true, 25 | // "noEmit": true, 26 | "isolatedModules": true 27 | }, 28 | "include": [ 29 | "**/*.ts" 30 | ], 31 | "exclude": [ 32 | "dist", 33 | "node_modules" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ['src/main.ts'], 6 | format: ['esm'], 7 | minify: true, 8 | shims: true, 9 | clean: true, 10 | terserOptions: { 11 | compress: true, 12 | keep_fnames: false, 13 | }, 14 | keepNames: false, 15 | }, 16 | ]) 17 | --------------------------------------------------------------------------------