├── .github
└── workflows
│ └── node.js.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── Makefile
├── README.md
├── package-lock.json
├── package.json
├── prerendercloud.d.ts
├── source
├── index.js
└── lib
│ ├── api-options.json
│ ├── got-retries.js
│ ├── options.js
│ ├── our-url.js
│ └── util.js
├── spec
├── botsOnlySpec.js
├── helpers
│ └── helper.js
├── indexSpec.js
├── screenshotsAndPdfsSpec.js
├── support
│ └── jasmine.json
├── throttleOnTimeoutSpec.js
├── urlSpec.js
└── whitelistQueryParamsSpec.js
├── test
├── connect.js
├── express.js
├── package.json
├── pdf.js
├── scrape.js
├── screenshot.js
└── yarn.lock
└── tsconfig.json
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | name: Node.js test and npm publish
2 | on:
3 | pull_request:
4 | branches:
5 | - "**"
6 | push:
7 | branches:
8 | - "master"
9 |
10 | jobs:
11 | test:
12 | name: make test
13 | runs-on: ubuntu-latest
14 | strategy:
15 | matrix:
16 | node-version: [12.x, 14.x, 16.x, 18.x, 20.x]
17 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
18 | steps:
19 | - uses: actions/checkout@v3
20 | - uses: actions/setup-node@v3
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 | # cache: "npm"
24 | # cache-dependency-path: "package-lock.json"
25 | - uses: actions/cache@v3
26 | with:
27 | path: "**/node_modules"
28 | key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
29 | - run: npm install
30 | - run: make build
31 | - run: make test
32 | env:
33 | CI: true
34 |
35 | build:
36 | # if: ${{ github.ref == 'refs/heads/master' }}
37 | name: npm publish
38 | needs: test
39 | runs-on: ubuntu-latest
40 | steps:
41 | - name: Checkout
42 | uses: actions/checkout@v3
43 | - run: make build
44 | - uses: actions/setup-node@v3
45 | with:
46 | node-version: 20.x
47 | registry-url: "https://registry.npmjs.org"
48 | - name: Npm Publish
49 | if: ${{ github.ref == 'refs/heads/master' }}
50 | run: npm publish ./publish
51 | env:
52 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | distribution
3 | publish
4 | test/out
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | source
2 | test
3 | spec
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 sanfrancesco
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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build publish test
2 |
3 | prettier:
4 | ./node_modules/.bin/prettier --write "source/**/*.js"
5 | ./node_modules/.bin/prettier --write "spec/**/*.js"
6 |
7 | build:
8 | npm run build
9 | rm -rf publish
10 | mkdir publish
11 | cp -r distribution publish/
12 | cp README.md package.json package-lock.json prerendercloud.d.ts publish/
13 |
14 | # following https://booker.codes/how-to-build-and-publish-es6-npm-modules-today-with-babel/ for transpiled npm packages
15 | publish: build
16 | npm publish ./publish
17 |
18 | test:
19 | NODE_ENV=test \
20 | PRERENDER_SERVICE_URL="https://service.prerender.cloud" \
21 | ./node_modules/jasmine/bin/jasmine.js
22 |
23 | devtest:
24 | @for script in screenshot pdf scrape; do \
25 | NODE_ENV=test PRERENDER_SERVICE_URL=http://localhost:3001 node ./test/$$script.js; \
26 | done
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # prerendercloud-nodejs
2 |
3 |
4 |
5 | 
6 |
7 | This package is the Node.js client for [Headless-Render-API.com](https://headless-render-api.com) (formerly named prerender.cloud from 2016 - 2022)
8 |
9 | Use it for **pre-rendering** (server-side rendering), or taking **screenshots** of webpages or converting webpages to **PDFs**.
10 |
11 | ```bash
12 | npm install prerendercloud-server --save
13 | ```
14 |
15 | ```javascript
16 | // simplest possible example usage of this lib
17 | const prerendercloud = require("prerendercloud");
18 |
19 | // if you are pre-rendering a JavaScript single-page app
20 | // served from express (or middleware compatible http server),
21 | // use as middleware in your existing server - or try our all-in-one
22 | // server https://github.com/sanfrancesco/prerendercloud-server
23 | app.use(prerendercloud);
24 |
25 | // or take a screenshot of a URL
26 | const fs = require("fs");
27 | prerendercloud
28 | .screenshot("http://example.com")
29 | .then((pngBuffer) =>
30 | fs.writeFileSync("out.png", pngBuffer, { encoding: null })
31 | );
32 |
33 | // or create a PDF from a URL
34 | prerendercloud
35 | .pdf("http://example.com")
36 | .then((pdfBuffer) =>
37 | fs.writeFileSync("out.pdf", pdfBuffer, { encoding: null })
38 | );
39 | ```
40 |
41 | The pre-render/server-side rendering functionality of this package (as opposed to mere screenshots/pdfs) is meant to be included in an existing web server where 404s are rendered as index.html
42 |
43 | - For an all-in-one single-page app web server plus server-side rendering see: https://github.com/sanfrancesco/prerendercloud-server
44 |
45 | ---
46 |
47 |
48 |
49 | - [Install](#install)
50 | - [npm](#npm)
51 | - [Auth \(API Token\)](#auth-api-token)
52 | - [Environment variable \(best practice\)](#environment-variable-best-practice)
53 | - [Hard coded](#hard-coded)
54 | - [Debugging](#debugging)
55 | - [Screenshots](#screenshots)
56 | - [PDFs](#pdfs)
57 | - [Scrape](#scrape)
58 | - [Prerendering or Server-side rendering with Express/Connect/Node http](#prerendering-or-server-side-rendering-with-expressconnectnode-http)
59 | - [Configure a condition for when traffic should go through Headless-Render-API.com](#configure-a-condition-for-when-traffic-should-go-through-headless-render-apicom)
60 | - [Enable for bots **ONLY** \(google, facebook, twitter, slack etc...\)](#enable-for-bots-only-google-facebook-twitter-slack-etc)
61 | - [Whitelist your own user-agent list \(overrides `botsOnly`\) \(case sensitive\)](#whitelist-your-own-user-agent-list-overrides-botsonly-case-sensitive)
62 | - [beforeRender \(short circuit the remote call to service.headless-render-api.com\)](#beforerender-short-circuit-the-remote-call-to-serviceheadless-render-apicom)
63 | - [blacklistPaths](#blacklistpaths)
64 | - [whitelistPaths](#whitelistpaths)
65 | - [shouldPrerender](#shouldprerender)
66 | - [shouldPrerenderAdditionalCheck](#shouldPrerenderAdditionalCheck)
67 | - [Caching](#caching)
68 | - [Disable Headless-Render-API.com server cache](#disable-headless-render-apicom-server-cache)
69 | - [Using the \(optional\) middleware cache](#using-the-optional-middleware-cache)
70 | - [Clearing the middleware cache](#clearing-the-middleware-cache)
71 | - [Server Options](#server-options)
72 | - [disableServerCache](#disableservercache)
73 | - [serverCacheDurationSeconds](#servercachedurationseconds)
74 | - [metaOnly](#metaonly)
75 | - [followRedirects](#followredirects)
76 | - [disableAjaxBypass](#disableajaxbypass)
77 | - [disableAjaxPreload](#disableajaxpreload)
78 | - [disableHeadDedupe](#disableheaddedupe)
79 | - [originHeaderWhitelist](#originheaderwhitelist)
80 | - [removeScriptTags](#removescripttags)
81 | - [removeTrailingSlash](#removetrailingslash)
82 | - [waitExtraLong](#waitextralong)
83 | - [withMetadata](#withmetadata)
84 | - [withScreenshot](#withscreenshot)
85 | - [DeviceWidth](#devicewidth)
86 | - [DeviceHeight](#deviceheight)
87 | - [Middleware Options](#middleware-options)
88 | - [host](#host)
89 | - [protocol](#protocol)
90 | - [whitelistQueryParams](#whitelistqueryparams)
91 | - [afterRenderBlocking \(executes before `afterRender`\)](#afterrenderblocking-executes-before-afterrender)
92 | - [afterRender \(a noop\) \(caching, analytics\) \(executes after `afterRenderBlocking`\)](#afterrender-a-noop-caching-analytics-executes-after-afterrenderblocking)
93 | - [bubbleUp5xxErrors](#bubbleup5xxerrors)
94 | - [retries](#retries)
95 | - [throttleOnFail](#throttleonfail)
96 | - [How errors from the server \(service.headless-render-api.com\) are handled](#how-errors-from-the-server-serviceheadless-render-apicom-are-handled)
97 |
98 |
99 |
100 |
101 |
102 |
103 | ## Install
104 |
105 |
106 |
107 |
108 | ### npm
109 |
110 | ```bash
111 | npm install prerendercloud --save
112 | ```
113 |
114 |
115 |
116 |
117 | ### Auth (API Token)
118 |
119 | Get a token after signing up at https://headless-render-api.com - it's necessary to move off of the rate-limited free tier
120 |
121 |
122 |
123 |
124 | #### Environment variable (best practice)
125 |
126 | ```javascript
127 | PRERENDER_TOKEN=mySecretToken node index.js
128 | ```
129 |
130 |
131 |
132 |
133 | #### Hard coded
134 |
135 | ```javascript
136 | const prerendercloud = require("prerendercloud");
137 | prerendercloud.set("prerenderToken", "mySecretToken");
138 | ```
139 |
140 |
141 |
142 |
143 | ### Debugging
144 |
145 | ```javascript
146 | DEBUG=prerendercloud node index.js
147 | ```
148 |
149 |
150 |
151 |
152 | ## Screenshots
153 |
154 | Capture a screenshot:
155 |
156 | ```javascript
157 | const prerendercloud = require("prerendercloud");
158 | prerendercloud
159 | .screenshot("http://example.com")
160 | .then((pngBuffer) =>
161 | fs.writeFileSync("out.png", pngBuffer, { encoding: null })
162 | );
163 | ```
164 |
165 | Capture specific element with padding:
166 |
167 | Note: viewportQuerySelector causes viewportWidth/viewportHeight to be ignored
168 |
169 | ```javascript
170 | prerendercloud
171 | .screenshot("http://example.com", {
172 | viewportQuerySelector: "#open-graph-div",
173 | viewportQuerySelectorPadding: 10,
174 | })
175 | .then((pngBuffer) =>
176 | fs.writeFileSync("out.png", pngBuffer, { encoding: null })
177 | );
178 | ```
179 |
180 | Customize dimensions and format:
181 |
182 | ```javascript
183 | prerendercloud
184 | .screenshot("http://example.com", {
185 | deviceWidth: 800,
186 | deviceHeight: 600,
187 | viewportWidth: 640,
188 | viewportHeight: 480,
189 | viewportX: 0,
190 | viewportY: 0,
191 | format: "jpeg", // png, webp, jpeg
192 | })
193 | .then((jpgBuffer) =>
194 | fs.writeFileSync("out.jpg", jpgBuffer, { encoding: null })
195 | );
196 | ```
197 |
198 | Set emulated media and viewport scale:
199 |
200 | ```javascript
201 | prerendercloud
202 | .screenshot("http://example.com", {
203 | // (screen, print, braille, embossed, handheld, projection, speech, tty, tv)
204 | emulatedMedia: "print",
205 | viewportScale: 2,
206 | })
207 | .then((pngBuffer) =>
208 | fs.writeFileSync("out.png", pngBuffer, { encoding: null })
209 | );
210 | ```
211 |
212 |
213 |
214 |
215 | ## PDFs
216 |
217 | Generate a PDF:
218 |
219 | ```javascript
220 | const prerendercloud = require("prerendercloud");
221 | prerendercloud
222 | .pdf("http://example.com")
223 | .then((pdfBuffer) =>
224 | fs.writeFileSync("out.pdf", pdfBuffer, { encoding: null })
225 | );
226 | ```
227 |
228 | Disable page breaks and set emulated media:
229 |
230 | ```javascript
231 | prerendercloud
232 | .pdf("http://example.com", {
233 | // Note: using noPageBreaks forces the following
234 | // - pageRanges: "1",
235 | // - preferCssPageSize: true
236 | noPageBreaks: true,
237 | printBackground: true,
238 |
239 | // emulatedMedia options: (screen, print, braille, embossed, handheld, projection, speech, tty, tv)
240 | emulatedMedia: "screen",
241 | })
242 | .then((pdfBuffer) =>
243 | fs.writeFileSync("out.pdf", pdfBuffer, { encoding: null })
244 | );
245 | ```
246 |
247 | Configure PDF options:
248 |
249 | ```javascript
250 | const prerendercloud = require("prerendercloud");
251 | prerendercloud
252 | .pdf("http://example.com", {
253 | pageRanges: "1-3",
254 | scale: 1.5,
255 | preferCssPageSize: true,
256 | printBackground: true,
257 | landscape: true,
258 | marginTop: 1.75,
259 | marginRight: 0.5,
260 | marginBottom: 1.75,
261 | marginLeft: 0.5,
262 | paperWidth: 8.5,
263 | paperHeight: 11,
264 | })
265 | .then((pdfBuffer) =>
266 | fs.writeFileSync("out.pdf", pdfBuffer, { encoding: null })
267 | );
268 | ```
269 |
270 |
271 |
272 |
273 | ## Scrape
274 |
275 | Scrape a webpage/URL. Optimized for scraping, this endpoint is based off our pre-rendering engine so it can correctly handle JavaScript apps.
276 |
277 | Scrape just the HTML:
278 |
279 | ```javascript
280 | const { body } = await prerendercloud.scrape("https://example.com");
281 | console.log(body);
282 | fs.writeFileSync("body.html", body);
283 | ```
284 |
285 | Or scrape HTML and take a screenshot, and parse important meta tags (title, h1, open graph, etc.):
286 |
287 | ```javascript
288 | const {
289 | body, // Buffer
290 | meta: {
291 | title,
292 | h1,
293 | description,
294 | ogImage,
295 | ogTitle,
296 | ogType,
297 | ogDescription,
298 | twitterCard,
299 | },
300 | links, // Array
301 | screenshot, // Buffer
302 | statusCode, // number
303 | headers, // object of headers
304 | } = await prerendercloud.scrape("https://example.com", {
305 | withMetadata: true,
306 | withScreenshot: true,
307 | followRedirects: false,
308 | });
309 |
310 | if (statusCode === 301 || statusCode === 302) {
311 | // get the redirect location from headers.location
312 | // if instead you'd rather follow redirects, set followRedirects: true
313 | } else {
314 | console.log(body.toString());
315 | console.log({
316 | meta: {
317 | title,
318 | h1,
319 | description,
320 | ogImage,
321 | ogTitle,
322 | ogDescription,
323 | twitterCard,
324 | },
325 | links,
326 | });
327 | fs.writeFileSync("body.html", body);
328 | fs.writeFileSync("screenshot.png", screenshot);
329 |
330 | // links is an array of all the links on the page
331 | // meta is an object that looks like: { title, h1, description, ogImage, ogTitle, ogType, ogDescription, twitterCard }
332 | // screenshot and body are Buffers (so they can be saved to file)
333 | // call body.toString() for stringified HTML
334 | }
335 | ```
336 |
337 |
338 |
339 |
340 | ## Prerendering or Server-side rendering with Express/Connect/Node http
341 |
342 | The `prerendercloud` middleware should be loaded first, before your other middleware, so it can forward the request to service.headless-render-api.com.
343 |
344 | ```javascript
345 | // the free, rate limited tier
346 | // and using https://expressjs.com/
347 | const prerendercloud = require("prerendercloud");
348 | expressApp.use(prerendercloud);
349 | ```
350 |
351 |
352 |
353 |
354 | ### Configure a condition for when traffic should go through Headless-Render-API.com
355 |
356 | The default behavior forwards all traffic through Headless-Render-API.com
357 |
358 |
359 |
360 |
361 | #### Enable for bots **ONLY** (google, facebook, twitter, slack etc...)
362 |
363 | We don't recommend this setting, instead use the **default** setting of pre-rendering all user-agents (because of performance boost and potential google cloaking penalties) but there may be a situation where you shouldn't or can't, for example: your site/app has JavaScript errors when trying to repaint the DOM after it's already been pre-rendered but you still want bots (twitter, slack, facebook etc...) to read the meta and open graph tags.
364 |
365 | **Note**: this will add or append 'User-Agent' to the [**Vary** header](https://varvy.com/mobile/vary-user-agent.html), which is another reason not to recommend this feature (because it significantly reduces HTTP cacheability)
366 |
367 | ```javascript
368 | const prerendercloud = require("prerendercloud");
369 | prerendercloud.set("botsOnly", true);
370 | ```
371 |
372 | You can also append your own agents to our botsOnly list by using an array:
373 |
374 | ```javascript
375 | const prerendercloud = require("prerendercloud");
376 | prerendercloud.set("botsOnly", ["altavista", "dogpile", "excite", "askjeeves"]);
377 | ```
378 |
379 |
380 |
381 |
382 | #### Whitelist your own user-agent list (overrides `botsOnly`) (case sensitive)
383 |
384 | **Note**: this will **NOT** add or append 'User-Agent' to the [**Vary** header](https://varvy.com/mobile/vary-user-agent.html). You should probably set the Vary header yourself, if using this feature.
385 |
386 | ```javascript
387 | const prerendercloud = require("prerendercloud");
388 | prerendercloud.set("whitelistUserAgents", [
389 | "twitterbot",
390 | "slackbot",
391 | "facebookexternalhit",
392 | ]);
393 | ```
394 |
395 |
396 |
397 |
398 | #### beforeRender (short circuit the remote call to service.headless-render-api.com)
399 |
400 | Useful for your own caching layer (in conjunction with `afterRender`), or analytics, or dependency injection for testing. Is only called when a remote call to service.headless-render-api.com is about to be made.
401 |
402 | ```javascript
403 | const prerendercloud = require("prerendercloud");
404 | prerendercloud.set("beforeRender", (req, done) => {
405 | // call it with a string to short-circuit the remote prerender codepath
406 | // (useful when implementing your own cache)
407 | done(null, "hello world"); // returns status 200, content-type text/html
408 |
409 | // or call it with an object to short-circuit the remote prerender codepath
410 | // (useful when implementing your own cache)
411 | done(null, { status: 202, body: "hello" }); // returns status 202, content-type text/html
412 |
413 | done(null, { status: 301, headers: { location: "/new-path" } }); // redirect to /new-path
414 |
415 | // or call it with nothing/empty/null/undefined to follow the remote prerender path
416 | // (useful for analytics)
417 | done();
418 | done("");
419 | done(null);
420 | done(undefined);
421 | });
422 | ```
423 |
424 |
425 |
426 | #### blacklistPaths
427 |
428 | Prevent paths from being prerendered. Takes a function that returns an array. It is executed before the shouldPrerender option.
429 |
430 | The primary use case is for CDN edge node clients (CloudFront Lambda@Edge) because they don't have the ability to quickly read the origin (AWS S3) filesystem, so they have to hard-code paths that shouldn't be prerendered.
431 |
432 | Paths you may not want prerendered are non-SPA, large pages, or pages with JavaScript that can't rehydrate prerendered DOMs.
433 |
434 | Trailing `*` works as wildcard. Only works when at the end.
435 |
436 | ```javascript
437 | const prerendercloud = require("prerendercloud");
438 | prerendercloud.set("blacklistPaths", (req) => [
439 | "/google-domain-verification",
440 | "/google-domain-verification.html",
441 | "/google-domain-verification/",
442 | "/image-gallery/*",
443 | ]);
444 | ```
445 |
446 |
447 |
448 | #### whitelistPaths
449 |
450 | Limit which URLs can trigger a pre-render request to the server.
451 |
452 | Takes a function that returns an **array** of **strings** or **regexes**. It is executed before the shouldPrerender option. Passing an empty array or string will do nothing (noop).
453 |
454 | Using this option will prevent bots/scrapers from hitting random URLs and increasing your billing. Recommended for Node.js server and Lambda@Edge (can be used with our without blacklist - blacklist takes precedent).
455 |
456 | Even better if used with `whitelistQueryParams` and/or `removeTrailingSlash`.
457 |
458 | ```javascript
459 | const prerendercloud = require("prerendercloud");
460 | prerendercloud.set("whitelistPaths", req => [
461 | "/docs",
462 | "/docs/"
463 | /\/users\/\d{1,6}\/profile$/, // without the ending $, this is equivalent to startsWith
464 | /\/users\/\d{1,6}\/profile\/?$/, // note the optional ending slash (\/?) and $
465 | "/google-domain-verification.html",
466 | "/google-domain-verification/",
467 | ]);
468 |
469 | ```
470 |
471 |
472 |
473 |
474 | #### shouldPrerender
475 |
476 | This is executed after the `beforeRender` but if present, replaces userAgent detection (it would override `botsOnly`).
477 |
478 | ```javascript
479 | const prerendercloud = require("prerendercloud");
480 | prerendercloud.set("shouldPrerender", (req) => {
481 | return req.headers["user-agent"] === "googlebot" && someStateOnMyServer();
482 | // return bool
483 | });
484 | ```
485 |
486 |
487 |
488 |
489 | #### shouldPrerenderAdditionalCheck
490 |
491 | Runs **in addition** to the default user-agent check. Useful if you have your own conditions.
492 |
493 | ```javascript
494 | // time delay
495 | const waitUntil = new Date() + 10000;
496 | prerendercloud.set("shouldPrerenderAdditionalCheck", (req) => {
497 | return new Date() > waitUntil;
498 | });
499 |
500 | // enable flag
501 | let isEnabled = false;
502 | prerendercloud.set("shouldPrerenderAdditionalCheck", (req) => {
503 | return isEnabled;
504 | });
505 | ```
506 |
507 |
508 |
509 |
510 | ### Caching
511 |
512 |
513 |
514 |
515 | #### Disable Headless-Render-API.com server cache
516 |
517 | The servers behind service.headless-render-api.com will cache for 5 minutes as a best practice. Adding the `Prerender-Disable-Cache` HTTP header via this config option disables that cache entirely. Disabling the service.headless-render-api.com cache is only recommended if you have your own cache either in this middleware or your client, otherwise all of your requests are going to be slow.
518 |
519 | ```javascript
520 | const prerendercloud = require("prerendercloud");
521 | prerendercloud.set("disableServerCache", true);
522 | app.use(prerendercloud);
523 | ```
524 |
525 |
526 |
527 |
528 | #### Using the (optional) middleware cache
529 |
530 | This middleware has a built-in LRU (drops least recently used) caching layer. It can be configured to let cache auto expire or you can manually remove entire domains from the cache. You proboably want to use this if you disabled the server cache.
531 |
532 | ```javascript
533 | const prerendercloud = require("prerendercloud");
534 | prerendercloud.set("enableMiddlewareCache", true);
535 |
536 | // optionally set max bytes (defaults to 500MB)
537 | prerendercloud.set("middlewareCacheMaxBytes", 1000000000); // 1GB
538 |
539 | // optionally set max age (defaults to forever - implying you should manually clear it)
540 | prerendercloud.set("middlewareCacheMaxAge", 1000 * 60 * 60); // 1 hour
541 |
542 | app.use(prerendercloud);
543 | ```
544 |
545 |
546 |
547 |
548 | ##### Clearing the middleware cache
549 |
550 | ```javascript
551 | // delete every page on the example.org domain
552 | prerendercloud.cache.clear("http://example.org");
553 |
554 | // delete every page on every domain
555 | prerendercloud.cache.reset();
556 | ```
557 |
558 |
559 |
560 |
561 | ### Server Options
562 |
563 | These options map to the HTTP header options listed here: https://headless-render-api.com/docs/api
564 |
565 |
566 |
567 | #### disableServerCache
568 |
569 | This option disables an enabled-by-default 5-minute cache.
570 |
571 | The servers behind service.headless-render-api.com will cache for 5 minutes as a best practice. Adding the `Prerender-Disable-Cache` HTTP header via this config option disables that cache entirely. Disabling the service.headless-render-api.com cache is only recommended if you have your own cache either in this middleware or your client, otherwise all of your requests are going to be slow.
572 |
573 | ```javascript
574 | const prerendercloud = require("prerendercloud");
575 | prerendercloud.set("disableServerCache", true);
576 | app.use(prerendercloud);
577 | ```
578 |
579 |
580 |
581 | #### serverCacheDurationSeconds
582 |
583 | This option configures the duration for Headless-Render-API.com's server cache:
584 |
585 | The servers behind service.headless-render-api.com will cache for 5 minutes as a best practice, configure that duration (in seconds):
586 |
587 | ```javascript
588 | const prerendercloud = require("prerendercloud");
589 | // max value: 2592000 (1 month)
590 | prerendercloud.set("serverCacheDurationSeconds", (req) => 300);
591 | app.use(prerendercloud);
592 | ```
593 |
594 |
595 |
596 | #### metaOnly
597 |
598 | This option tells the server to only prerender the `
` and `` tags in the `` section. The returned HTML payload will otherwise be unmodified.
599 |
600 | Example use case 1: your single-page app does not rehydrate the body/div cleanly but you still want open graph (link previews) to work.
601 |
602 | Example use case 2: you don't care about the benefits of server-side rendering but still want open graph (link previews) to work.
603 |
604 | ```javascript
605 | const prerendercloud = require("prerendercloud");
606 | prerendercloud.set("metaOnly", (req) =>
607 | req.url === "/long-page-insuitable-for-full-prerender" ? true : false
608 | );
609 | app.use(prerendercloud);
610 | ```
611 |
612 |
613 |
614 | #### followRedirects
615 |
616 | This option tells the server to follow a redirect.
617 |
618 | By default, if your origin server returns 301/302, Headless-Render-API.com will just return that outright - which is appropriate for the common use case of proxying traffic since it informs a bot that a URL has changed.
619 |
620 | ```javascript
621 | const prerendercloud = require("prerendercloud");
622 | prerendercloud.set("followRedirects", (req) => true);
623 | app.use(prerendercloud);
624 | ```
625 |
626 |
627 |
628 |
629 | #### disableAjaxBypass
630 |
631 | You can disable this if you're using CORS. Read more https://headless-render-api.com/docs and https://github.com/sanfrancesco/prerendercloud-ajaxmonkeypatch
632 |
633 | ```javascript
634 | const prerendercloud = require("prerendercloud");
635 | prerendercloud.set("disableAjaxBypass", true);
636 | app.use(prerendercloud);
637 | ```
638 |
639 |
640 |
641 |
642 | #### disableAjaxPreload
643 |
644 | This prevents screen flicker/repaint/flashing, but increases initial page load size (because it embeds the AJAX responses into your HTML). you can disable this if you manage your own "initial state". Read more https://headless-render-api.com/docs and https://github.com/sanfrancesco/prerendercloud-ajaxmonkeypatch
645 |
646 | ```javascript
647 | const prerendercloud = require("prerendercloud");
648 | prerendercloud.set("disableAjaxPreload", true);
649 | app.use(prerendercloud);
650 | ```
651 |
652 |
653 |
654 |
655 | #### disableHeadDedupe
656 |
657 | Removes a JavaScript monkeypatch from the prerendered page that is intended to prevent duplicate meta/title/script/style tags. Some libs/frameworks detect existing meta/title/style and don't need this, but in our experience this is still a worthwhile default. Read more https://github.com/sanfrancesco/prerendercloud-ajaxmonkeypatch#head-dedupe
658 |
659 | ```javascript
660 | const prerendercloud = require("prerendercloud");
661 | prerendercloud.set("disableHeadDedupe", true);
662 | app.use(prerendercloud);
663 | ```
664 |
665 |
666 |
667 |
668 | #### originHeaderWhitelist
669 |
670 | The only valid values (_right now_) are: `['Prerendercloud-Is-Mobile-Viewer']`, and anything starting with `prerendercloud-`. This feature is meant for forwarding headers from the original request to your site through to your origin (by default, all headers are dropped).
671 |
672 | ```javascript
673 | prerendercloud.set("originHeaderWhitelist", [
674 | "Prerendercloud-Is-Mobile-Viewer",
675 | ]);
676 | ```
677 |
678 |
679 |
680 |
681 | #### removeScriptTags
682 |
683 | This removes all script tags except for [application/ld+json](https://stackoverflow.com/questions/38670851/whats-a-script-type-application-ldjsonjsonobj-script-in-a-head-sec). Removing script tags prevents any JS from executing at all - so your app will no longer be isomorphic. Useful when Headless-Render-API.com is used as a scraper/crawler or in constrained environments (Lambda @ Edge).
684 |
685 | ```javascript
686 | const prerendercloud = require("prerendercloud");
687 | prerendercloud.set("removeScriptTags", true);
688 | ```
689 |
690 |
691 |
692 |
693 | #### removeTrailingSlash
694 |
695 | This is the opposite of what is often referred to "strict mode routing". When this is enabled, the server will normalize the URLs by removing a trailing slash.
696 |
697 | e.g.: example.com/docs/ -> example.com/docs
698 |
699 | The use case for this option is to achieve higher cache hit rate (so if a user/bots are hitting `/docs/` and `/docs`, they'll both be cached on Headless-Render-API.com servers as the same entity).
700 |
701 | SEO best practices:
702 |
703 | 1. 301 redirect trailing slash URLs to non trailing slash before this middleware is called (and then don't bother removingTrailingSlash because it should never happen)
704 | 2. or use [link rel canonical](https://en.wikipedia.org/wiki/Canonical_link_element) in conjunction with this
705 |
706 | ```javascript
707 | const prerendercloud = require("prerendercloud");
708 | prerendercloud.set("removeTrailingSlash", true);
709 | ```
710 |
711 |
712 |
713 |
714 | #### waitExtraLong
715 |
716 | Headless-Render-API.com will wait for all in-flight XHR/websockets requests to finish before rendering, but when critical XHR/websockets requests are sent after the page load event, Headless-Render-API.com may not wait long enough to see that it needs to wait for them. Common example use cases are sites hosted on IPFS, or sites that make an initial XHR request that returns endpoints that require additional XHR requests.
717 |
718 | ```javascript
719 | const prerendercloud = require("prerendercloud");
720 | prerendercloud.set("waitExtraLong", true);
721 | ```
722 |
723 |
724 |
725 | #### withMetadata
726 |
727 | When a function is passed that returns true, Headless-Render-API.com will return both the prerendered HTML, meta, and links
728 |
729 | ```javascript
730 | const prerendercloud = require("prerendercloud");
731 |
732 | prerendercloud.set("withMetadata", (req) => true);
733 | ```
734 |
735 | To make use of the meta and links, call `res.meta` or `res.links` from either `afterRender` or `afterRenderBlock`
736 |
737 |
738 |
739 | #### withScreenshot
740 |
741 | When a function is passed that returns true, Headless-Render-API.com will return both the prerendered HTML and a JPEG screenshot.
742 |
743 | ```javascript
744 | const prerendercloud = require("prerendercloud");
745 |
746 | prerendercloud.set("withScreenshot", (req) => true);
747 | ```
748 |
749 | To make use of the screenshot, call `res.screenshot` from either `afterRender` or `afterRenderBlock`
750 |
751 |
752 |
753 | ### DeviceWidth
754 |
755 | Self explanatory
756 |
757 | ```javascript
758 | const prerendercloud = require("prerendercloud");
759 |
760 | prerendercloud.set("deviceWidth", (req) =>
761 | req.url.match(/shareable\-cards/) ? 800 : null
762 | );
763 | ```
764 |
765 |
766 |
767 | ### DeviceHeight
768 |
769 | Self explanatory
770 |
771 | ```javascript
772 | const prerendercloud = require("prerendercloud");
773 |
774 | prerendercloud.set("deviceHeight", (req) =>
775 | req.url.match(/shareable\-cards/) ? 600 : null
776 | );
777 | ```
778 |
779 |
780 |
781 |
782 | ### Middleware Options
783 |
784 |
785 |
786 |
787 | #### host
788 |
789 | Force the middleware to hit your origin with a certain host. This is useful for environments like Lambda@Edge+CloudFront where you can't infer the actual host.
790 |
791 | ```javascript
792 | const prerendercloud = require("prerendercloud");
793 | prerendercloud.set("host", "example.com");
794 | ```
795 |
796 |
797 |
798 |
799 | #### protocol
800 |
801 | Force the middleware to hit your origin with a certain protocol (usually `https`). This is useful when you're using CloudFlare or any other https proxy that hits your origin at http but you also have a redirect to https.
802 |
803 | ```javascript
804 | const prerendercloud = require("prerendercloud");
805 | prerendercloud.set("protocol", "https");
806 | ```
807 |
808 |
809 |
810 |
811 | #### whitelistQueryParams
812 |
813 | Whitelist query string parameters on each request.
814 |
815 | The use case for this option is to achieve higher cache hit rate (so if a user/bots are hitting `docs?source=other` or `/docs` or `docs?source=another&foo=bar`, they'll all be cached on Headless-Render-API.com servers as the same entity).
816 |
817 | - `null` (the default), preserve all query params
818 | - `[]` empty whitelist means drop all query params
819 | - `['page', 'x', 'y']` only accept page, x, and y params (drop everything else)
820 |
821 | ```javascript
822 | const prerendercloud = require("prerendercloud");
823 |
824 | // e.g., the default: example.com/docs?source=other&page=2 -> example.com/docs?source=other&page=2
825 | prerendercloud.set("whitelistQueryParams", (req) => null);
826 |
827 | // e.g., if you whitelist only `page` query param: example.com/docs?source=other&page=2 -> example.com/docs?page=2
828 | prerendercloud.set("whitelistQueryParams", (req) =>
829 | req.path.startsWith("/docs") ? ["page"] : []
830 | );
831 |
832 | // e.g., if your whitelist is empty array: example.com/docs?source=other&page=2 -> example.com/docs
833 | prerendercloud.set("whitelistQueryParams", (req) => []);
834 | ```
835 |
836 |
837 |
838 | #### afterRenderBlocking (executes before `afterRender`)
839 |
840 | Same thing as `afterRender`, except it blocks. This is useful for mutating the response headers or body.
841 |
842 | Since it blocks, you have to call the `next` callback when done.
843 |
844 | Example use case: use with the `withMetadata` and/or `withScreenshot` option to save metadata or the screenshot to disk and add it as an open graph tag.
845 |
846 | ```javascript
847 | const prerendercloud = require("prerendercloud");
848 | prerendercloud.set("afterRenderBlocking", (err, req, res, next) => {
849 | // req: (standard node.js req object)
850 | // res: { statusCode, headers, body, screenshot, meta, links }
851 | console.log({ meta: res.meta, links: res.links });
852 | if (res.screenshot) {
853 | fs.writeFileSync("og.jpg", res.screenshot);
854 | res.body = res.body.replace(
855 | /\<\/head\>/,
856 | ""
857 | );
858 | }
859 |
860 | next();
861 | });
862 | ```
863 |
864 |
865 |
866 |
867 | #### afterRender (a noop) (caching, analytics) (executes after `afterRenderBlocking`)
868 |
869 | It's a noop because this middleware already takes over the response for your HTTP server. 2 example use cases of this: your own caching layer, or analytics/metrics.
870 |
871 | ```javascript
872 | const prerendercloud = require("prerendercloud");
873 | prerendercloud.set("afterRender", (err, req, res) => {
874 | // req: (standard node.js req object)
875 | // res: { statusCode, headers, body }
876 | console.log(`received ${res.body.length} bytes for ${req.url}`);
877 | });
878 | ```
879 |
880 |
881 |
882 |
883 | #### bubbleUp5xxErrors
884 |
885 | (note: 400 errors are always bubbled up, 429 rate limit errors are never bubbled up. This section is for 5xx errors which are usually either timeouts or Headless-Render-API.com server issues)
886 |
887 | This must be enabled if you want your webserver to show a 500 when Headless-Render-API.com throws a 5xx (retriable error). As mentioned in the previous section, by default, 5xx errors are ignored and non-prerendered content is returned so the user is uninterrupted.
888 |
889 | Bubbling up the 5xx error is useful if you're using a crawler to trigger prerenders and you want control over retries.
890 |
891 | It can take a bool or a function(err, req, res) that returns a bool. The sync function is executed before writing to `res`, or calling `next` (dependending on what bool is returned). It's useful when:
892 |
893 | - you want to bubble up errors only for certain errors, user-agents, IPs, etc...
894 | - or you want to store the errors (analytics)
895 |
896 | ```javascript
897 | const prerendercloud = require("prerendercloud");
898 | prerendercloud.set("bubbleUp5xxErrors", true);
899 | ```
900 |
901 | ```javascript
902 | const prerendercloud = require("prerendercloud");
903 | prerendercloud.set("bubbleUp5xxErrors", (err, req, res) => {
904 | // err object comes from https://github.com/sindresorhus/got lib
905 |
906 | // examples:
907 | // 1. if (err.statusCode === 503) return true;
908 | // 2. if (req.headers['user-agent'] === 'googlebot') return true;
909 | // 3. if (res.body && res.body.match(/timeout/)) return true;
910 | // 4. myDatabase.query('insert into errors(msg) values($1)', [err.message])
911 | // 5. Raven.captureException(err, { req, resBody: res.body })
912 |
913 | return false;
914 | });
915 | ```
916 |
917 |
918 |
919 |
920 | #### retries
921 |
922 | HTTP errors 500, 503, 504 and [network errors](https://github.com/floatdrop/is-retry-allowed/) are retriable. The default is 1 retry (2 total attempts) but you can change that to 0 or whatever here. There is exponential back-off. When Headless-Render-API.com is over capacity it will return 503 until the autoscaler boots up more capacity so this will address those service interruptions appropriately.
923 |
924 | ```javascript
925 | const prerendercloud = require("prerendercloud");
926 | prerendercloud.set("retries", 4);
927 | ```
928 |
929 |
930 |
931 |
932 | #### throttleOnFail
933 |
934 | If a request fails due to a retryable error (500, 503, 504) - typically a timeout, then this option will prevent pre-rendering that page for 5 minutes.
935 |
936 | It's useful if some of of your pages have an issue causing a timeout, so at least the non-prerendered content will be returned most of the time.
937 |
938 | Use this option with a function for `bubbleUp5xxErrors` so you can record the error in your error tracker so you can eventually fix it.
939 |
940 | Note, if you're using this with `bubbleUp5xxErrors` function that returns true (or a bool value of true), then a 503 error will be bubbled up.
941 |
942 | ```javascript
943 | const prerendercloud = require("prerendercloud");
944 | prerendercloud.set("throttleOnFail", true);
945 | ```
946 |
947 |
948 |
949 |
950 | ### How errors from the server (service.headless-render-api.com) are handled
951 |
952 | - when used as middleware
953 | - when Headless-Render-API.com service returns
954 | - **400 client error (bad request)**
955 | - e.g. try to prerender a localhost URL as opposed to a publicly accessible URL
956 | - the client itself returns the 400 error (the web page will not be accessible)
957 | - **429 client error (rate limited)**
958 | - the original server payload (not prerendered) is returned, so **the request is not interrupted due to unpaid bills or free accounts**
959 | - only happens while on the free tier (paid subscriptions are not rate limited)
960 | - the error message is written to STDERR
961 | - if the env var: DEBUG=prerendercloud is set, the error is also written to STDOUT
962 | - **500, 503, 504** (and [network errors](https://github.com/floatdrop/is-retry-allowed/))
963 | - these will be retried, by default, 1 time
964 | - you can disable retries with `.set('retries', 0)`
965 | - you can increase retries with `.set('retries', 5)` (or whatever)
966 | - 502 is not retried - it means your origin returned 5xx
967 | - **5xx (server error)**
968 | - when even the retries fail, the original server payload (not prerendered) is returned, so **the request is not interrupted due to server error**
969 | - the error message is written to STDERR
970 | - if the env var: DEBUG=prerendercloud is set, the error is also written to STDOUT
971 | - when used for screenshots/pdfs
972 | - retriable errors are retried (500, 503, 504 and [network errors](https://github.com/floatdrop/is-retry-allowed/))
973 | - the errors are returned in the promise catch API
974 | - the errors are from the [`got` library](https://github.com/sindresorhus/got#errors)
975 | - see URL
976 | - `.catch(err => console.log(err.url))`
977 | - see status code
978 | - `.catch(err => console.log(err.response.statusCode))`
979 | - see err response body
980 | - `.catch(err => console.log(err.response.body))`
981 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prerendercloud",
3 | "version": "1.48.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "prerendercloud",
9 | "version": "1.48.0",
10 | "license": "MIT",
11 | "dependencies": {
12 | "debug": "^4.3.4",
13 | "got-lite": "^8.0.4",
14 | "lru-cache": "^7.9.0",
15 | "vary": "^1.1.1"
16 | },
17 | "devDependencies": {
18 | "connect": "^3.7.0",
19 | "express": "^4.18.1",
20 | "jasmine": "^4.1.0",
21 | "nock": "^13.2.4",
22 | "prettier": "^2.6.2",
23 | "typescript": "^5.4.5"
24 | }
25 | },
26 | "node_modules/@sindresorhus/is": {
27 | "version": "0.6.0",
28 | "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.6.0.tgz",
29 | "integrity": "sha1-OD9Faya8lseInwMyB59DWLFsWNw=",
30 | "engines": {
31 | "node": ">=4"
32 | }
33 | },
34 | "node_modules/accepts": {
35 | "version": "1.3.8",
36 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
37 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
38 | "dev": true,
39 | "dependencies": {
40 | "mime-types": "~2.1.34",
41 | "negotiator": "0.6.3"
42 | },
43 | "engines": {
44 | "node": ">= 0.6"
45 | }
46 | },
47 | "node_modules/array-flatten": {
48 | "version": "1.1.1",
49 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
50 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
51 | "dev": true
52 | },
53 | "node_modules/balanced-match": {
54 | "version": "1.0.2",
55 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
56 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
57 | "dev": true
58 | },
59 | "node_modules/body-parser": {
60 | "version": "1.20.0",
61 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
62 | "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==",
63 | "dev": true,
64 | "dependencies": {
65 | "bytes": "3.1.2",
66 | "content-type": "~1.0.4",
67 | "debug": "2.6.9",
68 | "depd": "2.0.0",
69 | "destroy": "1.2.0",
70 | "http-errors": "2.0.0",
71 | "iconv-lite": "0.4.24",
72 | "on-finished": "2.4.1",
73 | "qs": "6.10.3",
74 | "raw-body": "2.5.1",
75 | "type-is": "~1.6.18",
76 | "unpipe": "1.0.0"
77 | },
78 | "engines": {
79 | "node": ">= 0.8",
80 | "npm": "1.2.8000 || >= 1.4.16"
81 | }
82 | },
83 | "node_modules/body-parser/node_modules/debug": {
84 | "version": "2.6.9",
85 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
86 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
87 | "dev": true,
88 | "dependencies": {
89 | "ms": "2.0.0"
90 | }
91 | },
92 | "node_modules/body-parser/node_modules/ms": {
93 | "version": "2.0.0",
94 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
95 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
96 | "dev": true
97 | },
98 | "node_modules/body-parser/node_modules/on-finished": {
99 | "version": "2.4.1",
100 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
101 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
102 | "dev": true,
103 | "dependencies": {
104 | "ee-first": "1.1.1"
105 | },
106 | "engines": {
107 | "node": ">= 0.8"
108 | }
109 | },
110 | "node_modules/brace-expansion": {
111 | "version": "1.1.11",
112 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
113 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
114 | "dev": true,
115 | "dependencies": {
116 | "balanced-match": "^1.0.0",
117 | "concat-map": "0.0.1"
118 | }
119 | },
120 | "node_modules/bytes": {
121 | "version": "3.1.2",
122 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
123 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
124 | "dev": true,
125 | "engines": {
126 | "node": ">= 0.8"
127 | }
128 | },
129 | "node_modules/call-bind": {
130 | "version": "1.0.2",
131 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
132 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
133 | "dev": true,
134 | "dependencies": {
135 | "function-bind": "^1.1.1",
136 | "get-intrinsic": "^1.0.2"
137 | },
138 | "funding": {
139 | "url": "https://github.com/sponsors/ljharb"
140 | }
141 | },
142 | "node_modules/concat-map": {
143 | "version": "0.0.1",
144 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
145 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
146 | "dev": true
147 | },
148 | "node_modules/connect": {
149 | "version": "3.7.0",
150 | "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
151 | "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==",
152 | "dev": true,
153 | "dependencies": {
154 | "debug": "2.6.9",
155 | "finalhandler": "1.1.2",
156 | "parseurl": "~1.3.3",
157 | "utils-merge": "1.0.1"
158 | },
159 | "engines": {
160 | "node": ">= 0.10.0"
161 | }
162 | },
163 | "node_modules/connect/node_modules/debug": {
164 | "version": "2.6.9",
165 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
166 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
167 | "dev": true,
168 | "dependencies": {
169 | "ms": "2.0.0"
170 | }
171 | },
172 | "node_modules/connect/node_modules/ms": {
173 | "version": "2.0.0",
174 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
175 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
176 | "dev": true
177 | },
178 | "node_modules/content-disposition": {
179 | "version": "0.5.4",
180 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
181 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
182 | "dev": true,
183 | "dependencies": {
184 | "safe-buffer": "5.2.1"
185 | },
186 | "engines": {
187 | "node": ">= 0.6"
188 | }
189 | },
190 | "node_modules/content-type": {
191 | "version": "1.0.4",
192 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
193 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
194 | "dev": true,
195 | "engines": {
196 | "node": ">= 0.6"
197 | }
198 | },
199 | "node_modules/cookie": {
200 | "version": "0.5.0",
201 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
202 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
203 | "dev": true,
204 | "engines": {
205 | "node": ">= 0.6"
206 | }
207 | },
208 | "node_modules/cookie-signature": {
209 | "version": "1.0.6",
210 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
211 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
212 | "dev": true
213 | },
214 | "node_modules/core-util-is": {
215 | "version": "1.0.3",
216 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
217 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
218 | },
219 | "node_modules/debug": {
220 | "version": "4.3.4",
221 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
222 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
223 | "dependencies": {
224 | "ms": "2.1.2"
225 | },
226 | "engines": {
227 | "node": ">=6.0"
228 | },
229 | "peerDependenciesMeta": {
230 | "supports-color": {
231 | "optional": true
232 | }
233 | }
234 | },
235 | "node_modules/decompress-response": {
236 | "version": "3.3.0",
237 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
238 | "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
239 | "dependencies": {
240 | "mimic-response": "^1.0.0"
241 | },
242 | "engines": {
243 | "node": ">=4"
244 | }
245 | },
246 | "node_modules/depd": {
247 | "version": "2.0.0",
248 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
249 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
250 | "dev": true,
251 | "engines": {
252 | "node": ">= 0.8"
253 | }
254 | },
255 | "node_modules/destroy": {
256 | "version": "1.2.0",
257 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
258 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
259 | "dev": true,
260 | "engines": {
261 | "node": ">= 0.8",
262 | "npm": "1.2.8000 || >= 1.4.16"
263 | }
264 | },
265 | "node_modules/duplexer3": {
266 | "version": "0.1.4",
267 | "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
268 | "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
269 | },
270 | "node_modules/ee-first": {
271 | "version": "1.1.1",
272 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
273 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
274 | "dev": true
275 | },
276 | "node_modules/encodeurl": {
277 | "version": "1.0.2",
278 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
279 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
280 | "dev": true,
281 | "engines": {
282 | "node": ">= 0.8"
283 | }
284 | },
285 | "node_modules/escape-html": {
286 | "version": "1.0.3",
287 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
288 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
289 | "dev": true
290 | },
291 | "node_modules/etag": {
292 | "version": "1.8.1",
293 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
294 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
295 | "dev": true,
296 | "engines": {
297 | "node": ">= 0.6"
298 | }
299 | },
300 | "node_modules/express": {
301 | "version": "4.18.1",
302 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz",
303 | "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==",
304 | "dev": true,
305 | "dependencies": {
306 | "accepts": "~1.3.8",
307 | "array-flatten": "1.1.1",
308 | "body-parser": "1.20.0",
309 | "content-disposition": "0.5.4",
310 | "content-type": "~1.0.4",
311 | "cookie": "0.5.0",
312 | "cookie-signature": "1.0.6",
313 | "debug": "2.6.9",
314 | "depd": "2.0.0",
315 | "encodeurl": "~1.0.2",
316 | "escape-html": "~1.0.3",
317 | "etag": "~1.8.1",
318 | "finalhandler": "1.2.0",
319 | "fresh": "0.5.2",
320 | "http-errors": "2.0.0",
321 | "merge-descriptors": "1.0.1",
322 | "methods": "~1.1.2",
323 | "on-finished": "2.4.1",
324 | "parseurl": "~1.3.3",
325 | "path-to-regexp": "0.1.7",
326 | "proxy-addr": "~2.0.7",
327 | "qs": "6.10.3",
328 | "range-parser": "~1.2.1",
329 | "safe-buffer": "5.2.1",
330 | "send": "0.18.0",
331 | "serve-static": "1.15.0",
332 | "setprototypeof": "1.2.0",
333 | "statuses": "2.0.1",
334 | "type-is": "~1.6.18",
335 | "utils-merge": "1.0.1",
336 | "vary": "~1.1.2"
337 | },
338 | "engines": {
339 | "node": ">= 0.10.0"
340 | }
341 | },
342 | "node_modules/express/node_modules/debug": {
343 | "version": "2.6.9",
344 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
345 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
346 | "dev": true,
347 | "dependencies": {
348 | "ms": "2.0.0"
349 | }
350 | },
351 | "node_modules/express/node_modules/finalhandler": {
352 | "version": "1.2.0",
353 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
354 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
355 | "dev": true,
356 | "dependencies": {
357 | "debug": "2.6.9",
358 | "encodeurl": "~1.0.2",
359 | "escape-html": "~1.0.3",
360 | "on-finished": "2.4.1",
361 | "parseurl": "~1.3.3",
362 | "statuses": "2.0.1",
363 | "unpipe": "~1.0.0"
364 | },
365 | "engines": {
366 | "node": ">= 0.8"
367 | }
368 | },
369 | "node_modules/express/node_modules/ms": {
370 | "version": "2.0.0",
371 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
372 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
373 | "dev": true
374 | },
375 | "node_modules/express/node_modules/on-finished": {
376 | "version": "2.4.1",
377 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
378 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
379 | "dev": true,
380 | "dependencies": {
381 | "ee-first": "1.1.1"
382 | },
383 | "engines": {
384 | "node": ">= 0.8"
385 | }
386 | },
387 | "node_modules/express/node_modules/statuses": {
388 | "version": "2.0.1",
389 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
390 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
391 | "dev": true,
392 | "engines": {
393 | "node": ">= 0.8"
394 | }
395 | },
396 | "node_modules/finalhandler": {
397 | "version": "1.1.2",
398 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
399 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
400 | "dev": true,
401 | "dependencies": {
402 | "debug": "2.6.9",
403 | "encodeurl": "~1.0.2",
404 | "escape-html": "~1.0.3",
405 | "on-finished": "~2.3.0",
406 | "parseurl": "~1.3.3",
407 | "statuses": "~1.5.0",
408 | "unpipe": "~1.0.0"
409 | },
410 | "engines": {
411 | "node": ">= 0.8"
412 | }
413 | },
414 | "node_modules/finalhandler/node_modules/debug": {
415 | "version": "2.6.9",
416 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
417 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
418 | "dev": true,
419 | "dependencies": {
420 | "ms": "2.0.0"
421 | }
422 | },
423 | "node_modules/finalhandler/node_modules/ms": {
424 | "version": "2.0.0",
425 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
426 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
427 | "dev": true
428 | },
429 | "node_modules/forwarded": {
430 | "version": "0.2.0",
431 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
432 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
433 | "dev": true,
434 | "engines": {
435 | "node": ">= 0.6"
436 | }
437 | },
438 | "node_modules/fresh": {
439 | "version": "0.5.2",
440 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
441 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
442 | "dev": true,
443 | "engines": {
444 | "node": ">= 0.6"
445 | }
446 | },
447 | "node_modules/from2": {
448 | "version": "2.3.0",
449 | "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
450 | "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
451 | "dependencies": {
452 | "inherits": "^2.0.1",
453 | "readable-stream": "^2.0.0"
454 | }
455 | },
456 | "node_modules/fs.realpath": {
457 | "version": "1.0.0",
458 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
459 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
460 | "dev": true
461 | },
462 | "node_modules/function-bind": {
463 | "version": "1.1.1",
464 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
465 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
466 | "dev": true
467 | },
468 | "node_modules/get-intrinsic": {
469 | "version": "1.1.1",
470 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
471 | "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
472 | "dev": true,
473 | "dependencies": {
474 | "function-bind": "^1.1.1",
475 | "has": "^1.0.3",
476 | "has-symbols": "^1.0.1"
477 | },
478 | "funding": {
479 | "url": "https://github.com/sponsors/ljharb"
480 | }
481 | },
482 | "node_modules/get-stream": {
483 | "version": "3.0.0",
484 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
485 | "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
486 | "engines": {
487 | "node": ">=4"
488 | }
489 | },
490 | "node_modules/glob": {
491 | "version": "7.2.0",
492 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
493 | "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
494 | "dev": true,
495 | "dependencies": {
496 | "fs.realpath": "^1.0.0",
497 | "inflight": "^1.0.4",
498 | "inherits": "2",
499 | "minimatch": "^3.0.4",
500 | "once": "^1.3.0",
501 | "path-is-absolute": "^1.0.0"
502 | },
503 | "engines": {
504 | "node": "*"
505 | },
506 | "funding": {
507 | "url": "https://github.com/sponsors/isaacs"
508 | }
509 | },
510 | "node_modules/got-lite": {
511 | "version": "8.0.4",
512 | "resolved": "https://registry.npmjs.org/got-lite/-/got-lite-8.0.4.tgz",
513 | "integrity": "sha512-a1DfXCyXKIfGzkcluPHVz0tBOHUiFIL793WCJhFtoizPn+qokcTqAaa3XFqYiPVg/7Efm/ZcRJcwtF1jaF0AlA==",
514 | "dependencies": {
515 | "@sindresorhus/is": "^0.6.0",
516 | "decompress-response": "^3.3.0",
517 | "duplexer3": "^0.1.4",
518 | "get-stream": "^3.0.0",
519 | "into-stream": "^3.1.0",
520 | "is-retry-allowed": "^1.1.0",
521 | "isurl": "^1.0.0-alpha5",
522 | "lowercase-keys": "^1.0.0",
523 | "mimic-response": "^1.0.0",
524 | "p-cancelable": "^0.3.0",
525 | "p-timeout": "^1.2.0",
526 | "pify": "^3.0.0",
527 | "safe-buffer": "^5.1.1",
528 | "timed-out": "^5.0.0",
529 | "url-parse-lax": "^3.0.0",
530 | "url-to-options": "^1.0.1"
531 | },
532 | "engines": {
533 | "node": ">=4"
534 | }
535 | },
536 | "node_modules/has": {
537 | "version": "1.0.3",
538 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
539 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
540 | "dev": true,
541 | "dependencies": {
542 | "function-bind": "^1.1.1"
543 | },
544 | "engines": {
545 | "node": ">= 0.4.0"
546 | }
547 | },
548 | "node_modules/has-symbol-support-x": {
549 | "version": "1.4.2",
550 | "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz",
551 | "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==",
552 | "engines": {
553 | "node": "*"
554 | }
555 | },
556 | "node_modules/has-symbols": {
557 | "version": "1.0.3",
558 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
559 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
560 | "dev": true,
561 | "engines": {
562 | "node": ">= 0.4"
563 | },
564 | "funding": {
565 | "url": "https://github.com/sponsors/ljharb"
566 | }
567 | },
568 | "node_modules/has-to-string-tag-x": {
569 | "version": "1.4.1",
570 | "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz",
571 | "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==",
572 | "dependencies": {
573 | "has-symbol-support-x": "^1.4.1"
574 | },
575 | "engines": {
576 | "node": "*"
577 | }
578 | },
579 | "node_modules/http-errors": {
580 | "version": "2.0.0",
581 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
582 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
583 | "dev": true,
584 | "dependencies": {
585 | "depd": "2.0.0",
586 | "inherits": "2.0.4",
587 | "setprototypeof": "1.2.0",
588 | "statuses": "2.0.1",
589 | "toidentifier": "1.0.1"
590 | },
591 | "engines": {
592 | "node": ">= 0.8"
593 | }
594 | },
595 | "node_modules/http-errors/node_modules/statuses": {
596 | "version": "2.0.1",
597 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
598 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
599 | "dev": true,
600 | "engines": {
601 | "node": ">= 0.8"
602 | }
603 | },
604 | "node_modules/iconv-lite": {
605 | "version": "0.4.24",
606 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
607 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
608 | "dev": true,
609 | "dependencies": {
610 | "safer-buffer": ">= 2.1.2 < 3"
611 | },
612 | "engines": {
613 | "node": ">=0.10.0"
614 | }
615 | },
616 | "node_modules/inflight": {
617 | "version": "1.0.6",
618 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
619 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
620 | "dev": true,
621 | "dependencies": {
622 | "once": "^1.3.0",
623 | "wrappy": "1"
624 | }
625 | },
626 | "node_modules/inherits": {
627 | "version": "2.0.4",
628 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
629 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
630 | },
631 | "node_modules/into-stream": {
632 | "version": "3.1.0",
633 | "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz",
634 | "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=",
635 | "dependencies": {
636 | "from2": "^2.1.1",
637 | "p-is-promise": "^1.1.0"
638 | },
639 | "engines": {
640 | "node": ">=4"
641 | }
642 | },
643 | "node_modules/ipaddr.js": {
644 | "version": "1.9.1",
645 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
646 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
647 | "dev": true,
648 | "engines": {
649 | "node": ">= 0.10"
650 | }
651 | },
652 | "node_modules/is-object": {
653 | "version": "1.0.2",
654 | "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
655 | "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
656 | "funding": {
657 | "url": "https://github.com/sponsors/ljharb"
658 | }
659 | },
660 | "node_modules/is-retry-allowed": {
661 | "version": "1.2.0",
662 | "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz",
663 | "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==",
664 | "engines": {
665 | "node": ">=0.10.0"
666 | }
667 | },
668 | "node_modules/isarray": {
669 | "version": "1.0.0",
670 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
671 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
672 | },
673 | "node_modules/isurl": {
674 | "version": "1.0.0",
675 | "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz",
676 | "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==",
677 | "dependencies": {
678 | "has-to-string-tag-x": "^1.2.0",
679 | "is-object": "^1.0.1"
680 | },
681 | "engines": {
682 | "node": ">= 4"
683 | }
684 | },
685 | "node_modules/jasmine": {
686 | "version": "4.1.0",
687 | "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.1.0.tgz",
688 | "integrity": "sha512-4VhjbUgwfNS9CBnUMoSWr9tdNgOoOhNIjAD8YRxTn+PmOf4qTSC0Uqhk66dWGnz2vJxtNIU0uBjiwnsp4Ud9VA==",
689 | "dev": true,
690 | "dependencies": {
691 | "glob": "^7.1.6",
692 | "jasmine-core": "^4.1.0"
693 | },
694 | "bin": {
695 | "jasmine": "bin/jasmine.js"
696 | }
697 | },
698 | "node_modules/jasmine-core": {
699 | "version": "4.1.0",
700 | "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.1.0.tgz",
701 | "integrity": "sha512-8E8BiffCL8sBwK1zU9cbavLe8xpJAgOduSJ6N8PJVv8VosQ/nxVTuXj2kUeHxTlZBVvh24G19ga7xdiaxlceKg==",
702 | "dev": true
703 | },
704 | "node_modules/json-stringify-safe": {
705 | "version": "5.0.1",
706 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
707 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
708 | "dev": true
709 | },
710 | "node_modules/lodash.set": {
711 | "version": "4.3.2",
712 | "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
713 | "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
714 | "dev": true
715 | },
716 | "node_modules/lowercase-keys": {
717 | "version": "1.0.1",
718 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
719 | "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
720 | "engines": {
721 | "node": ">=0.10.0"
722 | }
723 | },
724 | "node_modules/lru-cache": {
725 | "version": "7.9.0",
726 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.9.0.tgz",
727 | "integrity": "sha512-lkcNMUKqdJk96TuIXUidxaPuEg5sJo/+ZyVE2BDFnuZGzwXem7d8582eG8vbu4todLfT14snP6iHriCHXXi5Rw==",
728 | "engines": {
729 | "node": ">=12"
730 | }
731 | },
732 | "node_modules/media-typer": {
733 | "version": "0.3.0",
734 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
735 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
736 | "dev": true,
737 | "engines": {
738 | "node": ">= 0.6"
739 | }
740 | },
741 | "node_modules/merge-descriptors": {
742 | "version": "1.0.1",
743 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
744 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
745 | "dev": true
746 | },
747 | "node_modules/methods": {
748 | "version": "1.1.2",
749 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
750 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
751 | "dev": true,
752 | "engines": {
753 | "node": ">= 0.6"
754 | }
755 | },
756 | "node_modules/mime": {
757 | "version": "1.6.0",
758 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
759 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
760 | "dev": true,
761 | "bin": {
762 | "mime": "cli.js"
763 | },
764 | "engines": {
765 | "node": ">=4"
766 | }
767 | },
768 | "node_modules/mime-db": {
769 | "version": "1.52.0",
770 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
771 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
772 | "dev": true,
773 | "engines": {
774 | "node": ">= 0.6"
775 | }
776 | },
777 | "node_modules/mime-types": {
778 | "version": "2.1.35",
779 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
780 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
781 | "dev": true,
782 | "dependencies": {
783 | "mime-db": "1.52.0"
784 | },
785 | "engines": {
786 | "node": ">= 0.6"
787 | }
788 | },
789 | "node_modules/mimic-response": {
790 | "version": "1.0.1",
791 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
792 | "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
793 | "engines": {
794 | "node": ">=4"
795 | }
796 | },
797 | "node_modules/minimatch": {
798 | "version": "3.1.2",
799 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
800 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
801 | "dev": true,
802 | "dependencies": {
803 | "brace-expansion": "^1.1.7"
804 | },
805 | "engines": {
806 | "node": "*"
807 | }
808 | },
809 | "node_modules/ms": {
810 | "version": "2.1.2",
811 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
812 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
813 | },
814 | "node_modules/negotiator": {
815 | "version": "0.6.3",
816 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
817 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
818 | "dev": true,
819 | "engines": {
820 | "node": ">= 0.6"
821 | }
822 | },
823 | "node_modules/nock": {
824 | "version": "13.2.4",
825 | "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.4.tgz",
826 | "integrity": "sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug==",
827 | "dev": true,
828 | "dependencies": {
829 | "debug": "^4.1.0",
830 | "json-stringify-safe": "^5.0.1",
831 | "lodash.set": "^4.3.2",
832 | "propagate": "^2.0.0"
833 | },
834 | "engines": {
835 | "node": ">= 10.13"
836 | }
837 | },
838 | "node_modules/nock/node_modules/debug": {
839 | "version": "4.3.4",
840 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
841 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
842 | "dev": true,
843 | "dependencies": {
844 | "ms": "2.1.2"
845 | },
846 | "engines": {
847 | "node": ">=6.0"
848 | },
849 | "peerDependenciesMeta": {
850 | "supports-color": {
851 | "optional": true
852 | }
853 | }
854 | },
855 | "node_modules/nock/node_modules/ms": {
856 | "version": "2.1.2",
857 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
858 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
859 | "dev": true
860 | },
861 | "node_modules/object-inspect": {
862 | "version": "1.12.0",
863 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
864 | "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==",
865 | "dev": true,
866 | "funding": {
867 | "url": "https://github.com/sponsors/ljharb"
868 | }
869 | },
870 | "node_modules/on-finished": {
871 | "version": "2.3.0",
872 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
873 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
874 | "dev": true,
875 | "dependencies": {
876 | "ee-first": "1.1.1"
877 | },
878 | "engines": {
879 | "node": ">= 0.8"
880 | }
881 | },
882 | "node_modules/once": {
883 | "version": "1.4.0",
884 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
885 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
886 | "dev": true,
887 | "dependencies": {
888 | "wrappy": "1"
889 | }
890 | },
891 | "node_modules/p-cancelable": {
892 | "version": "0.3.0",
893 | "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz",
894 | "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==",
895 | "engines": {
896 | "node": ">=4"
897 | }
898 | },
899 | "node_modules/p-finally": {
900 | "version": "1.0.0",
901 | "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
902 | "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
903 | "engines": {
904 | "node": ">=4"
905 | }
906 | },
907 | "node_modules/p-is-promise": {
908 | "version": "1.1.0",
909 | "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz",
910 | "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=",
911 | "engines": {
912 | "node": ">=4"
913 | }
914 | },
915 | "node_modules/p-timeout": {
916 | "version": "1.2.1",
917 | "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz",
918 | "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=",
919 | "dependencies": {
920 | "p-finally": "^1.0.0"
921 | },
922 | "engines": {
923 | "node": ">=4"
924 | }
925 | },
926 | "node_modules/parseurl": {
927 | "version": "1.3.3",
928 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
929 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
930 | "dev": true,
931 | "engines": {
932 | "node": ">= 0.8"
933 | }
934 | },
935 | "node_modules/path-is-absolute": {
936 | "version": "1.0.1",
937 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
938 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
939 | "dev": true,
940 | "engines": {
941 | "node": ">=0.10.0"
942 | }
943 | },
944 | "node_modules/path-to-regexp": {
945 | "version": "0.1.7",
946 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
947 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
948 | "dev": true
949 | },
950 | "node_modules/pify": {
951 | "version": "3.0.0",
952 | "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
953 | "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
954 | "engines": {
955 | "node": ">=4"
956 | }
957 | },
958 | "node_modules/prepend-http": {
959 | "version": "2.0.0",
960 | "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
961 | "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
962 | "engines": {
963 | "node": ">=4"
964 | }
965 | },
966 | "node_modules/prettier": {
967 | "version": "2.6.2",
968 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz",
969 | "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==",
970 | "dev": true,
971 | "bin": {
972 | "prettier": "bin-prettier.js"
973 | },
974 | "engines": {
975 | "node": ">=10.13.0"
976 | },
977 | "funding": {
978 | "url": "https://github.com/prettier/prettier?sponsor=1"
979 | }
980 | },
981 | "node_modules/process-nextick-args": {
982 | "version": "2.0.1",
983 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
984 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
985 | },
986 | "node_modules/propagate": {
987 | "version": "2.0.1",
988 | "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
989 | "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
990 | "dev": true,
991 | "engines": {
992 | "node": ">= 8"
993 | }
994 | },
995 | "node_modules/proxy-addr": {
996 | "version": "2.0.7",
997 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
998 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
999 | "dev": true,
1000 | "dependencies": {
1001 | "forwarded": "0.2.0",
1002 | "ipaddr.js": "1.9.1"
1003 | },
1004 | "engines": {
1005 | "node": ">= 0.10"
1006 | }
1007 | },
1008 | "node_modules/qs": {
1009 | "version": "6.10.3",
1010 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
1011 | "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
1012 | "dev": true,
1013 | "dependencies": {
1014 | "side-channel": "^1.0.4"
1015 | },
1016 | "engines": {
1017 | "node": ">=0.6"
1018 | },
1019 | "funding": {
1020 | "url": "https://github.com/sponsors/ljharb"
1021 | }
1022 | },
1023 | "node_modules/range-parser": {
1024 | "version": "1.2.1",
1025 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1026 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1027 | "dev": true,
1028 | "engines": {
1029 | "node": ">= 0.6"
1030 | }
1031 | },
1032 | "node_modules/raw-body": {
1033 | "version": "2.5.1",
1034 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
1035 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
1036 | "dev": true,
1037 | "dependencies": {
1038 | "bytes": "3.1.2",
1039 | "http-errors": "2.0.0",
1040 | "iconv-lite": "0.4.24",
1041 | "unpipe": "1.0.0"
1042 | },
1043 | "engines": {
1044 | "node": ">= 0.8"
1045 | }
1046 | },
1047 | "node_modules/readable-stream": {
1048 | "version": "2.3.7",
1049 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
1050 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
1051 | "dependencies": {
1052 | "core-util-is": "~1.0.0",
1053 | "inherits": "~2.0.3",
1054 | "isarray": "~1.0.0",
1055 | "process-nextick-args": "~2.0.0",
1056 | "safe-buffer": "~5.1.1",
1057 | "string_decoder": "~1.1.1",
1058 | "util-deprecate": "~1.0.1"
1059 | }
1060 | },
1061 | "node_modules/readable-stream/node_modules/safe-buffer": {
1062 | "version": "5.1.2",
1063 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
1064 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
1065 | },
1066 | "node_modules/safe-buffer": {
1067 | "version": "5.2.1",
1068 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1069 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1070 | "funding": [
1071 | {
1072 | "type": "github",
1073 | "url": "https://github.com/sponsors/feross"
1074 | },
1075 | {
1076 | "type": "patreon",
1077 | "url": "https://www.patreon.com/feross"
1078 | },
1079 | {
1080 | "type": "consulting",
1081 | "url": "https://feross.org/support"
1082 | }
1083 | ]
1084 | },
1085 | "node_modules/safer-buffer": {
1086 | "version": "2.1.2",
1087 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1088 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1089 | "dev": true
1090 | },
1091 | "node_modules/send": {
1092 | "version": "0.18.0",
1093 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
1094 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
1095 | "dev": true,
1096 | "dependencies": {
1097 | "debug": "2.6.9",
1098 | "depd": "2.0.0",
1099 | "destroy": "1.2.0",
1100 | "encodeurl": "~1.0.2",
1101 | "escape-html": "~1.0.3",
1102 | "etag": "~1.8.1",
1103 | "fresh": "0.5.2",
1104 | "http-errors": "2.0.0",
1105 | "mime": "1.6.0",
1106 | "ms": "2.1.3",
1107 | "on-finished": "2.4.1",
1108 | "range-parser": "~1.2.1",
1109 | "statuses": "2.0.1"
1110 | },
1111 | "engines": {
1112 | "node": ">= 0.8.0"
1113 | }
1114 | },
1115 | "node_modules/send/node_modules/debug": {
1116 | "version": "2.6.9",
1117 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
1118 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
1119 | "dev": true,
1120 | "dependencies": {
1121 | "ms": "2.0.0"
1122 | }
1123 | },
1124 | "node_modules/send/node_modules/debug/node_modules/ms": {
1125 | "version": "2.0.0",
1126 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1127 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
1128 | "dev": true
1129 | },
1130 | "node_modules/send/node_modules/ms": {
1131 | "version": "2.1.3",
1132 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1133 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1134 | "dev": true
1135 | },
1136 | "node_modules/send/node_modules/on-finished": {
1137 | "version": "2.4.1",
1138 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1139 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1140 | "dev": true,
1141 | "dependencies": {
1142 | "ee-first": "1.1.1"
1143 | },
1144 | "engines": {
1145 | "node": ">= 0.8"
1146 | }
1147 | },
1148 | "node_modules/send/node_modules/statuses": {
1149 | "version": "2.0.1",
1150 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1151 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1152 | "dev": true,
1153 | "engines": {
1154 | "node": ">= 0.8"
1155 | }
1156 | },
1157 | "node_modules/serve-static": {
1158 | "version": "1.15.0",
1159 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
1160 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
1161 | "dev": true,
1162 | "dependencies": {
1163 | "encodeurl": "~1.0.2",
1164 | "escape-html": "~1.0.3",
1165 | "parseurl": "~1.3.3",
1166 | "send": "0.18.0"
1167 | },
1168 | "engines": {
1169 | "node": ">= 0.8.0"
1170 | }
1171 | },
1172 | "node_modules/setprototypeof": {
1173 | "version": "1.2.0",
1174 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1175 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1176 | "dev": true
1177 | },
1178 | "node_modules/side-channel": {
1179 | "version": "1.0.4",
1180 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
1181 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
1182 | "dev": true,
1183 | "dependencies": {
1184 | "call-bind": "^1.0.0",
1185 | "get-intrinsic": "^1.0.2",
1186 | "object-inspect": "^1.9.0"
1187 | },
1188 | "funding": {
1189 | "url": "https://github.com/sponsors/ljharb"
1190 | }
1191 | },
1192 | "node_modules/statuses": {
1193 | "version": "1.5.0",
1194 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
1195 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
1196 | "dev": true,
1197 | "engines": {
1198 | "node": ">= 0.6"
1199 | }
1200 | },
1201 | "node_modules/string_decoder": {
1202 | "version": "1.1.1",
1203 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
1204 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
1205 | "dependencies": {
1206 | "safe-buffer": "~5.1.0"
1207 | }
1208 | },
1209 | "node_modules/string_decoder/node_modules/safe-buffer": {
1210 | "version": "5.1.2",
1211 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
1212 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
1213 | },
1214 | "node_modules/timed-out": {
1215 | "version": "5.0.0",
1216 | "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-5.0.0.tgz",
1217 | "integrity": "sha512-+IAmaDqYnVi/HOMCeH4NDzdjTH9EHTC8xbe+PgXMKtovdMjbu25qYxeUj6u/yukMp1wDOhPeh6oTcIVepm5n7Q==",
1218 | "engines": {
1219 | "node": ">=8"
1220 | }
1221 | },
1222 | "node_modules/toidentifier": {
1223 | "version": "1.0.1",
1224 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1225 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1226 | "dev": true,
1227 | "engines": {
1228 | "node": ">=0.6"
1229 | }
1230 | },
1231 | "node_modules/type-is": {
1232 | "version": "1.6.18",
1233 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1234 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1235 | "dev": true,
1236 | "dependencies": {
1237 | "media-typer": "0.3.0",
1238 | "mime-types": "~2.1.24"
1239 | },
1240 | "engines": {
1241 | "node": ">= 0.6"
1242 | }
1243 | },
1244 | "node_modules/typescript": {
1245 | "version": "5.4.5",
1246 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
1247 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
1248 | "dev": true,
1249 | "bin": {
1250 | "tsc": "bin/tsc",
1251 | "tsserver": "bin/tsserver"
1252 | },
1253 | "engines": {
1254 | "node": ">=14.17"
1255 | }
1256 | },
1257 | "node_modules/unpipe": {
1258 | "version": "1.0.0",
1259 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1260 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
1261 | "dev": true,
1262 | "engines": {
1263 | "node": ">= 0.8"
1264 | }
1265 | },
1266 | "node_modules/url-parse-lax": {
1267 | "version": "3.0.0",
1268 | "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
1269 | "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
1270 | "dependencies": {
1271 | "prepend-http": "^2.0.0"
1272 | },
1273 | "engines": {
1274 | "node": ">=4"
1275 | }
1276 | },
1277 | "node_modules/url-to-options": {
1278 | "version": "1.0.1",
1279 | "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz",
1280 | "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=",
1281 | "engines": {
1282 | "node": ">= 4"
1283 | }
1284 | },
1285 | "node_modules/util-deprecate": {
1286 | "version": "1.0.2",
1287 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
1288 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
1289 | },
1290 | "node_modules/utils-merge": {
1291 | "version": "1.0.1",
1292 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1293 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
1294 | "dev": true,
1295 | "engines": {
1296 | "node": ">= 0.4.0"
1297 | }
1298 | },
1299 | "node_modules/vary": {
1300 | "version": "1.1.2",
1301 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1302 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
1303 | "engines": {
1304 | "node": ">= 0.8"
1305 | }
1306 | },
1307 | "node_modules/wrappy": {
1308 | "version": "1.0.2",
1309 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1310 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
1311 | "dev": true
1312 | }
1313 | }
1314 | }
1315 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prerendercloud",
3 | "version": "1.48.0",
4 | "description": "Express middleware for pre-rendering JavaScript single page apps with https://headless-render-api.com (formerly prerender.cloud from 2016 - 2022) for SEO and open graph tags",
5 | "main": "./distribution/index.js",
6 | "types": "./prerendercloud.d.ts",
7 | "scripts": {
8 | "build": "rm -rf distribution && mkdir distribution && cp -r source/* distribution/",
9 | "test": "jasmine"
10 | },
11 | "author": "Jonathan Otto (https://headless-render-api.com/)",
12 | "license": "MIT",
13 | "dependencies": {
14 | "debug": "^4.3.4",
15 | "got-lite": "^8.0.4",
16 | "lru-cache": "^7.9.0",
17 | "vary": "^1.1.1"
18 | },
19 | "devDependencies": {
20 | "connect": "^3.7.0",
21 | "express": "^4.18.1",
22 | "jasmine": "^4.1.0",
23 | "nock": "^13.2.4",
24 | "prettier": "^2.6.2",
25 | "typescript": "^5.4.5"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/prerendercloud.d.ts:
--------------------------------------------------------------------------------
1 | export = prerendercloud;
2 |
3 | declare function prerendercloud(...args: any[]): void;
4 |
5 | declare namespace prerendercloud {
6 | class Options {
7 | constructor(...args: any[]);
8 |
9 | isThrottled(...args: any[]): void;
10 |
11 | recordFail(...args: any[]): void;
12 |
13 | reset(...args: any[]): void;
14 |
15 | set(...args: any[]): void;
16 |
17 | static validOptions: string[];
18 | }
19 |
20 | const botsOnlyList: string[];
21 |
22 | const cache: any;
23 |
24 | function pdf(url: string, options?: PdfOptions): Promise;
25 |
26 | function prerender(url: string, params?: any): Promise;
27 |
28 | function resetOptions(): any;
29 |
30 | function scrape(url: string, options?: ScrapeOptions): Promise;
31 |
32 | function screenshot(
33 | url: string,
34 | options?: ScreenshotOptions
35 | ): Promise;
36 |
37 | function set(optionKey: string, optionValue: any): any;
38 |
39 | function userAgentIsBot(
40 | headers: { [key: string]: string },
41 | requestedPath?: string
42 | ): boolean;
43 |
44 | namespace util {
45 | function basenameIsHtml(basename: string): boolean;
46 |
47 | function isFunction(functionToCheck: any): boolean;
48 |
49 | function urlPathIsHtml(urlPath: string): boolean;
50 | }
51 |
52 | interface PdfOptions {
53 | noPageBreaks?: boolean;
54 | pageRanges?: string;
55 | scale?: number;
56 | preferCssPageSize?: boolean;
57 | printBackground?: boolean;
58 | landscape?: boolean;
59 | marginTop?: number;
60 | marginRight?: number;
61 | marginBottom?: number;
62 | marginLeft?: number;
63 | paperWidth?: number;
64 | paperHeight?: number;
65 | emulatedMedia?:
66 | | "screen"
67 | | "print"
68 | | "braille"
69 | | "embossed"
70 | | "handheld"
71 | | "projection"
72 | | "speech"
73 | | "tty"
74 | | "tv";
75 | waitExtraLong?: boolean;
76 | dontWaitForWebSockets?: boolean;
77 | blockCookies?: boolean;
78 | }
79 |
80 | interface ScreenshotOptions {
81 | deviceWidth?: number; // Default: 1366
82 | deviceHeight?: number; // Default: 768
83 | deviceIsMobile?: boolean; // Default: false
84 | viewportQuerySelector?: string;
85 | viewportQuerySelectorPadding?: number;
86 | viewportWidth?: number; // Defaults to deviceWidth
87 | viewportHeight?: number; // Defaults to deviceHeight
88 | viewportX?: number; // Default: 0
89 | viewportY?: number; // Default: 0
90 | viewportScale?: number; // Default: 1, min: 0.1, max: 3.0
91 | format?: "png" | "webp" | "jpeg"; // Default: png
92 | emulatedMedia?:
93 | | "screen"
94 | | "print"
95 | | "braille"
96 | | "embossed"
97 | | "handheld"
98 | | "projection"
99 | | "speech"
100 | | "tty"
101 | | "tv"; // Default: screen
102 | waitExtraLong?: boolean;
103 | dontWaitForWebSockets?: boolean;
104 | blockCookies?: boolean;
105 | }
106 |
107 | interface ScrapeOptions {
108 | withMetadata?: boolean;
109 | withScreenshot?: boolean;
110 | followRedirects?: boolean; // Default: false
111 | deviceIsMobile?: boolean; // Default: false
112 | deviceWidth?: number; // Default: 1366, min: 10, max: 10000
113 | deviceHeight?: number; // Default: 768, min: 10, max: 10000
114 | emulatedMedia?:
115 | | "screen"
116 | | "print"
117 | | "braille"
118 | | "embossed"
119 | | "handheld"
120 | | "projection"
121 | | "speech"
122 | | "tty"
123 | | "tv"; // Default: screen
124 | waitExtraLong?: boolean;
125 | dontWaitForWebSockets?: boolean;
126 | blockCookies?: boolean;
127 | }
128 |
129 | interface ScrapeResult {
130 | body: string;
131 | meta: {
132 | title: string;
133 | h1: string;
134 | description: string;
135 | ogImage: string;
136 | ogTitle: string;
137 | ogDescription: string;
138 | ogType: string;
139 | twitterCard: string;
140 | };
141 | links: string[];
142 | screenshot: Buffer;
143 | statusCode: number;
144 | headers: { [key: string]: string };
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/source/index.js:
--------------------------------------------------------------------------------
1 | const getNodeVersion = () => {
2 | try {
3 | return parseFloat(process.version.replace(/v/, ""));
4 | } catch (err) {
5 | return null;
6 | }
7 | };
8 |
9 | const nodeVersion = getNodeVersion();
10 |
11 | if (nodeVersion < 12.0) {
12 | try {
13 | console.log("prerendercloud requires node >= 12.0");
14 | process.exit(1);
15 | } catch (err) {
16 | // if calling process throws an error, probably running in serverless environment
17 | // e.g. vercel edge functions
18 | }
19 | }
20 | const debug = require("debug")("prerendercloud");
21 |
22 | const util = require("./lib/util");
23 | const Url = require("./lib/our-url");
24 | const middlewareCacheSingleton = {};
25 | const Options = require("./lib/options");
26 | const options = new Options(middlewareCacheSingleton);
27 | const apiOptions = require("./lib/api-options.json");
28 |
29 | const got = require("got-lite");
30 | require("./lib/got-retries")(got, options, debug);
31 |
32 | const vary = require("vary");
33 |
34 | // preserve (and send to client) these headers from service.headless-render-api.com which originally came from the origin server
35 | const headerWhitelist = [
36 | "vary",
37 | "content-type",
38 | "cache-control",
39 | "strict-transport-security",
40 | "content-security-policy",
41 | "public-key-pins",
42 | "x-frame-options",
43 | "x-xss-protection",
44 | "x-content-type-options",
45 | "location",
46 | ];
47 |
48 | const botUserAgentMapper = (ua) => ua.toLowerCase();
49 |
50 | const botsOnlyList = [
51 | "googlebot",
52 | "yahoo",
53 | "bingbot",
54 | "yandex",
55 | "baiduspider",
56 | "facebookexternalhit",
57 | "twitterbot",
58 | "rogerbot",
59 | "linkedinbot",
60 | "embedly",
61 | "quora link preview",
62 | "showyoubot",
63 | "outbrain",
64 | "pinterest/0.",
65 | "pinterestbot",
66 | "developers.google.com/+/web/snippet",
67 | "slackbot",
68 | "vkShare",
69 | "W3C_Validator",
70 | "redditbot",
71 | "Applebot",
72 | "WhatsApp",
73 | "flipboard",
74 | "tumblr",
75 | "bitlybot",
76 | "Bitrix link preview",
77 | "XING-contenttabreceiver",
78 | "Discordbot",
79 | "TelegramBot",
80 | "Google Search Console",
81 | ].map(botUserAgentMapper);
82 |
83 | const userAgentIsBot = (headers, requestedPath = "") => {
84 | const reqUserAgent =
85 | (headers["user-agent"] && headers["user-agent"].toLowerCase()) || "";
86 |
87 | if (headers["x-bufferbot"]) return true;
88 |
89 | if (requestedPath.match(/[?&]_escaped_fragment_/)) return true;
90 |
91 | return botsOnlyList.some((enabledUserAgent) =>
92 | reqUserAgent.includes(enabledUserAgent)
93 | );
94 | };
95 |
96 | const userAgentIsBotFromList = (botsOnlyList, headers, requestedPath = "") => {
97 | const reqUserAgent =
98 | (headers["user-agent"] && headers["user-agent"].toLowerCase()) || "";
99 |
100 | if (headers["x-bufferbot"]) return true;
101 |
102 | if (requestedPath.match(/[?&]_escaped_fragment_/)) return true;
103 |
104 | return botsOnlyList.some((enabledUserAgent) =>
105 | reqUserAgent.includes(enabledUserAgent)
106 | );
107 | };
108 |
109 | const getServiceUrl = (hardcoded) =>
110 | (hardcoded && hardcoded.replace(/\/+$/, "")) ||
111 | process.env.PRERENDER_SERVICE_URL ||
112 | "https://service.headless-render-api.com";
113 | const getRenderUrl = (action, url) =>
114 | [getServiceUrl(), action, url].filter((p) => p).join("/");
115 |
116 | // from https://stackoverflow.com/a/41072596
117 | // if there are multiple values with different case, it just takes the last
118 | // (which is wrong, it should merge some of them according to the HTTP spec, but it's fine for now)
119 | const objectKeysToLowerCase = function (origObj) {
120 | return Object.keys(origObj).reduce(function (newObj, key) {
121 | const val = origObj[key];
122 | const newVal = typeof val === "object" ? objectKeysToLowerCase(val) : val;
123 | newObj[key.toLowerCase()] = newVal;
124 | return newObj;
125 | }, {});
126 | };
127 |
128 | const is5xxError = (statusCode) => parseInt(statusCode / 100) === 5;
129 | const is4xxError = (statusCode) => {
130 | const n = parseInt(statusCode);
131 | const i = parseInt(statusCode / 100);
132 |
133 | return i === 4 && n !== 429;
134 | };
135 | const isGotClientTimeout = (err) =>
136 | err.name === "RequestError" && err.code === "ETIMEDOUT";
137 |
138 | const zlib = require("zlib");
139 | function compression(req, res, data) {
140 | if (
141 | req.headers["accept-encoding"] &&
142 | req.headers["accept-encoding"].match(/gzip/i)
143 | ) {
144 | zlib.gzip(data.body, (err, gzipped) => {
145 | if (err) {
146 | console.error(err);
147 | return res.status(500).send("compression error");
148 | }
149 |
150 | res.writeHead(
151 | data.statusCode,
152 | Object.assign({}, data.headers, { "content-encoding": "gzip" })
153 | );
154 | res.end(gzipped);
155 | });
156 | } else {
157 | res.writeHead(data.statusCode, data.headers);
158 | res.end(data.body);
159 | }
160 | }
161 |
162 | const handleSkip = (msg, next) => {
163 | debug(msg);
164 | if (process.env.NODE_ENV !== "test")
165 | console.error("prerendercloud middleware SKIPPED:", msg);
166 | return next();
167 | };
168 |
169 | const concurrentRequestCache = {};
170 |
171 | // response: { body, statusCode, headers }
172 | function createResponse(req, requestedUrl, response) {
173 | const lowerCasedHeaders = objectKeysToLowerCase(response.headers);
174 |
175 | const headers = {};
176 | headerWhitelist.forEach((h) => {
177 | if (lowerCasedHeaders[h]) headers[h] = lowerCasedHeaders[h];
178 | });
179 |
180 | let body = response.body;
181 | let screenshot;
182 | let meta;
183 | let links;
184 | if (
185 | (options.options.withScreenshot && options.options.withScreenshot(req)) ||
186 | (options.options.withMetadata && options.options.withMetadata(req))
187 | ) {
188 | headers["content-type"] = "text/html";
189 | let json;
190 | try {
191 | json = JSON.parse(body);
192 | } catch (err) {
193 | if (err.name && err.name.match(/SyntaxError/)) {
194 | console.log(options.options);
195 | console.log(req.host);
196 | console.log(req.url);
197 | console.log(response.headers);
198 | console.log(body);
199 | console.error(
200 | "withScreenshot expects JSON from server but parsing this failed:",
201 | body && body.toString().slice(0, 140) + "..."
202 | );
203 | }
204 |
205 | throw err;
206 | }
207 | screenshot = json.screenshot && Buffer.from(json.screenshot, "base64");
208 | body = json.body && Buffer.from(json.body, "base64").toString();
209 | meta = json.meta && JSON.parse(Buffer.from(json.meta, "base64"));
210 | links = json.links && JSON.parse(Buffer.from(json.links, "base64"));
211 | }
212 |
213 | const data = { statusCode: response.statusCode, headers, body };
214 |
215 | if (screenshot) data.screenshot = screenshot;
216 | if (meta) data.meta = meta;
217 | if (links) data.links = links;
218 |
219 | if (
220 | options.options.enableMiddlewareCache &&
221 | `${response.statusCode}`.startsWith("2") &&
222 | body &&
223 | body.length
224 | )
225 | middlewareCacheSingleton.instance.set(requestedUrl, data);
226 |
227 | return data;
228 | }
229 |
230 | class Prerender {
231 | constructor(req) {
232 | this.req = req;
233 | this.url = Url.parse(req, options);
234 | this.configureBotsOnlyList();
235 | }
236 |
237 | configureBotsOnlyList() {
238 | this.botsOnlyList = botsOnlyList;
239 |
240 | if (options.options.botsOnly && Array.isArray(options.options.botsOnly)) {
241 | options.options.botsOnly.forEach((additionalBotUserAgent) => {
242 | const alreadyExistsInList = this.botsOnlyList.find(
243 | (b) => b === additionalBotUserAgent
244 | );
245 | if (!alreadyExistsInList) {
246 | this.botsOnlyList.push(botUserAgentMapper(additionalBotUserAgent));
247 | }
248 | });
249 | }
250 | }
251 |
252 | // promise cache wrapper around ._get to prevent concurrent requests to same URL
253 | get() {
254 | if (concurrentRequestCache[this.url.requestedUrl])
255 | return concurrentRequestCache[this.url.requestedUrl];
256 |
257 | const promise = this._get();
258 |
259 | const deleteCache = () => {
260 | concurrentRequestCache[this.url.requestedUrl] = undefined;
261 | delete concurrentRequestCache[this.url.requestedUrl];
262 | };
263 |
264 | return (concurrentRequestCache[this.url.requestedUrl] = promise)
265 | .then((res) => {
266 | deleteCache();
267 | return res;
268 | })
269 | .catch((err) => {
270 | deleteCache();
271 | return Promise.reject(err);
272 | });
273 | }
274 |
275 | // fulfills promise when service.headless-render-api.com response is: 2xx, 4xx
276 | // rejects promise when request lib errors or service.headless-render-api.com response is: 5xx
277 | _get() {
278 | const apiRequestUrl = this._createApiRequestUrl();
279 | const headers = this._createHeaders();
280 |
281 | let requestPromise;
282 |
283 | if (options.isThrottled(this.url.requestedUrl)) {
284 | requestPromise = Promise.reject(new Error("throttled"));
285 | } else {
286 | debug("prerendering:", apiRequestUrl, headers);
287 | requestPromise = got.get(apiRequestUrl, {
288 | headers,
289 | retries: options.options.retries,
290 | followRedirect: false,
291 | timeout: options.options.timeout || 20000,
292 | });
293 | }
294 |
295 | return requestPromise
296 | .then((response) => {
297 | return createResponse(this.req, this.url.requestedUrl, response);
298 | })
299 | .catch((err) => {
300 | const shouldBubble = util.isFunction(options.options.bubbleUp5xxErrors)
301 | ? options.options.bubbleUp5xxErrors(err, this.req, err.response)
302 | : options.options.bubbleUp5xxErrors;
303 |
304 | const statusCode = err.response && parseInt(err.response.statusCode);
305 |
306 | const nonErrorStatusCode =
307 | statusCode === 404 || statusCode === 301 || statusCode === 302;
308 |
309 | if (!nonErrorStatusCode) {
310 | options.recordFail(this.url.requestedUrl);
311 | }
312 |
313 | if (shouldBubble && !nonErrorStatusCode) {
314 | if (err.response && is5xxError(err.response.statusCode))
315 | return createResponse(
316 | this.req,
317 | this.url.requestedUrl,
318 | err.response
319 | );
320 |
321 | if (err.response && is4xxError(err.response.statusCode))
322 | return createResponse(
323 | this.req,
324 | this.url.requestedUrl,
325 | err.response
326 | );
327 |
328 | if (err.message && err.message.match(/throttle/)) {
329 | return createResponse(this.req, this.url.requestedUrl, {
330 | body: "Error: headless-render-api.com client throttled this prerender request due to a recent timeout",
331 | statusCode: 503,
332 | headers: { "content-type": "text/html" },
333 | });
334 | }
335 |
336 | if (isGotClientTimeout(err))
337 | return createResponse(this.req, this.url.requestedUrl, {
338 | body: "Error: headless-render-api.com client timeout (as opposed to headless-render-api.com server timeout)",
339 | statusCode: 500,
340 | headers: { "content-type": "text/html" },
341 | });
342 |
343 | return Promise.reject(err);
344 | } else if (err.response && is4xxError(err.response.statusCode)) {
345 | return createResponse(this.req, this.url.requestedUrl, err.response);
346 | }
347 |
348 | return Promise.reject(err);
349 | });
350 | }
351 |
352 | writeHttpResponse(req, res, next, data) {
353 | const _writeHttpResponse = () =>
354 | this._writeHttpResponse(req, res, next, data);
355 |
356 | if (options.options.afterRenderBlocking) {
357 | // preserve original body from origin headless-render-api.com so mutations
358 | // from afterRenderBlocking don't affect concurrentRequestCache
359 | if (!data.origBodyBeforeAfterRenderBlocking) {
360 | data.origBodyBeforeAfterRenderBlocking = data.body;
361 | } else {
362 | data.body = data.origBodyBeforeAfterRenderBlocking;
363 | }
364 | return options.options.afterRenderBlocking(
365 | null,
366 | req,
367 | data,
368 | _writeHttpResponse
369 | );
370 | }
371 |
372 | _writeHttpResponse();
373 | }
374 |
375 | // data looks like { statusCode, headers, body }
376 | _writeHttpResponse(req, res, next, data) {
377 | if (options.options.afterRender)
378 | process.nextTick(() => options.options.afterRender(null, req, data));
379 |
380 | try {
381 | if (data.statusCode === 400) {
382 | debug(
383 | "service.headless-render-api.com returned status 400 request invalid:"
384 | );
385 | res.statusCode = 400;
386 | res.end(data.body);
387 | } else if (data.statusCode === 429) {
388 | return handleSkip("rate limited due to free tier", next);
389 | } else {
390 | return compression(req, res, data);
391 | }
392 | } catch (error) {
393 | console.error(
394 | "unrecoverable prerendercloud middleware error:",
395 | error && error.message
396 | );
397 | console.error(
398 | "submit steps to reproduce here: https://github.com/sanfrancesco/prerendercloud-nodejs/issues"
399 | );
400 | throw error;
401 | }
402 | }
403 |
404 | static middleware(req, res, next) {
405 | const prerender = new Prerender(req);
406 |
407 | const objForReqRes = {
408 | url: { requestedPath: prerender.url.requestedPath },
409 | };
410 | // this is for beforeRender(req, done) func so there's visibility into what URL is being used
411 | req.prerender = objForReqRes;
412 | // this is for lambda@edge downstream: https://github.com/sanfrancesco/prerendercloud-lambda-edge
413 | res.prerender = objForReqRes;
414 |
415 | if (options.options.botsOnly) {
416 | vary(res, "User-Agent");
417 | }
418 |
419 | if (!prerender._shouldPrerender()) {
420 | debug(
421 | "NOT prerendering",
422 | req.originalUrl,
423 | req && req.headers && { "user-agent": req.headers["user-agent"] }
424 | );
425 | return next();
426 | }
427 |
428 | if (options.options.enableMiddlewareCache) {
429 | const cached = middlewareCacheSingleton.instance.get(
430 | prerender.url.requestedUrl
431 | );
432 | if (cached) {
433 | debug(
434 | "returning cache",
435 | req.originalUrl,
436 | req && req.headers && { "user-agent": req.headers["user-agent"] }
437 | );
438 | return prerender.writeHttpResponse(req, res, next, cached);
439 | }
440 | }
441 |
442 | const remotePrerender = function () {
443 | return prerender
444 | .get()
445 | .then(function (data) {
446 | return prerender.writeHttpResponse(req, res, next, data);
447 | })
448 | .catch(function (error) {
449 | if (process.env.NODE_ENV !== "test") console.error(error);
450 | return handleSkip(`server error: ${error && error.message}`, next);
451 | });
452 | };
453 |
454 | if (options.options.beforeRender) {
455 | const donePassedToUserBeforeRender = function (err, stringOrObject) {
456 | if (!stringOrObject) {
457 | return remotePrerender();
458 | } else if (typeof stringOrObject === "string") {
459 | return prerender.writeHttpResponse(req, res, next, {
460 | statusCode: 200,
461 | headers: {
462 | "content-type": "text/html; charset=utf-8",
463 | },
464 | body: stringOrObject,
465 | });
466 | } else if (typeof stringOrObject === "object") {
467 | return prerender.writeHttpResponse(
468 | req,
469 | res,
470 | next,
471 | Object.assign(
472 | {
473 | statusCode: stringOrObject.status,
474 | headers: Object.assign(
475 | {
476 | "content-type": "text/html; charset=utf-8",
477 | },
478 | stringOrObject.headers
479 | ),
480 | body: stringOrObject.body,
481 | },
482 | {
483 | screenshot: stringOrObject.screenshot,
484 | meta: stringOrObject.meta,
485 | links: stringOrObject.links,
486 | }
487 | )
488 | );
489 | }
490 | };
491 | return options.options.beforeRender(req, donePassedToUserBeforeRender);
492 | } else {
493 | return remotePrerender();
494 | }
495 | }
496 |
497 | _shouldPrerender() {
498 | if (!(this.req && this.req.headers)) return false;
499 |
500 | if (this.req.method != "GET" && this.req.method != "HEAD") return false;
501 |
502 | if (this._alreadyPrerendered()) return false;
503 |
504 | if (!this._prerenderableExtension()) return false;
505 |
506 | if (this._isPrerenderCloudUserAgent()) return false;
507 |
508 | if (this._isBlacklistedPath()) return false;
509 |
510 | if (options.options.whitelistPaths && !this._isWhitelistedPath())
511 | return false;
512 |
513 | if (options.options.shouldPrerender) {
514 | return options.options.shouldPrerender(this.req);
515 | } else if (options.options.shouldPrerenderAdditionalCheck) {
516 | return (
517 | options.options.shouldPrerenderAdditionalCheck(this.req) &&
518 | this._prerenderableUserAgent()
519 | );
520 | } else {
521 | return this._prerenderableUserAgent();
522 | }
523 | }
524 |
525 | _createHeaders() {
526 | let h = {
527 | "User-Agent": "prerender-cloud-nodejs-middleware",
528 | "accept-encoding": "gzip",
529 | };
530 |
531 | if (this.req.headers["user-agent"])
532 | Object.assign(h, {
533 | "X-Original-User-Agent": this.req.headers["user-agent"],
534 | });
535 |
536 | let token = options.options.prerenderToken || process.env.PRERENDER_TOKEN;
537 |
538 | if (token) Object.assign(h, { "X-Prerender-Token": token });
539 |
540 | if (options.options.removeScriptTags)
541 | Object.assign(h, { "Prerender-Remove-Script-Tags": true });
542 |
543 | if (options.options.removeTrailingSlash)
544 | Object.assign(h, { "Prerender-Remove-Trailing-Slash": true });
545 |
546 | if (options.options.metaOnly && options.options.metaOnly(this.req))
547 | Object.assign(h, { "Prerender-Meta-Only": true });
548 |
549 | if (
550 | options.options.followRedirects &&
551 | options.options.followRedirects(this.req)
552 | )
553 | Object.assign(h, { "Prerender-Follow-Redirects": true });
554 |
555 | if (options.options.serverCacheDurationSeconds) {
556 | const duration = options.options.serverCacheDurationSeconds(this.req);
557 | if (duration != null) {
558 | Object.assign(h, { "Prerender-Cache-Duration": duration });
559 | }
560 | }
561 |
562 | if (options.options.deviceWidth) {
563 | const deviceWidth = options.options.deviceWidth(this.req);
564 | if (deviceWidth != null && typeof deviceWidth === "number") {
565 | Object.assign(h, { "Prerender-Device-Width": deviceWidth });
566 | }
567 | }
568 |
569 | if (options.options.deviceHeight) {
570 | const deviceHeight = options.options.deviceHeight(this.req);
571 | if (deviceHeight != null && typeof deviceHeight === "number") {
572 | Object.assign(h, { "Prerender-Device-Height": deviceHeight });
573 | }
574 | }
575 |
576 | if (options.options.waitExtraLong)
577 | Object.assign(h, { "Prerender-Wait-Extra-Long": true });
578 |
579 | if (options.options.disableServerCache) Object.assign(h, { noCache: true });
580 | if (options.options.disableAjaxBypass)
581 | Object.assign(h, { "Prerender-Disable-Ajax-Bypass": true });
582 | if (options.options.disableAjaxPreload)
583 | Object.assign(h, { "Prerender-Disable-Ajax-Preload": true });
584 | if (options.options.disableHeadDedupe)
585 | Object.assign(h, { "Prerender-Disable-Head-Dedupe": true });
586 |
587 | if (
588 | options.options.withScreenshot &&
589 | options.options.withScreenshot(this.req)
590 | )
591 | Object.assign(h, { "Prerender-With-Screenshot": true });
592 |
593 | if (options.options.withMetadata && options.options.withMetadata(this.req))
594 | Object.assign(h, { "Prerender-With-Metadata": true });
595 |
596 | if (this._hasOriginHeaderWhitelist()) {
597 | options.options.originHeaderWhitelist.forEach((_h) => {
598 | if (this.req.headers[_h])
599 | Object.assign(h, { [_h]: this.req.headers[_h] });
600 | });
601 |
602 | Object.assign(h, {
603 | "Origin-Header-Whitelist":
604 | options.options.originHeaderWhitelist.join(" "),
605 | });
606 | }
607 |
608 | return h;
609 | }
610 |
611 | _hasOriginHeaderWhitelist() {
612 | return (
613 | options.options.originHeaderWhitelist &&
614 | Array.isArray(options.options.originHeaderWhitelist)
615 | );
616 | }
617 |
618 | _createApiRequestUrl() {
619 | return getRenderUrl(null, this.url.requestedUrl);
620 | }
621 |
622 | _alreadyPrerendered() {
623 | return !!this.req.headers["x-prerendered"];
624 | }
625 |
626 | _prerenderableExtension() {
627 | return this.url.hasHtmlPath;
628 | }
629 |
630 | _isPrerenderCloudUserAgent() {
631 | let reqUserAgent = this.req.headers["user-agent"];
632 |
633 | if (!reqUserAgent) return false;
634 |
635 | reqUserAgent = reqUserAgent.toLowerCase();
636 |
637 | return reqUserAgent.match(/prerendercloud/i);
638 | }
639 |
640 | _isWhitelistedPath() {
641 | const paths = options.options.whitelistPaths(this.req);
642 |
643 | if (paths && Array.isArray(paths)) {
644 | if (paths.length === 0) {
645 | return true;
646 | }
647 |
648 | return paths.some((path) => {
649 | if (path instanceof RegExp) {
650 | return path.test(this.req.url);
651 | } else {
652 | return path === this.req.url;
653 | }
654 |
655 | return false;
656 | });
657 | }
658 |
659 | return true;
660 | }
661 |
662 | _isBlacklistedPath() {
663 | if (options.options.blacklistPaths) {
664 | const paths = options.options.blacklistPaths(this.req);
665 |
666 | if (paths && Array.isArray(paths)) {
667 | return paths.some((path) => {
668 | if (path === this.req.url) return true;
669 |
670 | if (path.endsWith("*")) {
671 | const starIndex = path.indexOf("*");
672 | const pathSlice = path.slice(0, starIndex);
673 |
674 | if (this.req.url.startsWith(pathSlice)) return true;
675 | }
676 |
677 | return false;
678 | });
679 | }
680 | }
681 |
682 | return false;
683 | }
684 |
685 | _prerenderableUserAgent() {
686 | const reqUserAgent = this.req.headers["user-agent"];
687 |
688 | if (!reqUserAgent) return false;
689 |
690 | if (options.options.whitelistUserAgents)
691 | return options.options.whitelistUserAgents.some((enabledUserAgent) =>
692 | reqUserAgent.includes(enabledUserAgent)
693 | );
694 |
695 | if (!options.options.botsOnly) return true;
696 |
697 | // bots only
698 | return userAgentIsBotFromList(
699 | this.botsOnlyList,
700 | this.req.headers,
701 | this.url.original
702 | );
703 | }
704 | }
705 |
706 | Prerender.middleware.set = options.set.bind(options, Prerender.middleware);
707 | // Prerender.middleware.cache =
708 | Object.defineProperty(Prerender.middleware, "cache", {
709 | get: function () {
710 | return middlewareCacheSingleton.instance;
711 | },
712 | });
713 |
714 | function validateOption(option, value) {
715 | const isValidType = validateOptionType(value, option.openApiType);
716 |
717 | if (!isValidType) {
718 | return false;
719 | }
720 |
721 | if (option.min != null && value < option.min) {
722 | return false;
723 | }
724 |
725 | if (option.max !== null && value > option.max) {
726 | return false;
727 | }
728 |
729 | // Check if openApiEnum is present and validate against it
730 | if (option.openApiEnum !== undefined && Array.isArray(option.openApiEnum)) {
731 | let _value = value;
732 | if (_value === "jpg") {
733 | _value = "jpeg";
734 | }
735 | if (!option.openApiEnum.includes(_value)) {
736 | return false;
737 | }
738 | }
739 |
740 | return true;
741 | }
742 |
743 | function validateOptionType(value, openApiType) {
744 | switch (openApiType) {
745 | case "integer":
746 | return Number.isInteger(value);
747 | case "float":
748 | return typeof value === "number" && !isNaN(value);
749 | case "boolean":
750 | return typeof value === "boolean";
751 | case "string":
752 | return typeof value === "string";
753 | default:
754 | // return valid if no type was defined
755 | return true;
756 | }
757 | }
758 |
759 | const assignHeaders = (jobType, headers, params = {}) => {
760 | const jobOptions = apiOptions[jobType];
761 |
762 | if (!jobOptions) {
763 | throw new Error(`unknown job type: ${jobType}`);
764 | }
765 |
766 | Object.keys(params).forEach((param) => {
767 | const option = jobOptions[param];
768 |
769 | if (option) {
770 | if (!validateOption(option, params[param])) {
771 | console.error(`Error: invalid value for ${param}`);
772 | console.error(`Option details:`, option);
773 | throw new Error(`invalid value for ${param}: ${params[param]}`);
774 | }
775 |
776 | headers[option.httpName] = params[param];
777 | } else {
778 | console.error(`Error: unknown param: ${param} for job type ${jobType}`);
779 | console.error("maybe you meant one of these", Object.keys(jobOptions));
780 | throw new Error(`unknown param: ${param}`);
781 | }
782 | });
783 | };
784 |
785 | const screenshotAndPdf = (action, url, params = {}) => {
786 | const headers = {};
787 |
788 | const token = options.options.prerenderToken || process.env.PRERENDER_TOKEN;
789 |
790 | if (token) Object.assign(headers, { "X-Prerender-Token": token });
791 |
792 | assignHeaders(action.toUpperCase(), headers, params);
793 |
794 | // console.log({ url: getRenderUrl(action, url), headers });
795 |
796 | return got(getRenderUrl(action, url), {
797 | encoding: null,
798 | headers,
799 | retries: options.options.retries,
800 | followRedirect: options.options.followRedirect || false,
801 | }).then((res) => {
802 | if (action === "pdf" || action === "screenshot") {
803 | return res.body;
804 | }
805 |
806 | let shouldReturnRedirect = false;
807 | if (
808 | !options.options.followRedirect &&
809 | (res.statusCode === 302 || res.statusCode === 301)
810 | ) {
811 | shouldReturnRedirect = true;
812 | }
813 |
814 | if (shouldReturnRedirect) {
815 | return {
816 | statusCode: res.statusCode,
817 | headers: res.headers,
818 | };
819 | }
820 |
821 | // scrape always returns an object as opposed to buffer
822 | // and if the response was not json, then it didn't have
823 | // options like withScreenshot or withMetadata
824 | const contentType = res.headers["content-type"];
825 | const isJson = contentType && contentType.match(/json/);
826 | if (!isJson) {
827 | return { body: res.body };
828 | }
829 |
830 | const parsed = JSON.parse(res.body);
831 |
832 | Object.keys(parsed).forEach((key) => {
833 | if (!parsed[key]) {
834 | return;
835 | }
836 | if (key === "statusCode" && parsed[key]) {
837 | parsed[key] = parseInt(parsed[key]);
838 | return;
839 | }
840 | if (key === "headers" && parsed[key]) {
841 | parsed[key] = parsed[key];
842 | return;
843 | }
844 |
845 | parsed[key] = Buffer.from(parsed[key], "base64");
846 | if (key === "meta" || key === "links") {
847 | parsed[key] = JSON.parse(parsed[key].toString());
848 | }
849 | });
850 |
851 | return parsed;
852 | });
853 | };
854 |
855 | Prerender.middleware.screenshot = screenshotAndPdf.bind(
856 | undefined,
857 | "screenshot"
858 | );
859 | Prerender.middleware.pdf = screenshotAndPdf.bind(undefined, "pdf");
860 | Prerender.middleware.scrape = screenshotAndPdf.bind(undefined, "scrape");
861 |
862 | Prerender.middleware.prerender = function (url, params) {
863 | const headers = {};
864 |
865 | const token = options.options.prerenderToken || process.env.PRERENDER_TOKEN;
866 |
867 | if (token) Object.assign(headers, { "X-Prerender-Token": token });
868 |
869 | return got(getRenderUrl(null, url), {
870 | encoding: null,
871 | headers,
872 | retries: options.options.retries,
873 | followRedirect: options.options.followRedirect || false,
874 | }).then((res) => res.body);
875 | };
876 |
877 | Prerender.middleware.botsOnlyList = botsOnlyList;
878 | Prerender.middleware.userAgentIsBot = userAgentIsBot;
879 |
880 | Prerender.middleware.util = util;
881 |
882 | // for testing only
883 | Prerender.middleware.resetOptions = options.reset.bind(options);
884 | Prerender.middleware.Options = Options;
885 |
886 | // expose got for serverless functions needing http get lib
887 | // in the future this should become a fetch interface so
888 | // the client could inject a global fetch made available by the runtime
889 | // this is deliberately undocumented, do not use unless you are prepared to
890 | // replace on subsequent updates
891 | Prerender.middleware._got = got;
892 |
893 | module.exports = Prerender.middleware;
894 |
--------------------------------------------------------------------------------
/source/lib/api-options.json:
--------------------------------------------------------------------------------
1 | {
2 | "PRERENDER": {
3 | "disableCache": {
4 | "httpName": "prerender-disable-cache",
5 | "openApiType": "boolean"
6 | },
7 | "cacheDuration": {
8 | "httpName": "prerender-cache-duration",
9 | "openApiType": "integer",
10 | "min": 10,
11 | "max": 2592000
12 | },
13 | "recache": {
14 | "httpName": "prerender-recache",
15 | "openApiType": "boolean"
16 | },
17 | "disableAjaxPreload": {
18 | "httpName": "prerender-disable-ajax-preload",
19 | "openApiType": "boolean"
20 | },
21 | "disableAjaxBypass": {
22 | "httpName": "prerender-disable-ajax-bypass",
23 | "openApiType": "boolean"
24 | },
25 | "disableHeadDeDupe": {
26 | "httpName": "prerender-disable-head-dedupe",
27 | "openApiType": "boolean"
28 | },
29 | "waitExtraLong": {
30 | "httpName": "prerender-wait-extra-long",
31 | "openApiType": "boolean"
32 | },
33 | "dontWaitForWebSockets": {
34 | "httpName": "prerender-dont-wait-for-web-sockets",
35 | "openApiType": "boolean"
36 | },
37 | "blockCookies": {
38 | "httpName": "prerender-block-cookies",
39 | "openApiType": "boolean"
40 | },
41 | "originHeaderWhitelist": {
42 | "httpName": "origin-header-whitelist",
43 | "openApiType": "string"
44 | },
45 | "metaOnly": {
46 | "httpName": "prerender-meta-only",
47 | "openApiType": "boolean"
48 | },
49 | "removeScriptTags": {
50 | "httpName": "prerender-remove-script-tags",
51 | "openApiType": "boolean"
52 | },
53 | "removeTrailingSlash": {
54 | "httpName": "prerender-remove-trailing-slash",
55 | "openApiType": "boolean"
56 | },
57 | "followRedirects": {
58 | "httpName": "prerender-follow-redirects",
59 | "openApiType": "boolean"
60 | },
61 | "deviceWidth": {
62 | "httpName": "prerender-device-width",
63 | "openApiType": "integer",
64 | "min": 10,
65 | "max": 10000
66 | },
67 | "deviceHeight": {
68 | "httpName": "prerender-device-height",
69 | "openApiType": "integer",
70 | "min": 10,
71 | "max": 10000
72 | },
73 | "deviceIsMobile": {
74 | "httpName": "prerender-device-is-mobile",
75 | "openApiType": "boolean"
76 | },
77 | "withScreenshot": {
78 | "httpName": "prerender-with-screenshot",
79 | "openApiType": "boolean"
80 | },
81 | "withMetadata": {
82 | "httpName": "prerender-with-metadata",
83 | "openApiType": "boolean"
84 | }
85 | },
86 | "SCREENSHOT": {
87 | "deviceWidth": {
88 | "httpName": "prerender-device-width",
89 | "openApiType": "integer",
90 | "min": 10,
91 | "max": 10000
92 | },
93 | "deviceHeight": {
94 | "httpName": "prerender-device-height",
95 | "openApiType": "integer",
96 | "min": 10,
97 | "max": 10000
98 | },
99 | "deviceIsMobile": {
100 | "httpName": "prerender-device-is-mobile",
101 | "openApiType": "boolean"
102 | },
103 | "viewportQuerySelector": {
104 | "httpName": "prerender-viewport-query-selector",
105 | "openApiType": "string"
106 | },
107 | "viewportQuerySelectorPadding": {
108 | "httpName": "prerender-viewport-query-selector-padding",
109 | "openApiType": "integer",
110 | "min": 0,
111 | "max": 1000
112 | },
113 | "viewportWidth": {
114 | "httpName": "prerender-viewport-width",
115 | "openApiType": "integer",
116 | "min": 1,
117 | "max": 10000
118 | },
119 | "viewportHeight": {
120 | "httpName": "prerender-viewport-height",
121 | "openApiType": "integer",
122 | "min": 1,
123 | "max": 10000
124 | },
125 | "viewportX": {
126 | "httpName": "prerender-viewport-x",
127 | "openApiType": "integer",
128 | "min": 0,
129 | "max": 10000
130 | },
131 | "viewportY": {
132 | "httpName": "prerender-viewport-y",
133 | "openApiType": "integer",
134 | "min": 0,
135 | "max": 10000
136 | },
137 | "viewportScale": {
138 | "httpName": "prerender-viewport-scale",
139 | "openApiType": "float",
140 | "min": 0.1,
141 | "max": 3
142 | },
143 | "format": {
144 | "httpName": "prerender-screenshot-format",
145 | "openApiType": "string",
146 | "openApiEnum": [
147 | "jpeg",
148 | "png",
149 | "webp"
150 | ]
151 | },
152 | "emulatedMedia": {
153 | "httpName": "prerender-emulated-media",
154 | "openApiType": "string",
155 | "openApiEnum": [
156 | "screen",
157 | "print",
158 | "braille",
159 | "embossed",
160 | "handheld",
161 | "projection",
162 | "speech",
163 | "tty",
164 | "tv"
165 | ]
166 | },
167 | "waitExtraLong": {
168 | "httpName": "prerender-wait-extra-long",
169 | "openApiType": "boolean"
170 | },
171 | "dontWaitForWebSockets": {
172 | "httpName": "prerender-dont-wait-for-web-sockets",
173 | "openApiType": "boolean"
174 | },
175 | "blockCookies": {
176 | "httpName": "prerender-block-cookies",
177 | "openApiType": "boolean"
178 | }
179 | },
180 | "PDF": {
181 | "noPageBreaks": {
182 | "httpName": "prerender-pdf-no-page-breaks",
183 | "openApiType": "boolean"
184 | },
185 | "pageRanges": {
186 | "httpName": "prerender-pdf-page-ranges",
187 | "openApiType": "string"
188 | },
189 | "scale": {
190 | "httpName": "prerender-pdf-scale",
191 | "openApiType": "float",
192 | "min": 0.1,
193 | "max": 2
194 | },
195 | "preferCssPageSize": {
196 | "httpName": "prerender-pdf-prefer-css-page-size",
197 | "openApiType": "boolean"
198 | },
199 | "printBackground": {
200 | "httpName": "prerender-pdf-print-background",
201 | "openApiType": "boolean"
202 | },
203 | "landscape": {
204 | "httpName": "prerender-pdf-landscape",
205 | "openApiType": "boolean"
206 | },
207 | "marginTop": {
208 | "httpName": "prerender-pdf-margin-top",
209 | "openApiType": "float",
210 | "min": 0,
211 | "max": 100
212 | },
213 | "marginRight": {
214 | "httpName": "prerender-pdf-margin-right",
215 | "openApiType": "float",
216 | "min": 0,
217 | "max": 100
218 | },
219 | "marginBottom": {
220 | "httpName": "prerender-pdf-margin-bottom",
221 | "openApiType": "float",
222 | "min": 0,
223 | "max": 100
224 | },
225 | "marginLeft": {
226 | "httpName": "prerender-pdf-margin-left",
227 | "openApiType": "float",
228 | "min": 0,
229 | "max": 100
230 | },
231 | "paperWidth": {
232 | "httpName": "prerender-pdf-paper-width",
233 | "openApiType": "float",
234 | "min": 1,
235 | "max": 100
236 | },
237 | "paperHeight": {
238 | "httpName": "prerender-pdf-paper-height",
239 | "openApiType": "float",
240 | "min": 1,
241 | "max": 100
242 | },
243 | "emulatedMedia": {
244 | "httpName": "prerender-emulated-media",
245 | "openApiType": "string",
246 | "openApiEnum": [
247 | "screen",
248 | "print",
249 | "braille",
250 | "embossed",
251 | "handheld",
252 | "projection",
253 | "speech",
254 | "tty",
255 | "tv"
256 | ]
257 | },
258 | "waitExtraLong": {
259 | "httpName": "prerender-wait-extra-long",
260 | "openApiType": "boolean"
261 | },
262 | "dontWaitForWebSockets": {
263 | "httpName": "prerender-dont-wait-for-web-sockets",
264 | "openApiType": "boolean"
265 | },
266 | "blockCookies": {
267 | "httpName": "prerender-block-cookies",
268 | "openApiType": "boolean"
269 | }
270 | },
271 | "SCRAPE": {
272 | "waitExtraLong": {
273 | "httpName": "prerender-wait-extra-long",
274 | "openApiType": "boolean"
275 | },
276 | "dontWaitForWebSockets": {
277 | "httpName": "prerender-dont-wait-for-web-sockets",
278 | "openApiType": "boolean"
279 | },
280 | "blockCookies": {
281 | "httpName": "prerender-block-cookies",
282 | "openApiType": "boolean"
283 | },
284 | "followRedirects": {
285 | "httpName": "prerender-follow-redirects",
286 | "openApiType": "boolean"
287 | },
288 | "deviceWidth": {
289 | "httpName": "prerender-device-width",
290 | "openApiType": "integer",
291 | "min": 10,
292 | "max": 10000
293 | },
294 | "deviceHeight": {
295 | "httpName": "prerender-device-height",
296 | "openApiType": "integer",
297 | "min": 10,
298 | "max": 10000
299 | },
300 | "deviceIsMobile": {
301 | "httpName": "prerender-device-is-mobile",
302 | "openApiType": "boolean"
303 | },
304 | "emulatedMedia": {
305 | "httpName": "prerender-emulated-media",
306 | "openApiType": "string",
307 | "openApiEnum": [
308 | "screen",
309 | "print",
310 | "braille",
311 | "embossed",
312 | "handheld",
313 | "projection",
314 | "speech",
315 | "tty",
316 | "tv"
317 | ]
318 | },
319 | "withScreenshot": {
320 | "httpName": "prerender-with-screenshot",
321 | "openApiType": "boolean"
322 | },
323 | "withMetadata": {
324 | "httpName": "prerender-with-metadata",
325 | "openApiType": "boolean"
326 | }
327 | }
328 | }
--------------------------------------------------------------------------------
/source/lib/got-retries.js:
--------------------------------------------------------------------------------
1 | const DELAY_MS = process.env.NODE_ENV === "test" ? 0 : 1000;
2 |
3 | function delay(ms) {
4 | return new Promise(function (resolve) {
5 | setTimeout(resolve, ms);
6 | });
7 | }
8 |
9 | const isClientTimeout = (err) =>
10 | err.name === "RequestError" && err.code === "ETIMEDOUT";
11 |
12 | module.exports = (got, options, debug) => {
13 | const isRetryableStatusCode = (code) =>
14 | code === 500 || code === 503 || code === 504;
15 |
16 | const isRetryable = (err, retries) =>
17 | retries <= options.options.retries &&
18 | (isClientTimeout(err) ||
19 | (err instanceof got.HTTPError &&
20 | isRetryableStatusCode(err.response.statusCode)));
21 |
22 | class GotGetWithRetry {
23 | constructor(url, options) {
24 | this.url = url;
25 | this.options = options;
26 | this.attempts = 0;
27 | }
28 |
29 | get() {
30 | return new Promise((resolve, reject) => {
31 | const createGet = () => {
32 | this.attempts += 1;
33 | const inst = got(this.url, this.options);
34 | inst.then(resolve).catch((err) => {
35 | // noop because we catch downstream... but if we don't have this, it throws unhandled rejection
36 | });
37 | inst.catch((err) => {
38 | // https://github.com/sindresorhus/got/pull/360#issuecomment-323501098
39 | if (isClientTimeout(err)) {
40 | inst.cancel();
41 | }
42 |
43 | if (!isRetryable(err, this.attempts)) {
44 | return reject(err);
45 | }
46 |
47 | debug("retrying", {
48 | url: this.url,
49 | statusCode:
50 | (err.response && err.response.statusCode) || "client-timeout",
51 | attempts: this.attempts,
52 | });
53 |
54 | const noise = Math.random() * 100;
55 | const ms = (1 << this.attempts) * DELAY_MS + noise;
56 |
57 | delay(ms).then(createGet);
58 | });
59 | };
60 |
61 | createGet();
62 | });
63 | }
64 | }
65 |
66 | got.get = function (url, options) {
67 | return new GotGetWithRetry(url, options).get();
68 | };
69 | };
70 |
--------------------------------------------------------------------------------
/source/lib/options.js:
--------------------------------------------------------------------------------
1 | const LRUCache = require("lru-cache");
2 | const util = require("./util");
3 |
4 | class MiddlewareCache {
5 | constructor(lruCache) {
6 | this.lruCache = lruCache;
7 | }
8 | reset() {
9 | this.lruCache.clear();
10 | }
11 | clear(startsWith) {
12 | if (!startsWith) throw new Error("must pass what cache key startsWith");
13 |
14 | startsWith = startsWith.replace(/^https?/, "");
15 | let httpPath = `http${startsWith}`;
16 | let httpsPath = `https${startsWith}`;
17 |
18 | this.lruCache.forEach(function (v, k, cache) {
19 | if (k.startsWith(httpPath) || k.startsWith(httpsPath)) cache.delete(k);
20 | });
21 | }
22 | set(url, res) {
23 | this.lruCache.set(url, res);
24 | }
25 | get(url) {
26 | return this.lruCache.get(url);
27 | }
28 | }
29 |
30 | let THROTTLED_URLS = {};
31 |
32 | const configureMiddlewareCache = (middlewareCacheSingleton, lruCache) => {
33 | // this prevCache dump/load is just for tests
34 | const prevCache =
35 | middlewareCacheSingleton.instance &&
36 | middlewareCacheSingleton.instance.lruCache.dump();
37 | if (prevCache) lruCache.load(prevCache);
38 |
39 | middlewareCacheSingleton.instance = new MiddlewareCache(lruCache);
40 | };
41 |
42 | module.exports = class Options {
43 | constructor(middlewareCacheSingleton) {
44 | this.middlewareCacheSingleton = middlewareCacheSingleton;
45 | this.reset();
46 | }
47 |
48 | recordFail(url) {
49 | THROTTLED_URLS[url] = new Date();
50 | setTimeout(function () {
51 | THROTTLED_URLS[url] = undefined;
52 | delete THROTTLED_URLS[url];
53 | }, 5 * 60 * 1000);
54 | }
55 |
56 | isThrottled(url) {
57 | if (!this.options.throttleOnFail) return false;
58 | return !!THROTTLED_URLS[url];
59 | }
60 |
61 | reset() {
62 | THROTTLED_URLS = {};
63 | this.options = { retries: 1 };
64 | }
65 |
66 | static get validOptions() {
67 | return [
68 | "timeout",
69 | "prerenderServiceUrl",
70 | "prerenderToken",
71 | "beforeRender",
72 | "afterRender",
73 | "whitelistUserAgents",
74 | "originHeaderWhitelist",
75 | "botsOnly",
76 | "disableServerCache",
77 | "disableAjaxBypass",
78 | "disableAjaxPreload",
79 | "disableHeadDedupe",
80 | "bubbleUp5xxErrors",
81 | "enableMiddlewareCache",
82 | "middlewareCacheMaxBytes",
83 | "middlewareCacheMaxAge",
84 | "whitelistQueryParams",
85 | "shouldPrerender",
86 | "shouldPrerenderAdditionalCheck",
87 | "removeScriptTags",
88 | "removeTrailingSlash",
89 | "protocol",
90 | "retries",
91 | "host",
92 | "waitExtraLong",
93 | "throttleOnFail",
94 | "withScreenshot",
95 | "afterRenderBlocking",
96 | "blacklistPaths",
97 | "whitelistPaths",
98 | "metaOnly",
99 | "withMetadata",
100 | "followRedirects",
101 | "serverCacheDurationSeconds",
102 | "deviceWidth",
103 | "deviceHeight",
104 | ];
105 | }
106 |
107 | set(prerenderMiddleware, name, val) {
108 | if (!Options.validOptions.includes(name))
109 | throw new Error(`${name} is unsupported option`);
110 |
111 | this.options[name] = val;
112 |
113 | if (name === "enableMiddlewareCache" && val === false) {
114 | this.middlewareCacheSingleton.instance = undefined;
115 | } else if (name.match(/middlewareCache/i)) {
116 | const lruCache = new LRUCache({
117 | maxSize: this.options.middlewareCacheMaxBytes || 500000000, // 500MB
118 | sizeCalculation: function (n, key) {
119 | if (n && n.body) {
120 | return n.body.length;
121 | } else if (n.length) {
122 | return n.length;
123 | }
124 |
125 | return 1;
126 | },
127 | dispose: function (key, n) {},
128 | ttl: this.options.middlewareCacheMaxAge || 0, // 0 is forever
129 | });
130 |
131 | configureMiddlewareCache(this.middlewareCacheSingleton, lruCache);
132 | } else if (
133 | name === "whitelistQueryParams" ||
134 | name === "withScreenshot" ||
135 | name === "afterRenderBlocking" ||
136 | name === "blacklistPaths" ||
137 | name === "whitelistPaths" ||
138 | name === "metaOnly" ||
139 | name === "withMetadata"
140 | ) {
141 | if (val != null && !util.isFunction(val)) {
142 | throw new Error(`${name} must be a function`);
143 | }
144 | }
145 |
146 | if (this.options["botsOnly"] && this.options["whitelistUserAgents"])
147 | throw new Error("Can't use both botsOnly and whitelistUserAgents");
148 |
149 | return prerenderMiddleware;
150 | }
151 | };
152 |
--------------------------------------------------------------------------------
/source/lib/our-url.js:
--------------------------------------------------------------------------------
1 | const querystring = require("querystring");
2 | const util = require("./util");
3 | const stdliburl = require("url");
4 |
5 | // http, connect, and express compatible URL parser
6 | class Url {
7 | static parse(req, options) {
8 | const obj = new this(req, options);
9 |
10 | return {
11 | host: obj.host(),
12 | original: obj.original(),
13 | path: obj.path(),
14 | query: obj.query(),
15 | basename: obj.basename(),
16 | hasHtmlPath: obj.hasHtmlPath(),
17 | requestedPath: obj.requestedPath(),
18 | requestedUrl: obj.requestedUrl(),
19 | };
20 | }
21 |
22 | constructor(req, options) {
23 | if (!req) throw new Error("missing req obj");
24 | this.req = req;
25 | this.options = options;
26 | const url = this.original();
27 | if (url) {
28 | this.parsed = stdliburl.parse(url, true); // true for 2nd argument means parse query params
29 | }
30 | }
31 |
32 | protocol() {
33 | if (this.options.options.protocol)
34 | return this.options.options.protocol + ":";
35 |
36 | // http://stackoverflow.com/a/10353248
37 | // https://github.com/expressjs/express/blob/3c54220a3495a7a2cdf580c3289ee37e835c0190/lib/request.js#L301
38 | let protocol =
39 | this.req.connection && this.req.connection.encrypted ? "https" : "http";
40 |
41 | if (this.req.headers && this.req.headers["cf-visitor"]) {
42 | const cfVisitorMatch = this.req.headers["cf-visitor"].match(
43 | /"scheme":"(https|http)"/
44 | );
45 | if (cfVisitorMatch) protocol = cfVisitorMatch[1];
46 | }
47 |
48 | let xForwardedProto =
49 | this.req.headers && this.req.headers["x-forwarded-proto"];
50 | if (xForwardedProto) {
51 | xForwardedProto = xForwardedProto.split(",")[0];
52 | const xForwardedProtoMatch = xForwardedProto.match(/(https|http)/);
53 | if (xForwardedProtoMatch) protocol = xForwardedProtoMatch[1];
54 | }
55 |
56 | return protocol + ":";
57 | }
58 |
59 | host() {
60 | if (this.options.options.host) return this.options.options.host;
61 | return this.req.headers && this.req.headers.host;
62 | }
63 |
64 | original() {
65 | return this.req.prerenderUrl || this.req.originalUrl || this.req.url;
66 | }
67 |
68 | path() {
69 | // in express, this is the same as req.path
70 | return this.parsed && this.parsed.pathname;
71 | }
72 |
73 | // returns {a:b, c:d} if query string exists, else null
74 | query() {
75 | // in express, req.query will return key/val object
76 | // parsed.query returns string: a=b&c=d
77 | return this.parsed && this.parsed.query;
78 | }
79 |
80 | // if the path is /admin/new.html?a=b&c=d, this returns /new.html
81 | basename() {
82 | const path = this.path();
83 | return (path && path.split("/").pop()) || "/";
84 | }
85 |
86 | hasHtmlPath() {
87 | return util.basenameIsHtml(this.basename());
88 | }
89 |
90 | requestedPath() {
91 | if (this.options.options.whitelistQueryParams) {
92 | const whitelistedQueryParams = this.options.options.whitelistQueryParams(
93 | this.req
94 | );
95 |
96 | if (whitelistedQueryParams != null) {
97 | const queryParams = Object.assign({}, this.query());
98 |
99 | Object.keys(queryParams).forEach((key) => {
100 | if (!whitelistedQueryParams.includes(key)) {
101 | delete queryParams[key];
102 | }
103 | });
104 |
105 | const whitelistedQueryString =
106 | (Object.keys(queryParams).length ? "?" : "") +
107 | querystring.stringify(queryParams);
108 |
109 | return this.path() + whitelistedQueryString;
110 | }
111 | }
112 |
113 | return this.original();
114 | }
115 |
116 | requestedUrl() {
117 | return this.protocol() + "//" + this.host() + this.requestedPath();
118 | }
119 | }
120 |
121 | module.exports = Url;
122 |
--------------------------------------------------------------------------------
/source/lib/util.js:
--------------------------------------------------------------------------------
1 | function urlPathIsHtml(urlPath) {
2 | const basename = urlPath.split("/").pop().replace(/\?.*/, ""); // remove query params
3 |
4 | return basenameIsHtml(basename);
5 | }
6 |
7 | function basenameIsHtml(basename) {
8 | if (basename === "") return true;
9 |
10 | // doesn't detect index.whatever.html (multiple dots)
11 | const hasHtmlOrNoExtension = !!basename.match(/^(([^.]|\.html?)+)$/);
12 |
13 | if (hasHtmlOrNoExtension) return true;
14 |
15 | // hack to handle basenames with multiple dots: index.whatever.html
16 | const endsInHtml = !!basename.match(/.html?$/);
17 |
18 | if (endsInHtml) return true;
19 |
20 | // hack to detect extensions that are not HTML so we can handle
21 | // paths with dots in them
22 | const endsInOtherExtension = basename.match(/\.[a-zA-Z0-9]{1,5}$/);
23 | if (!endsInOtherExtension) return true;
24 |
25 | return false;
26 | }
27 |
28 | function isFunction(functionToCheck) {
29 | var getType = {};
30 | return (
31 | functionToCheck &&
32 | getType.toString.call(functionToCheck) === "[object Function]"
33 | );
34 | }
35 |
36 | module.exports = { urlPathIsHtml, basenameIsHtml, isFunction };
37 |
--------------------------------------------------------------------------------
/spec/botsOnlySpec.js:
--------------------------------------------------------------------------------
1 | describe("userAgentIsBot", function () {
2 | withPrerenderMiddleware();
3 | it("detects bot", function () {
4 | expect(
5 | this.prerenderMiddleware.userAgentIsBot({ "user-agent": "googlebot" })
6 | ).toBe(true);
7 | });
8 | it("detects bot with strange case", function () {
9 | expect(
10 | this.prerenderMiddleware.userAgentIsBot({ "user-agent": "goOglebot" })
11 | ).toBe(true);
12 | });
13 | it("detects non-bot", function () {
14 | expect(
15 | this.prerenderMiddleware.userAgentIsBot({ "user-agent": "chrome" })
16 | ).toBe(false);
17 | });
18 | it("detects non-bot for empty user-agent", function () {
19 | expect(this.prerenderMiddleware.userAgentIsBot({})).toBe(false);
20 | });
21 | it("detects escaped fragment", function () {
22 | expect(
23 | this.prerenderMiddleware.userAgentIsBot(
24 | { "user-agent": "chrome" },
25 | "/file?_escaped_fragment_"
26 | )
27 | ).toBe(true);
28 | });
29 |
30 | it("detects x-bufferbot", function () {
31 | expect(
32 | this.prerenderMiddleware.userAgentIsBot(
33 | { "user-agent": "whatever", "x-bufferbot": "true" },
34 | "/"
35 | )
36 | ).toBe(true);
37 | });
38 |
39 | describe("botsOnlyList", function () {
40 | it("exports list", function () {
41 | expect(this.prerenderMiddleware.botsOnlyList.length > 0).toBe(true);
42 | });
43 | it("includes googlebot", function () {
44 | expect(this.prerenderMiddleware.botsOnlyList.includes("googlebot")).toBe(
45 | true
46 | );
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/spec/helpers/helper.js:
--------------------------------------------------------------------------------
1 | const stdliburl = require("url");
2 | const nock = require("nock");
3 |
4 | var prerenderMiddleware;
5 | if (!!process.env.CI) {
6 | console.log("running transpiled code");
7 | prerenderMiddleware = require("../../distribution/index");
8 | } else {
9 | prerenderMiddleware = require("../../source/index");
10 | }
11 |
12 | global.withNock = function () {
13 | afterEach(function () {
14 | nock.cleanAll();
15 | });
16 | beforeEach(function () {
17 | nock.disableNetConnect();
18 | });
19 | };
20 |
21 | global.withPrerenderMiddleware = function () {
22 | beforeEach(function () {
23 | this.prerenderMiddleware = prerenderMiddleware;
24 | this.prerenderMiddleware.resetOptions();
25 | });
26 | };
27 |
28 | global.configureUrlForReq = function (req, options) {
29 | if (req._requestedUrl) {
30 | parsed = stdliburl.parse(req._requestedUrl);
31 | // connect only has: req.headers.host (which includes port), req.url and req.originalUrl
32 | // express has .protocol and .path but we're optimizing for connect
33 | req.headers["host"] = parsed.host;
34 | req.url = parsed.path;
35 | req.originalUrl = parsed.path;
36 | req.method = options.method || "GET";
37 | }
38 | };
39 |
40 | global.withHttpMiddlewareMocks = function () {
41 | withPrerenderMiddleware();
42 | beforeEach(function () {
43 | this.req = {};
44 | this.res = {
45 | writeHead: jasmine.createSpy("writeHead"),
46 | getHeader: jasmine.createSpy("getHeader"),
47 | setHeader: jasmine.createSpy("setHeader"),
48 | };
49 | this.prerenderMiddleware.cache && this.prerenderMiddleware.cache.reset();
50 | this.configurePrerenderMiddleware = function (done, options) {
51 | if (!done) done = () => {};
52 | if (!options) options = {};
53 | this.prerenderMiddleware.set("waitExtraLong", options.waitExtraLong);
54 | this.prerenderMiddleware.set("host", options.host);
55 | this.prerenderMiddleware.set("protocol", options.protocol);
56 | this.prerenderMiddleware.set(
57 | "removeTrailingSlash",
58 | !!options.removeTrailingSlash
59 | );
60 | this.prerenderMiddleware.set(
61 | "removeScriptTags",
62 | !!options.removeScriptTags
63 | );
64 | this.prerenderMiddleware.set(
65 | "disableAjaxBypass",
66 | !!options.disableAjaxBypass
67 | );
68 | this.prerenderMiddleware.set(
69 | "disableAjaxPreload",
70 | !!options.disableAjaxPreload
71 | );
72 | this.prerenderMiddleware.set(
73 | "disableServerCache",
74 | !!options.disableServerCache
75 | );
76 | this.prerenderMiddleware.set(
77 | "disableHeadDedupe",
78 | !!options.disableHeadDedupe
79 | );
80 | this.prerenderMiddleware.set(
81 | "enableMiddlewareCache",
82 | !!options.enableMiddlewareCache
83 | );
84 | this.prerenderMiddleware.set("botsOnly", options.botsOnly);
85 | this.prerenderMiddleware.set(
86 | "bubbleUp5xxErrors",
87 | options.bubbleUp5xxErrors
88 | );
89 | this.prerenderMiddleware.set(
90 | "whitelistUserAgents",
91 | options.whitelistUserAgents
92 | );
93 | this.prerenderMiddleware.set(
94 | "originHeaderWhitelist",
95 | options.originHeaderWhitelist
96 | );
97 | this.prerenderMiddleware.set("withScreenshot", options.withScreenshot);
98 | this.prerenderMiddleware.set("withMetadata", options.withMetadata);
99 | this.prerenderMiddleware.set("beforeRender", options.beforeRender);
100 | this.prerenderMiddleware.set(
101 | "afterRenderBlocking",
102 | options.afterRenderBlocking
103 | );
104 | this.prerenderMiddleware.set("blacklistPaths", options.blacklistPaths);
105 | this.prerenderMiddleware.set("whitelistPaths", options.whitelistPaths);
106 | this.prerenderMiddleware.set("afterRender", options.afterRender);
107 | this.prerenderMiddleware.set("shouldPrerender", options.shouldPrerender);
108 | this.prerenderMiddleware.set(
109 | "whitelistQueryParams",
110 | options.whitelistQueryParams
111 | );
112 | this.prerenderMiddleware.set("metaOnly", options.metaOnly);
113 | this.prerenderMiddleware.set("followRedirects", options.followRedirects);
114 |
115 | this.prerenderMiddleware.set(
116 | "serverCacheDurationSeconds",
117 | options.serverCacheDurationSeconds
118 | );
119 |
120 | if (options.timeout) {
121 | this.prerenderMiddleware.set("timeout", options.timeout);
122 | }
123 |
124 | if (options.retries != null) {
125 | this.prerenderMiddleware.set("retries", options.retries);
126 | }
127 |
128 | this.prerenderMiddleware.set("throttleOnFail", options.throttleOnFail);
129 |
130 | this.next = jasmine.createSpy("nextMiddleware").and.callFake(done);
131 | this.res.end = jasmine.createSpy("end").and.callFake(done);
132 |
133 | configureUrlForReq(this.req, options);
134 | }.bind(this);
135 |
136 | this.callPrerenderMiddleware = function (done, options) {
137 | this.configurePrerenderMiddleware(done, options);
138 | this.prerenderMiddleware(this.req, this.res, this.next);
139 | }.bind(this);
140 | });
141 | };
142 |
--------------------------------------------------------------------------------
/spec/screenshotsAndPdfsSpec.js:
--------------------------------------------------------------------------------
1 | const nock = require("nock");
2 |
3 | describe("screenshots and PDFs", function () {
4 | withNock();
5 | withPrerenderMiddleware();
6 |
7 | function itWorks(prerenderAction) {
8 | describe("happy path", function () {
9 | beforeEach(function () {
10 | const self = this;
11 | nock("https://service.prerender.cloud")
12 | .get(/.*/)
13 | .reply(function (uri) {
14 | self.requestedUri = uri;
15 | self.headersSentToPrerenderCloud = this.req.headers;
16 | return [200, "body"];
17 | });
18 | });
19 |
20 | beforeEach(function (done) {
21 | const self = this;
22 | this.prerenderMiddleware.set("prerenderToken", "fake-token");
23 | this.prerenderMiddleware[prerenderAction]
24 | .call(this.prerenderMiddleware, "http://example.com")
25 | .then((res) => {
26 | self.res = res;
27 | done();
28 | });
29 | });
30 | it("calls correct API", function () {
31 | expect(this.requestedUri).toEqual(
32 | `/${prerenderAction}/http://example.com`
33 | );
34 | });
35 | it("return screenshot", function () {
36 | expect(this.res).toEqual(Buffer.from("body"));
37 | });
38 | it("it sends token", function () {
39 | expect(this.headersSentToPrerenderCloud["x-prerender-token"]).toEqual(
40 | "fake-token"
41 | );
42 | });
43 | });
44 |
45 | describe("service.prerender.cloud returns 502 bad gateway", function () {
46 | beforeEach(function () {
47 | const self = this;
48 | nock("https://service.prerender.cloud")
49 | .get(/.*/)
50 | .reply((uri) => {
51 | self.requestedUri = uri;
52 | return [502, "bad gateway"];
53 | });
54 | });
55 |
56 | beforeEach(function (done) {
57 | const self = this;
58 | this.prerenderMiddleware[prerenderAction]
59 | .call(this.prerenderMiddleware, "http://example.com")
60 | .then((res) => {
61 | self.res = res;
62 | done();
63 | })
64 | .catch((err) => {
65 | // console.log(err);
66 | self.err = err;
67 | done();
68 | });
69 | });
70 | it("calls correct API", function () {
71 | expect(this.requestedUri).toEqual(
72 | `/${prerenderAction}/http://example.com`
73 | );
74 | });
75 | it("does not return res", function () {
76 | expect(this.res).toBe(undefined);
77 | });
78 | it("returns 502", function () {
79 | expect(this.err.statusCode).toEqual(502);
80 | });
81 | });
82 | }
83 |
84 | itWorks("pdf");
85 | itWorks("screenshot");
86 | });
87 |
--------------------------------------------------------------------------------
/spec/support/jasmine.json:
--------------------------------------------------------------------------------
1 | {
2 | "spec_dir": "spec",
3 | "spec_files": [
4 | "**/*[sS]pec.?(m)js"
5 | ],
6 | "helpers": [
7 | "helpers/**/*.?(m)js"
8 | ],
9 | "env": {
10 | "stopSpecOnExpectationFailure": false,
11 | "random": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/spec/throttleOnTimeoutSpec.js:
--------------------------------------------------------------------------------
1 | const nock = require("nock");
2 |
3 | describe("options.throttler", function () {
4 | withPrerenderMiddleware();
5 | beforeEach(function () {
6 | jasmine.clock().install();
7 | });
8 | afterEach(function () {
9 | jasmine.clock().uninstall();
10 | });
11 | beforeEach(function () {
12 | this.options = new this.prerenderMiddleware.Options({});
13 | this.options.set({}, "throttleOnFail", true);
14 | this.options.recordFail("example1");
15 | });
16 |
17 | it("throttles", function () {
18 | expect(this.options.isThrottled("example1")).toBe(true);
19 | expect(this.options.isThrottled("example2")).toBe(false);
20 | });
21 |
22 | describe("after 5 minutes", function () {
23 | beforeEach(function () {
24 | jasmine.clock().tick(60 * 5 * 1000 + 1000);
25 | });
26 | it("does not throttle", function () {
27 | expect(this.options.isThrottled("example1")).toBe(false);
28 | });
29 | });
30 | });
31 |
32 | describe("timeout causes throttling", function () {
33 | withNock();
34 | withHttpMiddlewareMocks();
35 |
36 | function withPrerenderServerResponse(serverStatusCode, serverDelay) {
37 | if (serverDelay == null) serverDelay = 0;
38 | beforeEach(function () {
39 | this.attempts = 0;
40 | this.prerenderServer = nock("https://service.prerender.cloud")
41 | .get(/.*/)
42 | .times(2)
43 | .delay(serverDelay)
44 | .reply(() => {
45 | this.attempts += 1;
46 | return [serverStatusCode, "errmsg"];
47 | });
48 | });
49 | }
50 |
51 | function callMiddleware(clientOptions) {
52 | beforeEach(function (done) {
53 | if (!clientOptions) clientOptions = {};
54 | this.req = {
55 | headers: { "user-agent": "twitterbot/1.0" },
56 | _requestedUrl: "http://example.org/files.m4v.storage/lol-valid",
57 | };
58 |
59 | this.callPrerenderMiddleware(
60 | () => done(),
61 | Object.assign({}, clientOptions)
62 | );
63 | });
64 | }
65 | function itCalledNext() {
66 | it("calls next", function () {
67 | expect(this.next).toHaveBeenCalled();
68 | });
69 | }
70 | function itBubblesUp(statusCode, msg) {
71 | it("bubble the error up", function () {
72 | expect(this.res.writeHead.calls.mostRecent().args[0]).toEqual(
73 | statusCode,
74 | {}
75 | );
76 | expect(this.res.end.calls.mostRecent().args[0]).toMatch(msg);
77 | });
78 | }
79 |
80 | describe("on client timeout", function () {
81 | withPrerenderServerResponse(200, 500);
82 | describe("with throttling and bubbleUp5xxErrors enabled", function () {
83 | callMiddleware({
84 | timeout: 50,
85 | retries: 0,
86 | throttleOnFail: true,
87 | bubbleUp5xxErrors: true,
88 | });
89 | callMiddleware({
90 | timeout: 50,
91 | retries: 0,
92 | throttleOnFail: true,
93 | bubbleUp5xxErrors: true,
94 | });
95 | it("calls the server only once", function () {
96 | expect(this.attempts).toEqual(1);
97 | });
98 | itBubblesUp(
99 | 503,
100 | "Error: headless-render-api.com client throttled this prerender request due to a recent timeout"
101 | );
102 | });
103 | describe("with throttling enabled", function () {
104 | callMiddleware({ timeout: 50, retries: 0, throttleOnFail: true });
105 |
106 | callMiddleware({ timeout: 50, retries: 0, throttleOnFail: true });
107 |
108 | it("calls the server only once", function () {
109 | expect(this.attempts).toEqual(1);
110 | });
111 | itCalledNext();
112 | });
113 | describe("with throttling disabled", function () {
114 | callMiddleware({ timeout: 50, retries: 0, throttleOnFail: false });
115 | callMiddleware({ timeout: 50, retries: 0, throttleOnFail: false });
116 | it("calls the server only once", function () {
117 | expect(this.attempts).toEqual(2);
118 | });
119 | itCalledNext();
120 | });
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/spec/urlSpec.js:
--------------------------------------------------------------------------------
1 | describe("util", function () {
2 | withPrerenderMiddleware();
3 | describe("urlPathIsHtml", function () {
4 | it("detects no extension", function () {
5 | expect(this.prerenderMiddleware.util.urlPathIsHtml("/")).toBe(true);
6 | });
7 | it("detects html", function () {
8 | expect(this.prerenderMiddleware.util.urlPathIsHtml("index.html")).toBe(
9 | true
10 | );
11 | });
12 | it("detects htm", function () {
13 | expect(this.prerenderMiddleware.util.urlPathIsHtml("index.htm")).toBe(
14 | true
15 | );
16 | });
17 | it("detects double dot html", function () {
18 | expect(
19 | this.prerenderMiddleware.util.urlPathIsHtml("index.bak.html")
20 | ).toBe(true);
21 | });
22 | it("does not detect js", function () {
23 | expect(this.prerenderMiddleware.util.urlPathIsHtml("index.js")).toBe(
24 | false
25 | );
26 | });
27 | it("does not detect m4v", function () {
28 | expect(this.prerenderMiddleware.util.urlPathIsHtml("index.m4v")).toBe(
29 | false
30 | );
31 | });
32 |
33 | it("handles miscellaneous dots", function () {
34 | expect(
35 | this.prerenderMiddleware.util.urlPathIsHtml(
36 | "categories/1234;lat=-999999.8888888;lng=12341234.13371337;location=SanFrancisco"
37 | )
38 | ).toBe(true);
39 | });
40 |
41 | it("handles miscellaneous dots and query strings", function () {
42 | expect(
43 | this.prerenderMiddleware.util.urlPathIsHtml(
44 | "/ProximaNova-Bold.woff?cachebuster"
45 | )
46 | ).toBe(false);
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/spec/whitelistQueryParamsSpec.js:
--------------------------------------------------------------------------------
1 | const nock = require("nock");
2 |
3 | describe("whitelist query params", function () {
4 | withNock();
5 | withHttpMiddlewareMocks();
6 |
7 | function withPrerenderServerResponse(serverStatusCode) {
8 | beforeEach(function () {
9 | this.attempts = 0;
10 |
11 | const self = this;
12 | this.prerenderServer = nock("https://service.prerender.cloud")
13 | .get(/.*/)
14 | .reply(function (uri) {
15 | self.headersSentToServer = Object.assign({}, this.req.headers);
16 | self.uriCapturedOnPrerender = uri;
17 | return [
18 | 200,
19 | "prerendered-body",
20 | {
21 | "content-type": "text/html; charset=utf-8",
22 | },
23 | ];
24 | });
25 | });
26 | }
27 |
28 | function callMiddleware(clientOptions) {
29 | beforeEach(function (done) {
30 | if (!clientOptions) clientOptions = {};
31 |
32 | this.callPrerenderMiddleware(
33 | () => done(),
34 | Object.assign({}, clientOptions)
35 | );
36 | });
37 | }
38 |
39 | describe("with non-function", function () {
40 | callMiddleware({ whitelistQueryParams: () => true });
41 |
42 | it("throws", function () {
43 | expect(
44 | function () {
45 | this.prerenderMiddleware.set("whitelistQueryParams", true);
46 | }.bind(this)
47 | ).toThrow();
48 | });
49 | });
50 |
51 | describe("with query params", function () {
52 | beforeEach(function () {
53 | this.req = {
54 | headers: { "user-agent": "twitterbot/1.0" },
55 | _requestedUrl: "http://example.org/?a=b",
56 | };
57 | });
58 | withPrerenderServerResponse(200, 0);
59 | describe("with throttling and bubbleUp5xxErrors enabled", function () {
60 | callMiddleware();
61 |
62 | it("returns pre-rendered content", function () {
63 | expect(this.res.end).toHaveBeenCalledWith("prerendered-body");
64 | });
65 | it("passes query params", function () {
66 | expect(this.uriCapturedOnPrerender).toEqual("/http://example.org/?a=b");
67 | });
68 | });
69 |
70 | describe("with empty whitelistQueryParams", function () {
71 | callMiddleware({ whitelistQueryParams: () => [] });
72 |
73 | it("returns pre-rendered content", function () {
74 | expect(this.res.end).toHaveBeenCalledWith("prerendered-body");
75 | });
76 | it("does not pass any query params", function () {
77 | expect(this.uriCapturedOnPrerender).toEqual("/http://example.org/");
78 | });
79 | });
80 | describe("with present whitelistQueryParams", function () {
81 | callMiddleware({ whitelistQueryParams: () => ["a"] });
82 |
83 | it("returns pre-rendered content", function () {
84 | expect(this.res.end).toHaveBeenCalledWith("prerendered-body");
85 | });
86 | it("does not pass any query params", function () {
87 | expect(this.uriCapturedOnPrerender).toEqual("/http://example.org/?a=b");
88 | });
89 | });
90 | describe("with conflicting whitelistQueryParams", function () {
91 | callMiddleware({ whitelistQueryParams: () => ["b"] });
92 |
93 | it("returns pre-rendered content", function () {
94 | expect(this.res.end).toHaveBeenCalledWith("prerendered-body");
95 | });
96 | it("does not pass any query params", function () {
97 | expect(this.uriCapturedOnPrerender).toEqual("/http://example.org/");
98 | });
99 | });
100 | describe("with partial conflicting whitelistQueryParams", function () {
101 | beforeEach(function () {
102 | this.req = {
103 | headers: { "user-agent": "twitterbot/1.0" },
104 | _requestedUrl: "http://example.org/?a=b&b=c&d=e",
105 | };
106 | });
107 | callMiddleware({ whitelistQueryParams: () => ["b", "d"] });
108 |
109 | it("returns pre-rendered content", function () {
110 | expect(this.res.end).toHaveBeenCalledWith("prerendered-body");
111 | });
112 | it("does not pass any query params", function () {
113 | expect(this.uriCapturedOnPrerender).toEqual(
114 | "/http://example.org/?b=c&d=e"
115 | );
116 | });
117 | it("exposes requested URL on req obj", function () {
118 | // this is for users of the beforeRender(req, done) func
119 | expect(this.req.prerender.url.requestedPath).toEqual("/?b=c&d=e");
120 | });
121 | it("exposes requested URL on res obj", function () {
122 | // this is for lambda@edge downstream: https://github.com/sanfrancesco/prerendercloud-lambda-edge
123 | expect(this.res.prerender.url.requestedPath).toEqual("/?b=c&d=e");
124 | });
125 | });
126 | });
127 | });
128 |
--------------------------------------------------------------------------------
/test/connect.js:
--------------------------------------------------------------------------------
1 | var connect = require('connect');
2 | var http = require('http');
3 |
4 | var app = connect();
5 |
6 | // app.use(require('../distribution/index').set('prerenderToken', 'token'));
7 | app.use(require('../source/index').set('prerenderToken', 'token'));
8 |
9 | app.use((req, res) => {
10 | res.setHeader('Content-Type', 'text/html');
11 | res.end(`
12 |
13 |
16 | `)
17 | })
18 |
19 | http.createServer(app).listen(3000);
20 |
--------------------------------------------------------------------------------
/test/express.js:
--------------------------------------------------------------------------------
1 | var express = require("express");
2 | var app = express();
3 |
4 | // app.use(require('../distribution/index').set('prerenderToken', 'token'));
5 | app.use(
6 | require("../source/index")
7 | .set("disableServerCache", true)
8 | .set("bubbleUp5xxErrors", true)
9 | // .set("enableMiddlewareCache", true)
10 | );
11 |
12 | app.get("/", function(req, res) {
13 | res.send(`
14 |
15 |
22 | `);
23 | });
24 |
25 | const port = process.env.PORT || 3000;
26 | app.listen(port, function() {
27 | console.log(`Example app listening on port ${port}!`);
28 | });
29 |
--------------------------------------------------------------------------------
/test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "compression": "^1.7.1",
4 | "prerendercloud": "1.37.2",
5 | "response-time": "^2.3.2"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/test/pdf.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const prerendercloud = require("../source/index");
4 |
5 | (async () => {
6 | const outDir = path.join(__dirname, "out/pdf");
7 | fs.mkdirSync(outDir, { recursive: true });
8 |
9 | // test 1
10 | const pdf = await prerendercloud.pdf("http://example.com");
11 | fs.writeFileSync(path.join(outDir, "test1.pdf"), pdf);
12 |
13 | // test 2
14 | const pdf2 = await prerendercloud.pdf("http://example.com", {
15 | noPageBreaks: true,
16 | scale: 2,
17 | });
18 | fs.writeFileSync(path.join(outDir, "test2.pdf"), pdf2);
19 |
20 | // test 3
21 | const pdf3 = await prerendercloud.pdf("http://example.com", {
22 | marginTop: 0.1,
23 | marginRight: 0.1,
24 | marginBottom: 0.1,
25 | marginLeft: 0.1,
26 | paperWidth: 5,
27 | paperHeight: 3,
28 | pageRanges: "1",
29 | emulatedMedia: "screen",
30 | });
31 | fs.writeFileSync(path.join(outDir, "test3.pdf"), pdf3);
32 | })();
33 |
--------------------------------------------------------------------------------
/test/scrape.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const prerendercloud = require("../source/index");
4 |
5 | (async () => {
6 | const outDir = path.join(__dirname, "out/scrape");
7 | fs.mkdirSync(outDir, { recursive: true });
8 |
9 | // test 1
10 | const { body: body1 } = await prerendercloud.scrape("http://example.com");
11 | fs.writeFileSync(path.join(outDir, "test1-body.html"), body1);
12 |
13 | // test 2
14 | const { body: body2, screenshot: screenshot2 } = await prerendercloud.scrape(
15 | "http://example.com",
16 | {
17 | withScreenshot: true,
18 | }
19 | );
20 | fs.writeFileSync(path.join(outDir, "test2-with-screenshot.png"), screenshot2);
21 | fs.writeFileSync(path.join(outDir, "test2-body.html"), body2);
22 |
23 | // test 3
24 | const {
25 | body: body3,
26 | screenshot: screenshot3,
27 | meta: meta3,
28 | links: link3,
29 | headers: headers3,
30 | statusCode: statusCode3,
31 | } = await prerendercloud.scrape("http://example.com", {
32 | withScreenshot: true,
33 | withMetadata: true,
34 | });
35 | if (statusCode3 !== 200) {
36 | throw new Error(`Expected statusCode to be 200, but got ${statusCode3}`);
37 | } else {
38 | console.log("test3 withMetadata statusCode passed");
39 | }
40 | if (Object.keys(headers3).length === 0) {
41 | throw new Error(`Expected headers to be non-empty, but got ${headers3}`);
42 | } else {
43 | console.log("test3 withMetadata headers passed");
44 | }
45 |
46 | const expectedLinks = ["https://www.iana.org/domains/example"];
47 | const expectedMeta = {
48 | title: "Example Domain",
49 | h1: "Example Domain",
50 | description: null,
51 | ogImage: null,
52 | ogTitle: null,
53 | ogDescription: null,
54 | ogType: null,
55 | twitterCard: null,
56 | };
57 | if (JSON.stringify(link3) !== JSON.stringify(expectedLinks)) {
58 | throw new Error(
59 | `Expected links to be ${JSON.stringify(
60 | expectedLinks
61 | )}, but got ${JSON.stringify(link3)}`
62 | );
63 | } else {
64 | console.log("test3 withMetadata links passed");
65 | }
66 | if (JSON.stringify(meta3) !== JSON.stringify(expectedMeta)) {
67 | throw new Error(
68 | `Expected meta to be ${JSON.stringify(
69 | expectedMeta
70 | )}, but got ${JSON.stringify(meta3)}`
71 | );
72 | } else {
73 | console.log("test3 withMetadata meta passed");
74 | }
75 |
76 | fs.writeFileSync(path.join(outDir, "test3-with-screenshot.png"), screenshot3);
77 | fs.writeFileSync(path.join(outDir, "test3-body.html"), body3);
78 | })();
79 |
--------------------------------------------------------------------------------
/test/screenshot.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const prerendercloud = require("../source/index");
4 |
5 | (async () => {
6 | const outDir = path.join(__dirname, "out/screenshot");
7 | fs.mkdirSync(outDir, { recursive: true });
8 |
9 | // test1
10 | const screenshot1 = await prerendercloud.screenshot("http://example.com");
11 | fs.writeFileSync(path.join(outDir, "test1.png"), screenshot1);
12 |
13 | // test 2
14 | const screenshot2 = await prerendercloud.screenshot("http://example.com", {
15 | format: "webp",
16 | viewportScale: 3,
17 | viewportX: 300,
18 | viewportY: 50,
19 | viewportWidth: 765,
20 | viewportHeight: 330,
21 | });
22 | fs.writeFileSync(path.join(outDir, "test2.webp"), screenshot2);
23 |
24 | // test 3
25 | const screenshot3 = await prerendercloud.screenshot("http://example.com", {
26 | format: "jpeg",
27 | viewportScale: 1.5,
28 | });
29 | fs.writeFileSync(path.join(outDir, "test3.jpeg"), screenshot3);
30 | })();
31 |
--------------------------------------------------------------------------------
/test/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@sindresorhus/is@^0.6.0":
6 | version "0.6.0"
7 | resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.6.0.tgz#383f456b26bc96c7889f0332079f4358b16c58dc"
8 | integrity sha1-OD9Faya8lseInwMyB59DWLFsWNw=
9 |
10 | accepts@~1.3.4:
11 | version "1.3.4"
12 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f"
13 | dependencies:
14 | mime-types "~2.1.16"
15 | negotiator "0.6.1"
16 |
17 | bytes@3.0.0:
18 | version "3.0.0"
19 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
20 |
21 | compressible@~2.0.11:
22 | version "2.0.12"
23 | resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.12.tgz#c59a5c99db76767e9876500e271ef63b3493bd66"
24 | dependencies:
25 | mime-db ">= 1.30.0 < 2"
26 |
27 | compression@^1.7.1:
28 | version "1.7.1"
29 | resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.1.tgz#eff2603efc2e22cf86f35d2eb93589f9875373db"
30 | dependencies:
31 | accepts "~1.3.4"
32 | bytes "3.0.0"
33 | compressible "~2.0.11"
34 | debug "2.6.9"
35 | on-headers "~1.0.1"
36 | safe-buffer "5.1.1"
37 | vary "~1.1.2"
38 |
39 | core-util-is@~1.0.0:
40 | version "1.0.2"
41 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
42 | integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
43 |
44 | debug@2.6.9, debug@^2.2.0:
45 | version "2.6.9"
46 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
47 | dependencies:
48 | ms "2.0.0"
49 |
50 | decompress-response@^3.3.0:
51 | version "3.3.0"
52 | resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
53 | integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=
54 | dependencies:
55 | mimic-response "^1.0.0"
56 |
57 | depd@~1.1.0:
58 | version "1.1.1"
59 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
60 |
61 | duplexer3@^0.1.4:
62 | version "0.1.4"
63 | resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
64 | integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
65 |
66 | from2@^2.1.1:
67 | version "2.3.0"
68 | resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
69 | integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=
70 | dependencies:
71 | inherits "^2.0.1"
72 | readable-stream "^2.0.0"
73 |
74 | get-stream@^3.0.0:
75 | version "3.0.0"
76 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
77 | integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
78 |
79 | got-lite@^8.0.1:
80 | version "8.0.1"
81 | resolved "https://registry.yarnpkg.com/got-lite/-/got-lite-8.0.1.tgz#a1dad4b478ff00dce74e35957c680e4c8cf0111a"
82 | integrity sha512-XMRuTVTiQzkPrjsDu+1GF/SoYusUdPXjRTuuiGBX2mqE4djU4aW9ea7E7p6kosCFNvLYW5bHmVvYT7J7WEmplQ==
83 | dependencies:
84 | "@sindresorhus/is" "^0.6.0"
85 | decompress-response "^3.3.0"
86 | duplexer3 "^0.1.4"
87 | get-stream "^3.0.0"
88 | into-stream "^3.1.0"
89 | is-retry-allowed "^1.1.0"
90 | isurl "^1.0.0-alpha5"
91 | lowercase-keys "^1.0.0"
92 | mimic-response "^1.0.0"
93 | p-cancelable "^0.3.0"
94 | p-timeout "^1.2.0"
95 | pify "^3.0.0"
96 | safe-buffer "^5.1.1"
97 | timed-out "^4.0.1"
98 | url-parse-lax "^3.0.0"
99 | url-to-options "^1.0.1"
100 |
101 | has-symbol-support-x@^1.4.1:
102 | version "1.4.2"
103 | resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455"
104 | integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==
105 |
106 | has-to-string-tag-x@^1.2.0:
107 | version "1.4.1"
108 | resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d"
109 | integrity sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==
110 | dependencies:
111 | has-symbol-support-x "^1.4.1"
112 |
113 | inherits@^2.0.1, inherits@~2.0.3:
114 | version "2.0.3"
115 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
116 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
117 |
118 | into-stream@^3.1.0:
119 | version "3.1.0"
120 | resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6"
121 | integrity sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=
122 | dependencies:
123 | from2 "^2.1.1"
124 | p-is-promise "^1.1.0"
125 |
126 | is-object@^1.0.1:
127 | version "1.0.1"
128 | resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
129 | integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA=
130 |
131 | is-retry-allowed@^1.1.0:
132 | version "1.1.0"
133 | resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
134 | integrity sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=
135 |
136 | isarray@~1.0.0:
137 | version "1.0.0"
138 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
139 | integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
140 |
141 | isurl@^1.0.0-alpha5:
142 | version "1.0.0"
143 | resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67"
144 | integrity sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==
145 | dependencies:
146 | has-to-string-tag-x "^1.2.0"
147 | is-object "^1.0.1"
148 |
149 | lowercase-keys@^1.0.0:
150 | version "1.0.1"
151 | resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
152 | integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
153 |
154 | lru-cache@^4.0.2:
155 | version "4.1.5"
156 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
157 | integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
158 | dependencies:
159 | pseudomap "^1.0.2"
160 | yallist "^2.1.2"
161 |
162 | "mime-db@>= 1.30.0 < 2":
163 | version "1.31.0"
164 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.31.0.tgz#a49cd8f3ebf3ed1a482b60561d9105ad40ca74cb"
165 |
166 | mime-db@~1.30.0:
167 | version "1.30.0"
168 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
169 |
170 | mime-types@~2.1.16:
171 | version "2.1.17"
172 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
173 | dependencies:
174 | mime-db "~1.30.0"
175 |
176 | mimic-response@^1.0.0:
177 | version "1.0.1"
178 | resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
179 | integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
180 |
181 | ms@2.0.0:
182 | version "2.0.0"
183 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
184 |
185 | negotiator@0.6.1:
186 | version "0.6.1"
187 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
188 |
189 | on-headers@~1.0.1:
190 | version "1.0.1"
191 | resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
192 |
193 | p-cancelable@^0.3.0:
194 | version "0.3.0"
195 | resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
196 | integrity sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==
197 |
198 | p-finally@^1.0.0:
199 | version "1.0.0"
200 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
201 | integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
202 |
203 | p-is-promise@^1.1.0:
204 | version "1.1.0"
205 | resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
206 | integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=
207 |
208 | p-timeout@^1.2.0:
209 | version "1.2.1"
210 | resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.1.tgz#5eb3b353b7fce99f101a1038880bb054ebbea386"
211 | integrity sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=
212 | dependencies:
213 | p-finally "^1.0.0"
214 |
215 | pify@^3.0.0:
216 | version "3.0.0"
217 | resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
218 | integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
219 |
220 | prepend-http@^2.0.0:
221 | version "2.0.0"
222 | resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
223 | integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
224 |
225 | prerendercloud@1.37.2:
226 | version "1.37.2"
227 | resolved "https://registry.yarnpkg.com/prerendercloud/-/prerendercloud-1.37.2.tgz#39bce56db1aef2e48ba1b86e28b28a122286c626"
228 | integrity sha512-b1r9eugKwlDxN8/rNzfQbxZ5bJgdiz+wlh9X4k7NZVr3bsJjanCAkh0YIj2TsemXv1XH+97uhgX98P0lHwkUpQ==
229 | dependencies:
230 | debug "^2.2.0"
231 | got-lite "^8.0.1"
232 | lru-cache "^4.0.2"
233 | vary "^1.1.1"
234 |
235 | process-nextick-args@~2.0.0:
236 | version "2.0.0"
237 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
238 | integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
239 |
240 | pseudomap@^1.0.2:
241 | version "1.0.2"
242 | resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
243 | integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
244 |
245 | readable-stream@^2.0.0:
246 | version "2.3.6"
247 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
248 | integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
249 | dependencies:
250 | core-util-is "~1.0.0"
251 | inherits "~2.0.3"
252 | isarray "~1.0.0"
253 | process-nextick-args "~2.0.0"
254 | safe-buffer "~5.1.1"
255 | string_decoder "~1.1.1"
256 | util-deprecate "~1.0.1"
257 |
258 | response-time@^2.3.2:
259 | version "2.3.2"
260 | resolved "https://registry.yarnpkg.com/response-time/-/response-time-2.3.2.tgz#ffa71bab952d62f7c1d49b7434355fbc68dffc5a"
261 | dependencies:
262 | depd "~1.1.0"
263 | on-headers "~1.0.1"
264 |
265 | safe-buffer@5.1.1:
266 | version "5.1.1"
267 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
268 |
269 | safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
270 | version "5.1.2"
271 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
272 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
273 |
274 | string_decoder@~1.1.1:
275 | version "1.1.1"
276 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
277 | integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
278 | dependencies:
279 | safe-buffer "~5.1.0"
280 |
281 | timed-out@^4.0.1:
282 | version "4.0.1"
283 | resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
284 | integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=
285 |
286 | url-parse-lax@^3.0.0:
287 | version "3.0.0"
288 | resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
289 | integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=
290 | dependencies:
291 | prepend-http "^2.0.0"
292 |
293 | url-to-options@^1.0.1:
294 | version "1.0.1"
295 | resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
296 | integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=
297 |
298 | util-deprecate@~1.0.1:
299 | version "1.0.2"
300 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
301 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
302 |
303 | vary@^1.1.1, vary@~1.1.2:
304 | version "1.1.2"
305 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
306 |
307 | yallist@^2.1.2:
308 | version "2.1.2"
309 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
310 | integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
311 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // Change this to match your project
3 | "include": ["source/**/*"],
4 | "compilerOptions": {
5 | // Tells TypeScript to read JS files, as
6 | // normally they are ignored as source files
7 | "allowJs": true,
8 | // Generate d.ts files
9 | "declaration": true,
10 | // This compiler run should
11 | // only output d.ts files
12 | "emitDeclarationOnly": true,
13 | // Types should go into this directory.
14 | // Removing this would place the .d.ts files
15 | // next to the .js files
16 | "outDir": "distribution",
17 | // go to js file when using IDE functions like
18 | // "Go to Definition" in VSCode
19 | "declarationMap": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------