├── .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 | 
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 | [](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 | 
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 | [](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 |
--------------------------------------------------------------------------------