├── .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 | ![Github Actions CI](https://github.com/sanfrancesco/prerendercloud-nodejs/actions/workflows/node.js.yml/badge.svg) 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 `<meta>` tags in the `<head>` 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 | <a id="followredirects"></a> 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 | <a name="disableajaxbypass"></a> 627 | <a id="disableajaxbypass"></a> 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 | <a name="disableajaxpreload"></a> 640 | <a id="disableajaxpreload"></a> 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 | <a name="disableheaddedupe"></a> 653 | <a id="disableheaddedupe"></a> 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 | <a name="originheaderwhitelist"></a> 666 | <a id="originheaderwhitelist"></a> 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 | <a name="removescripttags"></a> 679 | <a id="removescripttags"></a> 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 | <a name="removetrailingslash"></a> 691 | <a id="removetrailingslash"></a> 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 | <a name="waitextralong"></a> 712 | <a id="waitextralong"></a> 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 | <a id="withmetadata"></a> 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 | <a id="withscreenshot"></a> 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 | <a id="devicewidth"></a> 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 | <a id="deviceheight"></a> 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 | <a name="middleware-options"></a> 780 | <a id="middleware-options"></a> 781 | 782 | ### Middleware Options 783 | 784 | <a name="host"></a> 785 | <a id="host"></a> 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 | <a name="protocol"></a> 797 | <a id="protocol"></a> 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 | <a name="whitelistqueryparams"></a> 809 | <a id="whitelistqueryparams"></a> 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 | <a id="afterrenderblocking-executes-before-afterrender"></a> 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 | "<meta property='og:image' content='/og.jpg' /></head>" 857 | ); 858 | } 859 | 860 | next(); 861 | }); 862 | ``` 863 | 864 | <a name="afterrender-a-noop-caching-analytics"></a> 865 | <a id="afterrender-a-noop-caching-analytics-executes-after-afterrenderblocking"></a> 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 | <a name="bubbleup5xxerrors"></a> 881 | <a id="bubbleup5xxerrors"></a> 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 | <a name="retries"></a> 918 | <a id="retries"></a> 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 | <a name="throttleonfail"></a> 930 | <a id="throttleonfail"></a> 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 | <a name="how-errors-from-the-server-serviceprerendercloud-are-handled"></a> 948 | <a id="how-errors-from-the-server-serviceheadless-render-apicom-are-handled"></a> 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 <support@headless-render-api.com> (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<Buffer>; 25 | 26 | function prerender(url: string, params?: any): Promise<Buffer>; 27 | 28 | function resetOptions(): any; 29 | 30 | function scrape(url: string, options?: ScrapeOptions): Promise<ScrapeResult>; 31 | 32 | function screenshot( 33 | url: string, 34 | options?: ScreenshotOptions 35 | ): Promise<Buffer>; 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 | <div id='root'></div> 13 | <script type='text/javascript'> 14 | document.getElementById('root').innerHTML = "hello"; 15 | </script> 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 | <div id='root'></div> 15 | <script type='text/javascript'> 16 | const el = document.createElement('meta'); 17 | el.setAttribute('name', 'prerender-status-code'); 18 | el.setAttribute('content', '404'); 19 | document.head.appendChild(el); 20 | document.getElementById('root').innerHTML = "hello"; 21 | </script> 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 | --------------------------------------------------------------------------------