bits-to-dead-trees 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,3 +30,6 @@ jobs:
30
30
 
31
31
  - name: Run typecheck
32
32
  run: npm run ts-check
33
+
34
+ - name: Run tests
35
+ run: npm test
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # bits-to-dead-trees
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - b4ebed6: Allow more options for pdf, context and goto to be passed. Also working validations.
8
+
9
+ We allow way more options now to be given in the payload:
10
+
11
+ - `pdfOptions` are the options for the `pdf` call in Playwright: https://playwright.dev/docs/api/class-page#page-pdf.
12
+ - `gotoOptions` are the options for the `goto` method in Playwright: https://playwright.dev/docs/api/class-page#page-goto
13
+ - if you don't pass anything, we default to a timeout of 60000 and wait for `networkidle`
14
+ - `browserContextOptions` are the options for the browser context in Playwright: https://playwright.dev/docs/api/class-browser#browser-new-context
15
+ - if you don't pass anything, we default to set `ignoreHTTPSErrors` to `true`
16
+
17
+ We finally added tests too! ✌️
18
+
19
+ ## Breaking Changes
20
+
21
+ - We don't allow `options` in the payload anymore. Those are `pdfOptions` now. To adapt your code, rename `options` to `pdfOptions` and you should be good to go.
22
+ This change was needed to allow more options for other methods we're using (like `timeout` for `goto`).
23
+ - We have stricter validation now and don't allow unknown keys in the option objects now. Previously we've ignored them, but we've now opted to bail out with
24
+ an validation error so the user gets notified on typos and such.
25
+
26
+ - 484b197: Return an error if page could not be loaded
27
+
28
+ ## Breaking Changes
29
+
30
+ Previously we've ignored if the page could not be loaded and tried to create a PDF anyway. However,
31
+ this lead to PDFs with random error messages on them, instead of a proper error in logging or similar.
32
+
33
+ To provide an better experience, we've opted to change this, so errors are caught earlier.
34
+
35
+ ### Minor Changes
36
+
37
+ - c7e554e: Update to Playwright 1.30.0
38
+ - 8b2ed78: Merge passed options with default options
39
+
3
40
  ## 1.5.1
4
41
 
5
42
  ### Patch Changes
package/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM mcr.microsoft.com/playwright:v1.29.2-jammy
1
+ FROM mcr.microsoft.com/playwright:v1.30.0-jammy
2
2
 
3
3
  ENV NODE_ENV=production
4
4
  ENV SERVER_PORT=8000
@@ -15,7 +15,7 @@ COPY ["package.json", "package-lock.json*", "./"]
15
15
 
16
16
  RUN npm install --omit=dev --ignore-scripts
17
17
 
18
- COPY ["index.js", "pdf_request_body.json", "./"]
18
+ COPY ["server.js", "app.js", "pdf_request_body.json", "./"]
19
19
 
20
20
  ENTRYPOINT ["tini", "-v", "--"]
21
- CMD ["node", "index.js"]
21
+ CMD ["node", "server.js"]
package/README.md CHANGED
@@ -34,11 +34,17 @@ The server exposes the `/pdf` endpoint that listens to a POST request and expect
34
34
  ```json
35
35
  {
36
36
  "url": "https://your-target-url.com/site/you/want/a/pdf/of",
37
- "options": {}
37
+ "pdfOptions": {},
38
+ "gotoOptions": {},
39
+ "browserContextOptions": {}
38
40
  }
39
41
  ```
40
42
 
41
- `options` are the options Playwright knows about PDF: https://playwright.dev/docs/api/class-page#page-pdf. We pass them to Playwright directly, so please refer to their docs.
43
+ - `pdfOptions` are the options for the `pdf` call in Playwright: https://playwright.dev/docs/api/class-page#page-pdf.
44
+ - `gotoOptions` are the options for the `goto` method in Playwright: https://playwright.dev/docs/api/class-page#page-goto
45
+ - if you don't pass anything, we default to a timeout of 60000 and wait for `networkidle`
46
+ - `browserContextOptions` are the options for the browser context in Playwright: https://playwright.dev/docs/api/class-browser#browser-new-context
47
+ - if you don't pass anything, we default to set `ignoreHTTPSErrors` to `true`
42
48
 
43
49
  The response is the PDF file.
44
50
 
package/app.js ADDED
@@ -0,0 +1,138 @@
1
+ // @ts-check
2
+
3
+ import { fastify } from "fastify";
4
+ import { chromium } from "playwright-core";
5
+ import { readFileSync } from "node:fs";
6
+
7
+ // we'd love to do this, but eslint doesn't like import assertions yet
8
+ // import PdfRequestBodySchema from "./pdf_request_body.json" assert { type: "json" };
9
+ const PdfRequestBodySchema = JSON.parse(
10
+ readFileSync("./pdf_request_body.json", { encoding: "utf-8" })
11
+ );
12
+
13
+ /**
14
+ * @typedef {Parameters<import("playwright-core").Page["pdf"]>[0]} PdfOptions
15
+ * @typedef {Parameters<import("playwright-core").Page["goto"]>[1]} GotoOptions
16
+ * @typedef {import("playwright-core").BrowserContextOptions} BrowserContextOptions
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} PdfRequestBody
21
+ * @property {string} url
22
+ * @property {PdfOptions} pdfOptions
23
+ * @property {BrowserContextOptions} browserContextOptions
24
+ * @property {GotoOptions} gotoOptions
25
+ */
26
+
27
+ export const defaultBrowserContextOptions = /** @type {const} */ ({
28
+ ignoreHTTPSErrors: true,
29
+ });
30
+ export const defaultGotoOptions = /** @type {const} */ ({
31
+ timeout: 60000,
32
+ waitUntil: "networkidle",
33
+ });
34
+
35
+ /**
36
+ *
37
+ * @param {string} url
38
+ * @param {PdfOptions} pdfOptions
39
+ * @param {BrowserContextOptions} browserContextOptions
40
+ * @param {GotoOptions} gotoOptions
41
+ * @returns Buffer
42
+ */
43
+ const createPDF = async (
44
+ url,
45
+ pdfOptions = {},
46
+ browserContextOptions = {},
47
+ gotoOptions = {}
48
+ ) => {
49
+ const browser = await chromium.launch();
50
+
51
+ try {
52
+ const context = await browser.newContext(browserContextOptions);
53
+ const page = await context.newPage();
54
+
55
+ const response = await page.goto(url, gotoOptions);
56
+ if (response?.ok()) {
57
+ return page.pdf(pdfOptions);
58
+ }
59
+
60
+ return Promise.reject({
61
+ error: true,
62
+ message: await response?.text(),
63
+ status: response?.status(),
64
+ statusText: response?.statusText(),
65
+ });
66
+ } finally {
67
+ await browser.close();
68
+ }
69
+ };
70
+
71
+ /**
72
+ *
73
+ * @param {import("fastify").FastifyServerOptions} opts
74
+ */
75
+ function build(opts = {}) {
76
+ const fastifyOptions = {
77
+ ...opts,
78
+ ajv: {
79
+ customOptions: {
80
+ // we want validation errors if someone passes an unknown option
81
+ removeAdditional: false,
82
+ // Don't adhere to ajvs strict types, as our schema is auto generated anyway
83
+ allowUnionTypes: true,
84
+ },
85
+ },
86
+ };
87
+ const app = fastify(fastifyOptions);
88
+
89
+ app.post(
90
+ "/pdf",
91
+ { schema: { body: PdfRequestBodySchema }, attachValidation: true },
92
+ /**
93
+ *
94
+ * @param {import('fastify').FastifyRequest<{Body: PdfRequestBody}>} request
95
+ * @param {import('fastify').FastifyReply} response
96
+ */
97
+ async (request, response) => {
98
+ if (request.validationError) {
99
+ response.code(400);
100
+
101
+ return {
102
+ error: true,
103
+ message: request.validationError.message,
104
+ };
105
+ }
106
+
107
+ const { url, pdfOptions, browserContextOptions, gotoOptions } =
108
+ request.body;
109
+
110
+ const pdfOpts = { ...pdfOptions };
111
+
112
+ const contextOpts = {
113
+ ...defaultBrowserContextOptions,
114
+ ...browserContextOptions,
115
+ };
116
+
117
+ const gotoOpts = {
118
+ ...defaultGotoOptions,
119
+ ...gotoOptions,
120
+ };
121
+
122
+ try {
123
+ return await createPDF(url, pdfOpts, contextOpts, gotoOpts);
124
+ } catch (error) {
125
+ // @ts-ignore
126
+ response.code(error.status ?? 500);
127
+
128
+ return error;
129
+ }
130
+ }
131
+ );
132
+
133
+ app.get("/health", async (_, response) => response.code(200).send());
134
+
135
+ return app;
136
+ }
137
+
138
+ export default build;
package/nodemon.json CHANGED
@@ -1,4 +1,9 @@
1
1
  {
2
- "watch": ["index.js", "schemas/pdf_request_body.json", "package-lock.json"],
2
+ "watch": [
3
+ "app.js",
4
+ "server.js",
5
+ "schemas/pdf_request_body.json",
6
+ "package-lock.json"
7
+ ],
3
8
  "ext": "js,json"
4
9
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bits-to-dead-trees",
3
3
  "author": "Robin Mehner <robin@coding-robin.de>",
4
- "version": "1.5.1",
4
+ "version": "2.0.0",
5
5
  "description": "Turn HTML into PDF files",
6
6
  "repository": "github:rmehner/bits-to-dead-trees",
7
7
  "license": "Apache-2.0",
@@ -13,32 +13,36 @@
13
13
  "engines": {
14
14
  "node": ">= 16"
15
15
  },
16
- "main": "index.js",
16
+ "main": "server.js",
17
+ "type": "module",
17
18
  "scripts": {
18
19
  "check-format": "prettier --check **/*.{js,ts,json,md}",
19
20
  "lint": "eslint *.js",
20
21
  "format": "prettier --write **/*.{js,ts,json,md}",
21
- "start": "node index.js",
22
- "start:dev": "nodemon --inspect index.js",
23
- "dump-schemas": "typescript-json-schema types/schemas.ts PdfRequestBody --out pdf_request_body.json && npm run format",
22
+ "start": "node server.js",
23
+ "start:dev": "nodemon --inspect server.js",
24
+ "dump-schemas": "typescript-json-schema --noExtraProps --required --strictNullChecks types/schemas.ts PdfRequestBody --out pdf_request_body.json",
24
25
  "update-schemas": "npm run dump-schemas && npm run format",
25
26
  "postinstall": "npx playwright install chromium",
26
27
  "add-changeset": "changeset",
27
28
  "release": "changeset publish",
29
+ "test": "vitest --reporter verbose",
28
30
  "ts-check": "npx tsc --noEmit"
29
31
  },
30
32
  "dependencies": {
31
- "fastify": "^4.11.0",
32
- "playwright-core": "^1.29.2"
33
+ "fastify": "^4.12.0",
34
+ "playwright-core": "^1.30.0"
33
35
  },
34
36
  "devDependencies": {
35
37
  "@changesets/cli": "^2.26.0",
36
- "eslint": "^8.31.0",
38
+ "@types/lodash": "^4.14.191",
39
+ "eslint": "^8.32.0",
37
40
  "eslint-config-prettier": "^8.6.0",
38
41
  "nodemon": "^2.0.20",
39
42
  "pino-pretty": "^9.1.1",
40
- "prettier": "^2.8.2",
43
+ "prettier": "^2.8.3",
41
44
  "tslib": "^2.4.1",
42
- "typescript-json-schema": "^0.55.0"
45
+ "typescript-json-schema": "^0.55.0",
46
+ "vitest": "^0.28.2"
43
47
  }
44
48
  }
@@ -1,84 +1,581 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "properties": {
4
- "options": {
3
+ "additionalProperties": false,
4
+ "definitions": {
5
+ "BrowserContextOptions": {
6
+ "additionalProperties": false,
5
7
  "properties": {
6
- "displayHeaderFooter": {
7
- "description": "Display header and footer. Defaults to `false`.",
8
+ "acceptDownloads": {
9
+ "description": "Whether to automatically download all the attachments. Defaults to `true` where all the downloads are accepted.",
8
10
  "type": "boolean"
9
11
  },
10
- "footerTemplate": {
11
- "description": "HTML template for the print footer. Should use the same format as the `headerTemplate`.",
12
+ "baseURL": {
13
+ "description": "When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto),\n[page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route),\n[page.waitForURL(url[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-url),\n[page.waitForRequest(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-request),\nor\n[page.waitForResponse(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-response)\nit takes the base URL in consideration by using the\n[`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor for building the corresponding URL.\nExamples:\n- baseURL: `http://localhost:3000` and navigating to `/bar.html` results in `http://localhost:3000/bar.html`\n- baseURL: `http://localhost:3000/foo/` and navigating to `./bar.html` results in\n `http://localhost:3000/foo/bar.html`\n- baseURL: `http://localhost:3000/foo` (without trailing slash) and navigating to `./bar.html` results in\n `http://localhost:3000/bar.html`",
12
14
  "type": "string"
13
15
  },
14
- "format": {
15
- "description": "Paper format. If set, takes priority over `width` or `height` options. Defaults to 'Letter'.",
16
- "type": "string"
16
+ "bypassCSP": {
17
+ "description": "Toggles bypassing page's Content-Security-Policy.",
18
+ "type": "boolean"
19
+ },
20
+ "colorScheme": {
21
+ "anyOf": [
22
+ {
23
+ "enum": ["dark", "light", "no-preference"],
24
+ "type": "string"
25
+ },
26
+ {
27
+ "type": "null"
28
+ }
29
+ ],
30
+ "description": "Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See\n[page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.\nPassing `null` resets emulation to system defaults. Defaults to `'light'`."
31
+ },
32
+ "deviceScaleFactor": {
33
+ "description": "Specify device scale factor (can be thought of as dpr). Defaults to `1`.",
34
+ "type": "number"
35
+ },
36
+ "extraHTTPHeaders": {
37
+ "additionalProperties": {
38
+ "type": "string"
39
+ },
40
+ "description": "An object containing additional HTTP headers to be sent with every request.",
41
+ "type": "object"
42
+ },
43
+ "forcedColors": {
44
+ "anyOf": [
45
+ {
46
+ "enum": ["active", "none"],
47
+ "type": "string"
48
+ },
49
+ {
50
+ "type": "null"
51
+ }
52
+ ],
53
+ "description": "Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See\n[page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.\nPassing `null` resets emulation to system defaults. Defaults to `'none'`."
54
+ },
55
+ "geolocation": {
56
+ "$ref": "#/definitions/Geolocation"
57
+ },
58
+ "hasTouch": {
59
+ "description": "Specifies if viewport supports touch events. Defaults to false.",
60
+ "type": "boolean"
61
+ },
62
+ "httpCredentials": {
63
+ "$ref": "#/definitions/HTTPCredentials",
64
+ "description": "Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)."
65
+ },
66
+ "ignoreHTTPSErrors": {
67
+ "description": "Whether to ignore HTTPS errors when sending network requests. Defaults to `false`.",
68
+ "type": "boolean"
69
+ },
70
+ "isMobile": {
71
+ "description": "Whether the `meta viewport` tag is taken into account and touch events are enabled. Defaults to `false`. Not\nsupported in Firefox.",
72
+ "type": "boolean"
73
+ },
74
+ "javaScriptEnabled": {
75
+ "description": "Whether or not to enable JavaScript in the context. Defaults to `true`.",
76
+ "type": "boolean"
17
77
  },
18
- "headerTemplate": {
19
- "description": "HTML template for the print header. Should be valid HTML markup with following classes used to inject printing\nvalues into them:\n- `'date'` formatted print date\n- `'title'` document title\n- `'url'` document location\n- `'pageNumber'` current page number\n- `'totalPages'` total pages in the document",
78
+ "locale": {
79
+ "description": "Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value,\n`Accept-Language` request header value as well as number and date formatting rules.",
20
80
  "type": "string"
21
81
  },
22
- "height": {
23
- "description": "Paper height, accepts values labeled with units.",
24
- "type": ["string", "number"]
82
+ "logger": {
83
+ "$ref": "#/definitions/Logger",
84
+ "description": "Logger sink for Playwright logging."
25
85
  },
26
- "landscape": {
27
- "description": "Paper orientation. Defaults to `false`.",
86
+ "offline": {
87
+ "description": "Whether to emulate network being offline. Defaults to `false`.",
28
88
  "type": "boolean"
29
89
  },
30
- "margin": {
31
- "description": "Paper margins, defaults to none.",
90
+ "permissions": {
91
+ "description": "A list of permissions to grant to all pages in this context. See\n[browserContext.grantPermissions(permissions[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions)\nfor more details.",
92
+ "items": {
93
+ "type": "string"
94
+ },
95
+ "type": "array"
96
+ },
97
+ "proxy": {
98
+ "additionalProperties": false,
99
+ "description": "Network proxy settings to use with this context.\n\n**NOTE** For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If\nall contexts override the proxy, global proxy will be never used and can be any string, for example `launch({\nproxy: { server: 'http://per-context' } })`.",
32
100
  "properties": {
33
- "bottom": {
34
- "description": "Bottom margin, accepts values labeled with units. Defaults to `0`.",
35
- "type": ["string", "number"]
101
+ "bypass": {
102
+ "description": "Optional comma-separated domains to bypass proxy, for example `\".com, chromium.org, .domain.com\"`.",
103
+ "type": "string"
36
104
  },
37
- "left": {
38
- "description": "Left margin, accepts values labeled with units. Defaults to `0`.",
39
- "type": ["string", "number"]
105
+ "password": {
106
+ "description": "Optional password to use if HTTP proxy requires authentication.",
107
+ "type": "string"
40
108
  },
41
- "right": {
42
- "description": "Right margin, accepts values labeled with units. Defaults to `0`.",
43
- "type": ["string", "number"]
109
+ "server": {
110
+ "description": "Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example `http://myproxy.com:3128` or\n`socks5://myproxy.com:3128`. Short form `myproxy.com:3128` is considered an HTTP proxy.",
111
+ "type": "string"
44
112
  },
45
- "top": {
46
- "description": "Top margin, accepts values labeled with units. Defaults to `0`.",
47
- "type": ["string", "number"]
113
+ "username": {
114
+ "description": "Optional username to use if HTTP proxy requires authentication.",
115
+ "type": "string"
116
+ }
117
+ },
118
+ "required": ["server"],
119
+ "type": "object"
120
+ },
121
+ "recordHar": {
122
+ "additionalProperties": false,
123
+ "description": "Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into `recordHar.path` file.\nIf not specified, the HAR is not recorded. Make sure to await\n[browserContext.close()](https://playwright.dev/docs/api/class-browsercontext#browser-context-close) for the HAR to\nbe saved.",
124
+ "properties": {
125
+ "content": {
126
+ "description": "Optional setting to control resource content management. If `omit` is specified, content is not persisted. If\n`attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is\nspecified, content is stored inline the HAR file as per HAR specification. Defaults to `attach` for `.zip` output\nfiles and to `embed` for all other file extensions.",
127
+ "enum": ["attach", "embed", "omit"],
128
+ "type": "string"
129
+ },
130
+ "mode": {
131
+ "description": "When set to `minimal`, only record information necessary for routing from HAR. This omits sizes, timing, page,\ncookies, security and other types of HAR information that are not used when replaying from HAR. Defaults to `full`.",
132
+ "enum": ["full", "minimal"],
133
+ "type": "string"
134
+ },
135
+ "omitContent": {
136
+ "description": "Optional setting to control whether to omit request content from the HAR. Defaults to `false`. Deprecated, use\n`content` policy instead.",
137
+ "type": "boolean"
138
+ },
139
+ "path": {
140
+ "description": "Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `content: 'attach'` is used by\ndefault.",
141
+ "type": "string"
142
+ },
143
+ "urlFilter": {
144
+ "anyOf": [
145
+ {
146
+ "$ref": "#/definitions/RegExp"
147
+ },
148
+ {
149
+ "type": "string"
150
+ }
151
+ ],
152
+ "description": "A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was\nprovided and the passed URL is a path, it gets merged via the\n[`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor."
153
+ }
154
+ },
155
+ "required": ["path"],
156
+ "type": "object"
157
+ },
158
+ "recordVideo": {
159
+ "additionalProperties": false,
160
+ "description": "Enables video recording for all pages into `recordVideo.dir` directory. If not specified videos are not recorded.\nMake sure to await\n[browserContext.close()](https://playwright.dev/docs/api/class-browsercontext#browser-context-close) for videos to\nbe saved.",
161
+ "properties": {
162
+ "dir": {
163
+ "description": "Path to the directory to put videos into.",
164
+ "type": "string"
165
+ },
166
+ "size": {
167
+ "additionalProperties": false,
168
+ "description": "Optional dimensions of the recorded videos. If not specified the size will be equal to `viewport` scaled down to\nfit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450. Actual picture of\neach page will be scaled down if necessary to fit the specified size.",
169
+ "properties": {
170
+ "height": {
171
+ "description": "Video frame height.",
172
+ "type": "number"
173
+ },
174
+ "width": {
175
+ "description": "Video frame width.",
176
+ "type": "number"
177
+ }
178
+ },
179
+ "required": ["height", "width"],
180
+ "type": "object"
181
+ }
182
+ },
183
+ "required": ["dir"],
184
+ "type": "object"
185
+ },
186
+ "reducedMotion": {
187
+ "anyOf": [
188
+ {
189
+ "enum": ["no-preference", "reduce"],
190
+ "type": "string"
191
+ },
192
+ {
193
+ "type": "null"
194
+ }
195
+ ],
196
+ "description": "Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See\n[page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.\nPassing `null` resets emulation to system defaults. Defaults to `'no-preference'`."
197
+ },
198
+ "screen": {
199
+ "additionalProperties": false,
200
+ "description": "Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the\n`viewport` is set.",
201
+ "properties": {
202
+ "height": {
203
+ "description": "page height in pixels.",
204
+ "type": "number"
205
+ },
206
+ "width": {
207
+ "description": "page width in pixels.",
208
+ "type": "number"
209
+ }
210
+ },
211
+ "required": ["height", "width"],
212
+ "type": "object"
213
+ },
214
+ "serviceWorkers": {
215
+ "description": "Whether to allow sites to register Service workers. Defaults to `'allow'`.\n- `'allow'`: [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) can be\n registered.\n- `'block'`: Playwright will block all registration of Service Workers.",
216
+ "enum": ["allow", "block"],
217
+ "type": "string"
218
+ },
219
+ "storageState": {
220
+ "anyOf": [
221
+ {
222
+ "additionalProperties": false,
223
+ "properties": {
224
+ "cookies": {
225
+ "description": "cookies to set for context",
226
+ "items": {
227
+ "additionalProperties": false,
228
+ "properties": {
229
+ "domain": {
230
+ "description": "domain and path are required",
231
+ "type": "string"
232
+ },
233
+ "expires": {
234
+ "description": "Unix time in seconds.",
235
+ "type": "number"
236
+ },
237
+ "httpOnly": {
238
+ "type": "boolean"
239
+ },
240
+ "name": {
241
+ "type": "string"
242
+ },
243
+ "path": {
244
+ "description": "domain and path are required",
245
+ "type": "string"
246
+ },
247
+ "sameSite": {
248
+ "description": "sameSite flag",
249
+ "enum": ["Lax", "None", "Strict"],
250
+ "type": "string"
251
+ },
252
+ "secure": {
253
+ "type": "boolean"
254
+ },
255
+ "value": {
256
+ "type": "string"
257
+ }
258
+ },
259
+ "required": [
260
+ "domain",
261
+ "expires",
262
+ "httpOnly",
263
+ "name",
264
+ "path",
265
+ "sameSite",
266
+ "secure",
267
+ "value"
268
+ ],
269
+ "type": "object"
270
+ },
271
+ "type": "array"
272
+ },
273
+ "origins": {
274
+ "description": "localStorage to set for context",
275
+ "items": {
276
+ "additionalProperties": false,
277
+ "properties": {
278
+ "localStorage": {
279
+ "items": {
280
+ "additionalProperties": false,
281
+ "properties": {
282
+ "name": {
283
+ "type": "string"
284
+ },
285
+ "value": {
286
+ "type": "string"
287
+ }
288
+ },
289
+ "required": ["name", "value"],
290
+ "type": "object"
291
+ },
292
+ "type": "array"
293
+ },
294
+ "origin": {
295
+ "type": "string"
296
+ }
297
+ },
298
+ "required": ["localStorage", "origin"],
299
+ "type": "object"
300
+ },
301
+ "type": "array"
302
+ }
303
+ },
304
+ "required": ["cookies", "origins"],
305
+ "type": "object"
306
+ },
307
+ {
308
+ "type": "string"
309
+ }
310
+ ],
311
+ "description": "Populates context with given storage state. This option can be used to initialize context with logged-in\ninformation obtained via\n[browserContext.storageState([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state).\nEither a path to the file with saved storage, or an object with the following fields:"
312
+ },
313
+ "strictSelectors": {
314
+ "description": "If set to true, enables strict selectors mode for this context. In the strict selectors mode all operations on\nselectors that imply single target DOM element will throw when more than one element matches the selector. This\noption does not affect any Locator APIs (Locators are always strict). See [Locator] to learn more about the strict\nmode.",
315
+ "type": "boolean"
316
+ },
317
+ "timezoneId": {
318
+ "description": "Changes the timezone of the context. See\n[ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1)\nfor a list of supported timezone IDs.",
319
+ "type": "string"
320
+ },
321
+ "userAgent": {
322
+ "description": "Specific user agent to use in this context.",
323
+ "type": "string"
324
+ },
325
+ "videoSize": {
326
+ "additionalProperties": false,
327
+ "properties": {
328
+ "height": {
329
+ "description": "Video frame height.",
330
+ "type": "number"
331
+ },
332
+ "width": {
333
+ "description": "Video frame width.",
334
+ "type": "number"
48
335
  }
49
336
  },
337
+ "required": ["height", "width"],
50
338
  "type": "object"
51
339
  },
52
- "pageRanges": {
53
- "description": "Paper ranges to print, e.g., '1-5, 8, 11-13'. Defaults to the empty string, which means print all pages.",
340
+ "videosPath": {
341
+ "type": "string"
342
+ },
343
+ "viewport": {
344
+ "anyOf": [
345
+ {
346
+ "$ref": "#/definitions/ViewportSize"
347
+ },
348
+ {
349
+ "type": "null"
350
+ }
351
+ ],
352
+ "description": "Emulates consistent viewport for each page. Defaults to an 1280x720 viewport. `null` disables the default viewport."
353
+ }
354
+ },
355
+ "type": "object"
356
+ },
357
+ "Geolocation": {
358
+ "additionalProperties": false,
359
+ "properties": {
360
+ "accuracy": {
361
+ "description": "Non-negative accuracy value. Defaults to `0`.",
362
+ "type": "number"
363
+ },
364
+ "latitude": {
365
+ "description": "Latitude between -90 and 90.",
366
+ "type": "number"
367
+ },
368
+ "longitude": {
369
+ "description": "Longitude between -180 and 180.",
370
+ "type": "number"
371
+ }
372
+ },
373
+ "required": ["latitude", "longitude"],
374
+ "type": "object"
375
+ },
376
+ "HTTPCredentials": {
377
+ "additionalProperties": false,
378
+ "properties": {
379
+ "password": {
54
380
  "type": "string"
55
381
  },
56
- "path": {
57
- "description": "The file path to save the PDF to. If `path` is a relative path, then it is resolved relative to the current working\ndirectory. If no path is provided, the PDF won't be saved to the disk.",
382
+ "username": {
383
+ "type": "string"
384
+ }
385
+ },
386
+ "required": ["password", "username"],
387
+ "type": "object"
388
+ },
389
+ "Logger": {
390
+ "additionalProperties": false,
391
+ "description": "Playwright generates a lot of logs and they are accessible via the pluggable logger sink.\n\n```js\nconst { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.\n\n(async () => {\n const browser = await chromium.launch({\n logger: {\n isEnabled: (name, severity) => name === 'browser',\n log: (name, severity, message, args) => console.log(`${name} ${message}`)\n }\n });\n ...\n})();\n```",
392
+ "type": "object"
393
+ },
394
+ "RegExp": {
395
+ "additionalProperties": false,
396
+ "properties": {
397
+ "dotAll": {
398
+ "type": "boolean"
399
+ },
400
+ "flags": {
58
401
  "type": "string"
59
402
  },
60
- "preferCSSPageSize": {
61
- "description": "Give any CSS `@page` size declared in the page priority over what is declared in `width` and `height` or `format`\noptions. Defaults to `false`, which will scale the content to fit the paper size.",
403
+ "global": {
404
+ "type": "boolean"
405
+ },
406
+ "ignoreCase": {
62
407
  "type": "boolean"
63
408
  },
64
- "printBackground": {
65
- "description": "Print background graphics. Defaults to `false`.",
409
+ "lastIndex": {
410
+ "type": "number"
411
+ },
412
+ "multiline": {
66
413
  "type": "boolean"
67
414
  },
68
- "scale": {
69
- "description": "Scale of the webpage rendering. Defaults to `1`. Scale amount must be between 0.1 and 2.",
415
+ "source": {
416
+ "type": "string"
417
+ },
418
+ "sticky": {
419
+ "type": "boolean"
420
+ },
421
+ "unicode": {
422
+ "type": "boolean"
423
+ }
424
+ },
425
+ "required": [
426
+ "dotAll",
427
+ "flags",
428
+ "global",
429
+ "ignoreCase",
430
+ "lastIndex",
431
+ "multiline",
432
+ "source",
433
+ "sticky",
434
+ "unicode"
435
+ ],
436
+ "type": "object"
437
+ },
438
+ "ViewportSize": {
439
+ "additionalProperties": false,
440
+ "properties": {
441
+ "height": {
442
+ "description": "page height in pixels.",
70
443
  "type": "number"
71
444
  },
72
445
  "width": {
73
- "description": "Paper width, accepts values labeled with units.",
74
- "type": ["string", "number"]
446
+ "description": "page width in pixels.",
447
+ "type": "number"
75
448
  }
76
449
  },
450
+ "required": ["height", "width"],
77
451
  "type": "object"
452
+ }
453
+ },
454
+ "properties": {
455
+ "browserContextOptions": {
456
+ "anyOf": [
457
+ {
458
+ "$ref": "#/definitions/BrowserContextOptions"
459
+ },
460
+ {
461
+ "type": "null"
462
+ }
463
+ ]
464
+ },
465
+ "gotoOptions": {
466
+ "anyOf": [
467
+ {
468
+ "additionalProperties": false,
469
+ "properties": {
470
+ "referer": {
471
+ "description": "Referer header value. If provided it will take preference over the referer header value set by\n[page.setExtraHTTPHeaders(headers)](https://playwright.dev/docs/api/class-page#page-set-extra-http-headers).",
472
+ "type": "string"
473
+ },
474
+ "timeout": {
475
+ "description": "Maximum operation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can\nbe changed by using the\n[browserContext.setDefaultNavigationTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-navigation-timeout),\n[browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout),\n[page.setDefaultNavigationTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout)\nor [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.",
476
+ "type": "number"
477
+ },
478
+ "waitUntil": {
479
+ "description": "When to consider operation succeeded, defaults to `load`. Events can be either:\n- `'domcontentloaded'` - consider operation to be finished when the `DOMContentLoaded` event is fired.\n- `'load'` - consider operation to be finished when the `load` event is fired.\n- `'networkidle'` - consider operation to be finished when there are no network connections for at least `500`\n ms.\n- `'commit'` - consider operation to be finished when network response is received and the document started\n loading.",
480
+ "enum": ["commit", "domcontentloaded", "load", "networkidle"],
481
+ "type": "string"
482
+ }
483
+ },
484
+ "type": "object"
485
+ },
486
+ {
487
+ "type": "null"
488
+ }
489
+ ]
490
+ },
491
+ "pdfOptions": {
492
+ "anyOf": [
493
+ {
494
+ "additionalProperties": false,
495
+ "properties": {
496
+ "displayHeaderFooter": {
497
+ "description": "Display header and footer. Defaults to `false`.",
498
+ "type": "boolean"
499
+ },
500
+ "footerTemplate": {
501
+ "description": "HTML template for the print footer. Should use the same format as the `headerTemplate`.",
502
+ "type": "string"
503
+ },
504
+ "format": {
505
+ "description": "Paper format. If set, takes priority over `width` or `height` options. Defaults to 'Letter'.",
506
+ "type": "string"
507
+ },
508
+ "headerTemplate": {
509
+ "description": "HTML template for the print header. Should be valid HTML markup with following classes used to inject printing\nvalues into them:\n- `'date'` formatted print date\n- `'title'` document title\n- `'url'` document location\n- `'pageNumber'` current page number\n- `'totalPages'` total pages in the document",
510
+ "type": "string"
511
+ },
512
+ "height": {
513
+ "description": "Paper height, accepts values labeled with units.",
514
+ "type": ["string", "number"]
515
+ },
516
+ "landscape": {
517
+ "description": "Paper orientation. Defaults to `false`.",
518
+ "type": "boolean"
519
+ },
520
+ "margin": {
521
+ "additionalProperties": false,
522
+ "description": "Paper margins, defaults to none.",
523
+ "properties": {
524
+ "bottom": {
525
+ "description": "Bottom margin, accepts values labeled with units. Defaults to `0`.",
526
+ "type": ["string", "number"]
527
+ },
528
+ "left": {
529
+ "description": "Left margin, accepts values labeled with units. Defaults to `0`.",
530
+ "type": ["string", "number"]
531
+ },
532
+ "right": {
533
+ "description": "Right margin, accepts values labeled with units. Defaults to `0`.",
534
+ "type": ["string", "number"]
535
+ },
536
+ "top": {
537
+ "description": "Top margin, accepts values labeled with units. Defaults to `0`.",
538
+ "type": ["string", "number"]
539
+ }
540
+ },
541
+ "type": "object"
542
+ },
543
+ "pageRanges": {
544
+ "description": "Paper ranges to print, e.g., '1-5, 8, 11-13'. Defaults to the empty string, which means print all pages.",
545
+ "type": "string"
546
+ },
547
+ "path": {
548
+ "description": "The file path to save the PDF to. If `path` is a relative path, then it is resolved relative to the current working\ndirectory. If no path is provided, the PDF won't be saved to the disk.",
549
+ "type": "string"
550
+ },
551
+ "preferCSSPageSize": {
552
+ "description": "Give any CSS `@page` size declared in the page priority over what is declared in `width` and `height` or `format`\noptions. Defaults to `false`, which will scale the content to fit the paper size.",
553
+ "type": "boolean"
554
+ },
555
+ "printBackground": {
556
+ "description": "Print background graphics. Defaults to `false`.",
557
+ "type": "boolean"
558
+ },
559
+ "scale": {
560
+ "description": "Scale of the webpage rendering. Defaults to `1`. Scale amount must be between 0.1 and 2.",
561
+ "type": "number"
562
+ },
563
+ "width": {
564
+ "description": "Paper width, accepts values labeled with units.",
565
+ "type": ["string", "number"]
566
+ }
567
+ },
568
+ "type": "object"
569
+ },
570
+ {
571
+ "type": "null"
572
+ }
573
+ ]
78
574
  },
79
575
  "url": {
80
576
  "type": "string"
81
577
  }
82
578
  },
579
+ "required": ["url"],
83
580
  "type": "object"
84
581
  }
package/server.js ADDED
@@ -0,0 +1,29 @@
1
+ import app from "./app.js";
2
+ const SERVER_PORT = process.env["SERVER_PORT"] ?? 8000;
3
+ const SERVER_ADDRESS = process.env["SERVER_ADDRESS"] ?? "localhost";
4
+
5
+ const server = app({
6
+ logger: {
7
+ transport:
8
+ process.env["NODE_ENV"] !== "production"
9
+ ? {
10
+ target: "pino-pretty",
11
+ options: {
12
+ translateTime: "HH:MM:ss Z",
13
+ ignore: "pid,hostname",
14
+ },
15
+ }
16
+ : undefined,
17
+ },
18
+ });
19
+
20
+ const startServer = async () => {
21
+ try {
22
+ await server.listen({ port: +SERVER_PORT, host: SERVER_ADDRESS });
23
+ } catch (error) {
24
+ console.error(error);
25
+ process.exit(1);
26
+ }
27
+ };
28
+
29
+ startServer();
@@ -0,0 +1,226 @@
1
+ import { chromium } from "playwright-core";
2
+ import { afterEach, beforeEach, describe, expect, it, vi, Mock } from "vitest";
3
+
4
+ import build, {
5
+ defaultBrowserContextOptions,
6
+ defaultGotoOptions,
7
+ } from "../app";
8
+ const app = build();
9
+
10
+ describe("GET /health", () => {
11
+ it("provides a simple health check", async () => {
12
+ const response = await app.inject({
13
+ method: "GET",
14
+ url: "/health",
15
+ });
16
+
17
+ expect(response.statusCode).toEqual(200);
18
+ });
19
+ });
20
+
21
+ vi.mock("playwright-core", () => {
22
+ const playwright = {
23
+ chromium: {
24
+ launch: vi.fn(),
25
+ },
26
+ };
27
+ return playwright;
28
+ });
29
+
30
+ describe("POST /pdf", () => {
31
+ afterEach(() => {
32
+ vi.resetAllMocks();
33
+ });
34
+
35
+ let gotoMethod = vi.fn();
36
+ let pdf = vi.fn();
37
+ let newPage = vi.fn();
38
+ let newContext = vi.fn();
39
+ let close = vi.fn();
40
+
41
+ beforeEach(() => {
42
+ gotoMethod = vi.fn().mockResolvedValue({
43
+ ok() {
44
+ return true;
45
+ },
46
+ status() {
47
+ return 200;
48
+ },
49
+ });
50
+ pdf = vi.fn().mockResolvedValue(Buffer.from("pdf"));
51
+ newPage = vi.fn().mockResolvedValue({ goto: gotoMethod, pdf });
52
+ newContext = vi.fn().mockResolvedValue({ newPage });
53
+ close = vi.fn();
54
+
55
+ (chromium.launch as Mock<any[], any>).mockResolvedValue({
56
+ newContext,
57
+ close,
58
+ });
59
+ });
60
+
61
+ it("sends a buffer of the PDF as response", async () => {
62
+ const response = await app.inject({
63
+ method: "POST",
64
+ url: "/pdf",
65
+ payload: { url: "/foobar" },
66
+ });
67
+
68
+ expect(gotoMethod).toHaveBeenCalledTimes(1);
69
+ expect(gotoMethod).toHaveBeenLastCalledWith("/foobar", defaultGotoOptions);
70
+
71
+ expect(response.statusCode).toEqual(200);
72
+ expect(response.payload).toEqual("pdf");
73
+ });
74
+
75
+ it("allows to overwrite gotoOptions but merges them with the defaults", async () => {
76
+ await app.inject({
77
+ method: "POST",
78
+ url: "/pdf",
79
+ payload: { url: "/foobar", gotoOptions: { timeout: 300 } },
80
+ });
81
+
82
+ expect(gotoMethod).toHaveBeenCalledWith("/foobar", {
83
+ timeout: 300,
84
+ waitUntil: "networkidle",
85
+ });
86
+ });
87
+
88
+ it("uses the default browserContextOptions if not passed or empty", async () => {
89
+ await app.inject({
90
+ method: "POST",
91
+ url: "/pdf",
92
+ payload: { url: "/foobar", browserContextOptions: {} },
93
+ });
94
+
95
+ expect(newContext).toHaveBeenCalledWith(defaultBrowserContextOptions);
96
+ });
97
+
98
+ it("allows to overwrite browserContextOptions", async () => {
99
+ await app.inject({
100
+ method: "POST",
101
+ url: "/pdf",
102
+ payload: {
103
+ url: "/foobar",
104
+ browserContextOptions: { ignoreHTTPSErrors: false },
105
+ },
106
+ });
107
+
108
+ expect(newContext).toHaveBeenCalledWith({
109
+ ignoreHTTPSErrors: false,
110
+ });
111
+ });
112
+
113
+ it("returns validation errors", async () => {
114
+ const response = await app.inject({
115
+ method: "POST",
116
+ url: "/pdf",
117
+ payload: {
118
+ url: "/foobar",
119
+ gotoOptions: { doesNot: "exist" },
120
+ },
121
+ });
122
+
123
+ expect(response.statusCode).toEqual(400);
124
+
125
+ const parsedResponse = JSON.parse(response.body);
126
+
127
+ expect(parsedResponse.error).toBeTruthy();
128
+ expect(parsedResponse.message).toContain("body/gotoOptions");
129
+ });
130
+
131
+ it("returns error if page could not load", async () => {
132
+ gotoMethod.mockResolvedValue({
133
+ ok() {
134
+ return false;
135
+ },
136
+
137
+ status() {
138
+ return 404;
139
+ },
140
+
141
+ statusText() {
142
+ return "Not found";
143
+ },
144
+
145
+ async text() {
146
+ return "We tried very hard, but could not find the page.";
147
+ },
148
+ });
149
+
150
+ const response = await app.inject({
151
+ method: "POST",
152
+ url: "/pdf",
153
+ payload: {
154
+ url: "/foobar",
155
+ },
156
+ });
157
+
158
+ expect(response.statusCode).toEqual(404);
159
+ const error = JSON.parse(response.body);
160
+
161
+ expect(error).toMatchObject({
162
+ error: true,
163
+ status: 404,
164
+ statusText: "Not found",
165
+ message: "We tried very hard, but could not find the page.",
166
+ });
167
+ });
168
+
169
+ it("allows to pass options to the pdf call", async () => {
170
+ await app.inject({
171
+ method: "POST",
172
+ url: "/pdf",
173
+ payload: {
174
+ url: "/foobar",
175
+ pdfOptions: {
176
+ headerTemplate: "moin",
177
+ },
178
+ },
179
+ });
180
+
181
+ expect(pdf).toHaveBeenCalledWith({ headerTemplate: "moin" });
182
+ });
183
+
184
+ it("returns 500 if something broke", async () => {
185
+ pdf.mockRejectedValue(new Error("OH NO"));
186
+
187
+ const response = await app.inject({
188
+ method: "POST",
189
+ url: "/pdf",
190
+ payload: {
191
+ url: "/foobar",
192
+ },
193
+ });
194
+
195
+ expect(response.statusCode).toEqual(500);
196
+ expect(JSON.parse(response.body)).toMatchObject({
197
+ statusCode: 500,
198
+ error: "Internal Server Error",
199
+ message: "OH NO",
200
+ });
201
+ });
202
+
203
+ it("allows null as empty for options", async () => {
204
+ await app.inject({
205
+ method: "POST",
206
+ url: "/pdf",
207
+ payload: { url: "/foobar", gotoOptions: null },
208
+ });
209
+
210
+ expect(gotoMethod).toHaveBeenCalledWith("/foobar", defaultGotoOptions);
211
+ });
212
+
213
+ it("closes the browser", async () => {
214
+ pdf.mockRejectedValue("OH NO");
215
+
216
+ await app.inject({
217
+ method: "POST",
218
+ url: "/pdf",
219
+ payload: {
220
+ url: "/foobar",
221
+ },
222
+ });
223
+
224
+ expect(close).toHaveBeenCalled();
225
+ });
226
+ });
package/tsconfig.json CHANGED
@@ -7,6 +7,8 @@
7
7
  "skipLibCheck": true,
8
8
  "forceConsistentCasingInFileNames": true,
9
9
  "resolveJsonModule": true,
10
+ "esModuleInterop": true,
11
+ "module": "ESNext",
10
12
 
11
13
  "checkJs": true
12
14
  }
package/types/schemas.ts CHANGED
@@ -1,7 +1,10 @@
1
- import { Page } from "playwright-core";
1
+ import { BrowserContextOptions, Page } from "playwright-core";
2
2
 
3
3
  export type PdfOptions = Parameters<Page["pdf"]>[0];
4
+ export type GotoOptions = Parameters<Page["goto"]>[1];
4
5
  export type PdfRequestBody = {
5
6
  url: string;
6
- options?: PdfOptions;
7
+ pdfOptions?: PdfOptions | null;
8
+ gotoOptions?: GotoOptions | null;
9
+ browserContextOptions?: BrowserContextOptions | null;
7
10
  };
package/index.js DELETED
@@ -1,95 +0,0 @@
1
- // @ts-check
2
-
3
- /**
4
- * @typedef {Parameters<import("playwright-core").Page["pdf"]>[0]} PdfOptions
5
- */
6
-
7
- /**
8
- * @typedef {Object} PdfRequestBody
9
- * @property {string} url
10
- * @property {PdfOptions} options
11
- */
12
-
13
- const { fastify } = require("fastify");
14
- const playwright = require("playwright-core");
15
- const PdfRequestBodySchema = require("./pdf_request_body.json");
16
-
17
- const SERVER_PORT = process.env["SERVER_PORT"] ?? 8000;
18
- const SERVER_ADDRESS = process.env["SERVER_ADDRESS"] ?? "localhost";
19
-
20
- const server = fastify({
21
- logger: {
22
- transport:
23
- process.env["NODE_ENV"] !== "production"
24
- ? {
25
- target: "pino-pretty",
26
- options: {
27
- translateTime: "HH:MM:ss Z",
28
- ignore: "pid,hostname",
29
- },
30
- }
31
- : undefined,
32
- },
33
- });
34
-
35
- /**
36
- *
37
- * @param {string} url
38
- * @param {PdfOptions} options
39
- * @returns Buffer
40
- */
41
- const createPDF = async (url, options = {}) => {
42
- const browser = await playwright.chromium.launch();
43
- const context = await browser.newContext({ ignoreHTTPSErrors: true });
44
- const page = await context.newPage();
45
- await page.goto(url, { timeout: 60000 });
46
- await page.waitForLoadState("networkidle");
47
-
48
- const pdf = await page.pdf(options);
49
- await browser.close();
50
-
51
- return pdf;
52
- };
53
-
54
- server.post(
55
- "/pdf",
56
- { schema: { body: PdfRequestBodySchema }, attachValidation: true },
57
- /**
58
- *
59
- * @param {import('fastify').FastifyRequest<{Body: PdfRequestBody}>} request
60
- * @param {import('fastify').FastifyReply} response
61
- */
62
- async (request, response) => {
63
- if (request.validationError) {
64
- response.code(400);
65
-
66
- return {
67
- error: true,
68
- message: request.validationError.message,
69
- };
70
- }
71
-
72
- try {
73
- return createPDF(request.body.url, request.body.options);
74
- } catch (error) {
75
- response.code(500);
76
-
77
- return {
78
- error,
79
- };
80
- }
81
- }
82
- );
83
-
84
- server.get("/health", async (_, response) => response.code(200).send());
85
-
86
- const startServer = async () => {
87
- try {
88
- await server.listen({ port: +SERVER_PORT, host: SERVER_ADDRESS });
89
- } catch (error) {
90
- console.error(error);
91
- process.exit(1);
92
- }
93
- };
94
-
95
- startServer();