[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"extends\": [\"next\", \"prettier\"],\n  \"plugins\": [],\n  \"root\": true,\n  \"rules\": {}\n}\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: pastelsky\nopen_collective: bundlephobia\nko_fi: # Replace with a single Ko-fi username\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/1-package-build-failure---inaccurate-sizes.md",
    "content": "---\nname: Package Build failure / inaccurate sizes\nabout: Unexpected failures that are not explained by the FAQ's page - https://github.com/pastelsky/bundlephobia#faq\ntitle: '<error-name>: <package-name> fails to <condition>'\nlabels: ''\nassignees: pastelsky\n---\n\n## Package name\n\n### Entire (stringified) error that I see in my browser console\n\n```\n\n```\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/2-similar-package-suggestion.md",
    "content": "---\nname: Similar package suggestion\nabout: Suggest a better alternative to a popular package\ntitle: 'Package suggestion: <alternative> for <package / category>'\nlabels: similar suggestion\nassignees: ''\n---\n\n**Package name**\n\n**Alternative to**\n\n<!-- Name popular package(s) this package is an alternative to. -->\n\n**Quality check**\n\n<!-- All of the below factors are necessary for acceptance -->\n\n- [ ] Package has sufficient overlap in functionality to act as a replacement.\n- [ ] Package is actively maintained, and/or stable for use.\n- [ ] Package has at least 1000 weekly downloads on NPM or is relatively popular on GitHub.\n- [ ] This package is a better alternative to what is already suggested for this category (please explain why), or the category is new.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/3-feature-request---improvement.md",
    "content": "---\nname: Feature request / Improvement\nabout: Suggest an idea for this project\ntitle: ''\nlabels: Improvement\nassignees: ''\n---\n\n**Please describe the feature/suggestion**\n\n**Describe the solution you'd like**\n\n**Describe any alternatives you've considered**\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/4-bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n---\n\n**Describe the bug**\n\n**To Reproduce**\n\n**Expected behavior**\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions\n\nname: CI\n\non:\n  push:\n    branches: [bundlephobia]\n  pull_request:\n    branches: [bundlephobia]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [24.x]\n\n    steps:\n      - uses: actions/checkout@v2\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v3\n        with:\n          node-version: ${{ matrix.node-version }}\n      - name: Enable Corepack\n        run: corepack enable\n      - name: Install project dependencies\n        run: yarn install\n      - name: Run lint\n        run: yarn lint\n      - run: yarn build\n        env:\n          CI: true\n"
  },
  {
    "path": ".gitignore",
    "content": ".next\n.env\nbuild\nout\nnow.json\n\n# package directories\nnode_modules\n\n# Serverless directories\n.serverless\n\n# Logs\nyarn-error.log\nlogs\n**/.yarn/cache\n**/.yarn/install-state.gz\n\n# JetBrains IDE\n.idea\n\n# TypeScript\ntsconfig.tsbuildinfo\nnext-env.d.ts\n\n# keys\nscripts/keys\n\n# Maintenance scripts and logs\nmaintenance_logs/\ntop-packages.json\npopulate-v3-*.json\n"
  },
  {
    "path": ".npmrc",
    "content": "registry=https://registry.npmjs.org\n"
  },
  {
    "path": ".nvmrc",
    "content": "v24.13.0\n"
  },
  {
    "path": ".prettierignore",
    "content": ".next\nbin\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - '24'\n"
  },
  {
    "path": ".yarnrc.yml",
    "content": "enableInlineBuilds: true\n\nnodeLinker: node-modules\n\nnpmRegistryServer: 'https://registry.npmjs.org'\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "Thanks for looking to help 👋. Have a nice time contributing to bundlephobia.\nIf you've any queries regarding setup or contributing, feel free to open an issue.\nI'll try my best to answer as soon as I can.\n\nNote: This repository only contains the frontend, and the request server.\nIf you're looking to make changes to the core logic – building of packages and size calculation, you need to look here instead - [package-build-stats](https://github.com/pastelsky/package-build-stats)\n\n## Running locally\n\n### Adding the necessary keys (Optional)\n\nAdd a `.env` file to the root with Algolia credentials. The server should still run without this, but some features might be disabled.\n\n```ini\n# App Id for NPM Registry\nALGOLIA_APP_ID=OFCNCOG2CU\n\n# API Key\nALGOLIA_API_KEY=<api-key-obtained-from-algolia>\n```\n\nIn addition, one can specify -\n\n```ini\nBUILD_SERVICE_ENDPOINT=<endpoint-to-service>\n```\n\nIn the absence of such an endpoint, packages will be built locally using the [`getPackageStats` function](https://github.com/pastelsky/package-build-stats)\nand\n\n```ini\nCACHE_SERVICE_ENDPOINT=<endpoint-to-service>\n\nFIREBASE_API_KEY=<apiKey>\nFIREBASE_AUTH_DOMAIN=<domain>\nFIREBASE_DATABASE_URL=<url>\n```\n\nfor caching to work (optional).\n\n### Canvas compile issues\n\nBundlephobia relies on [`canvas`](https://www.npmjs.com/package/canvas) which may need to be built from source (depending on your platform). If so, [install the required packages listed in their docs](https://github.com/Automattic/node-canvas#compiling).\n\n### Commands\n\n| script           | description                        |\n| ---------------- | :--------------------------------- |\n| `yarn run dev`   | Start a development server locally |\n| `yarn run build` | Build for production               |\n| `yarn run prod`  | Start a production server locally  |\n"
  },
  {
    "path": "ISSUE_TEMPLATE.md",
    "content": "## Type\n\n<!-- Bug / Feature Request -->\n\n## Package name\n\n### Entire error (stringified) I see in my browser console\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Shubham Kanodia\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <img src=\"https://cdn.rawgit.com/pastelsky/bundlephobia/bundlephobia/client/assets/site-logo.svg\" alt=\"\" width=\"290\" height=\"235\" />\n</p>\n<p align=\"center\">\n  <img src=\"https://img.shields.io/npm/v/package-build-stats.svg\" />\n  <img src=\"https://img.shields.io/npm/l/package-build-stats.svg\" />\n  <a href=\"https://discord.gg/trbWvVet44\">\n    <img src=\"https://badgen.net/badge/icon/discord?icon=discord&label\"/>\n  </a>\n</p>\n<p align=\"center\">\n  <a href=\"https://bundlephobia.com\"> bundlephobia.com </a> <br />\n</p>\n<p align=\"center\">\n  Know the performance impact of including an npm package in your app's bundle.\n</p>\n\n<p align=\"center\">\n    <b><a href=\"https://github.com/pastelsky/bundlephobia/issues/683\"> Bundlephobia's looking for contributors and co-maintainers </a> </b>\n</p>\n\n## Features\n\n- Works with ES6 packages\n- Can build css and scss packages as well (beta)\n- Reports historical trends\n- See package composition\n\n## Badges\n\n- [badgen.net](https://badgen.net/#bundlephobia) - example size of react: ![react](https://badgen.net/bundlephobia/minzip/react)\n- [shields.io](https://shields.io/#/examples/size) - example size of react: ![react](https://img.shields.io/bundlephobia/minzip/react.svg)\n\n## Built using bundlephobia\n\n- Size in browser - As seen on package searches at [yarnpkg.com](https://yarnpkg.com)\n- [bundlephobia-cli](https://github.com/AdrieanKhisbe/bundle-phobia-cli) - A Command Line client for bundlephobia\n- [importcost](https://atom.io/packages/importcost) - An Atom plugin to display size of imported packages\n- [JS Bundle Size Cross-Browser Extension](https://github.com/vicrazumov/js-bundle-size) - Chrome and Firefox extension automatically adding package size to the github and npm pages.\n- [npmcharts.com](https://npmcharts.com/compare/bundle-phobia-cli) - bundle size stats at top of page\n- [Rollpkg](https://github.com/rafgraph/rollpkg) - A build tool to create packages with Rollup and TypeScript\n\n## Support\n\nLiked bundlephobia? Used it's API to build something cool? Let us know!\n\nWe could use some 💛 and sponsorship on –\n\n<a href=\"https://github.com/sponsors/pastelsky\">\n  <img src=\"https://opencollective.com/bundlephobia/tiers/backer.svg\"/>\n</a>\n\n## FAQ\n\n#### 1. Why does search for package X throw `MissingDependencyError` ?\n\nThis error is thrown if a package `require`s a dependency without adding it in its dependencies or peerDependencies list. In the absence of such a definition, we cannot reliably report the size of the package - since we cannot resolve any information about the package.\n\nIn such a case, it's best to report an issue with the package author asking the missing package to be added to its `package.json`\n\n#### 2. I see a `BuildError` for package X, but I'm not sure why.\n\nYou can see a detailed stack trace in your devtools console, and [open an issue](https://github.com/pastelsky/bundlephobia/issues/new) with the relevant details. Working on a more ideal solution for this.\n\n## Contributing\n\nSee [Contributing](https://github.com/pastelsky/bundlephobia/blob/bundlephobia/CONTRIBUTING.md)\n\n## Sponsors\n\n<a href=\"https://www.digitalocean.com?utm_medium=opensource&utm_source=bundlephobia\"><img width=\"100px\" src=\"https://upload.wikimedia.org/wikipedia/commons/f/ff/DigitalOcean_logo.svg\"/></a>\n"
  },
  {
    "path": "__tests__/errors-cache.test.js",
    "content": "const fetch = require('node-fetch')\n\nconst baseURL = 'http://127.0.0.1:5000/api/size?package='\n\ndescribe('build api', () => {\n  beforeEach(function () {\n    jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000\n  })\n\n  it('builds correct packages', async done => {\n    const resultURL = baseURL + 'react@16.5.0'\n    const result = await fetch(resultURL)\n    const errorJSON = await result.json()\n\n    expect(result.status).toBe(200)\n    expect(result.headers.get('cache-control')).toBe('max-age=86400')\n\n    expect(errorJSON).toEqual({\n      scoped: false,\n      name: 'react',\n      version: '16.5.0',\n      description:\n        'React is a JavaScript library for building user interfaces.',\n      repository: 'https://github.com/facebook/react',\n      dependencyCount: 4,\n      hasJSNext: false,\n      hasJSModule: false,\n      hasSideEffects: true,\n      size: 5951,\n      gzip: 2528,\n      dependencySizes: [{ name: 'react', approximateSize: 5957 }],\n    })\n\n    done()\n  })\n\n  it('handles hash bang in the beginning of packages', async done => {\n    const resultURL = baseURL + '@bundlephobia/test-build-error'\n    const result = await fetch(resultURL)\n    const resultJSON = await result.json()\n\n    expect(result.status).toBe(200)\n    expect(result.headers.get('cache-control')).toBe('max-age=86400')\n\n    expect(resultJSON.size).toBe(183)\n    expect(resultJSON.gzip).toBe(153)\n\n    done()\n  })\n\n  it('gives right error messages on when trying to build blocklisted packages', async done => {\n    const resultURL = baseURL + 'polymer-cli'\n    const result = await fetch(resultURL)\n    const errorJSON = await result.json()\n\n    expect(result.status).toBe(403)\n    expect(result.headers.get('cache-control')).toBe('max-age=60')\n\n    expect(errorJSON.error.code).toBe('BlocklistedPackageError')\n    expect(errorJSON.error.message).toBe(\n      'The package you were looking for is blocklisted due to suspicious activity in the past'\n    )\n\n    done()\n  })\n\n  it('gives right error messages on when trying to build entry point error ', async done => {\n    const resultURL = baseURL + '@bundlephobia/test-entry-point-error'\n    const result = await fetch(resultURL)\n    const errorJSON = await result.json()\n\n    expect(result.status).toBe(500)\n    expect(result.headers.get('cache-control')).toBe('max-age=3600')\n\n    expect(errorJSON.error.code).toBe('EntryPointError')\n    expect(errorJSON.error.message).toBe(\n      \"We could not guess a valid entry point for this package. Perhaps the author hasn't specified one in its package.json ?\"\n    )\n\n    done()\n  })\n\n  it('ignores errors when trying to build packages with missing dependency errors', async done => {\n    const resultURL = baseURL + '@bundlephobia/missing-dependency-error'\n    const result = await fetch(resultURL)\n    const resultJSON = await result.json()\n\n    expect(result.status).toBe(200)\n    expect(result.headers.get('cache-control')).toBe('max-age=86400')\n\n    expect(resultJSON.size).toBe(243)\n    expect(resultJSON.gzip).toBe(178)\n    expect(resultJSON.ignoredMissingDependencies).toStrictEqual([\n      'missing-package',\n    ])\n    done()\n  })\n\n  it(\"gives right error messages on when trying to build packages that don't exist\", async done => {\n    const resultURL = baseURL + '@bundlephobia/does-not-exist'\n    const result = await fetch(resultURL)\n    const errorJSON = await result.json()\n\n    expect(result.status).toBe(404)\n    expect(result.headers.get('cache-control')).toBe('max-age=60')\n\n    expect(errorJSON.error.code).toBe('PackageNotFoundError')\n    expect(errorJSON.error.message).toBe(\n      \"The package you were looking for doesn't exist.\"\n    )\n\n    done()\n  })\n\n  it(\"gives right error messages on when trying to build packages versions that don't exist\", async done => {\n    const resultURL = baseURL + '@bundlephobia/test-entry-point-error@459.0.0'\n    const result = await fetch(resultURL)\n    const errorJSON = await result.json()\n\n    expect(result.status).toBe(404)\n    expect(result.headers.get('cache-control')).toBe('max-age=60')\n    expect(errorJSON.error.code).toBe('PackageVersionMismatchError')\n\n    done()\n  })\n})\n"
  },
  {
    "path": "__tests__/stress-test.sh",
    "content": "(curl localhost:5000/api/size?package=react && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=preact && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=d3 && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=c3 && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=inferno && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=react-router && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=localforage && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=request && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=lodash && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=moment && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=antd && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=vue && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=react-clipboard && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=react-ink && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=glamorous && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=preact-compat && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=inferno-compat && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=immutable && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=redux && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=mobx && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=mobx-react && echo '\\n') &\nsleep 3\n(curl localhost:5000/api/size?package=styled-components && echo '\\n') &\nwait\n#sleep 5\n#(curl localhost:5000/api/size?package=backbone && echo '\\n') &\n#sleep 5\n#(curl localhost:5000/api/size?package=jquery && echo '\\n') &\n#sleep 5\n#(curl localhost:5000/api/size?package=formik && echo '\\n') &\n#sleep 5\n#(curl localhost:5000/api/size?package=animejs && echo '\\n') &\n#sleep 5\n#(curl localhost:5000/api/size?package=three && echo '\\n') &\n#sleep 5\n#(curl localhost:5000/api/size?package=babylonjs && echo '\\n') &\n#sleep 5\n#(curl localhost:5000/api/size?package=emotion && echo '\\n') &\n#sleep 5\n#(curl localhost:5000/api/size?package=react-treeview && echo '\\n') &\n#sleep 5\n#(curl localhost:5000/api/size?package=react-bootstrap && echo '\\n') &\n#sleep 5\n#(curl localhost:5000/api/size?package=vuex && echo '\\n') &\n#sleep 5\n#(curl localhost:5000/api/size?package=mojs && echo '\\n') &\n#sleep 5\n#(curl localhost:5000/api/size?normalize.css && echo '\\n') &\n"
  },
  {
    "path": "__tests__/utils.test.js",
    "content": "import { parsePackageString } from '../utils/common.utils'\n\ndescribe('parsePackageString', () => {\n  it('handles scoped packages correctly', () => {\n    expect(parsePackageString('@babel/core@9.8.0')).toEqual({\n      scoped: true,\n      name: '@babel/core',\n      version: '9.8.0',\n    })\n  })\n\n  it('handles scoped packages without versions correctly', () => {\n    expect(parsePackageString('@babel/core')).toEqual({\n      scoped: true,\n      name: '@babel/core',\n      version: null,\n    })\n  })\n\n  it('handles regular packages correctly', () => {\n    expect(parsePackageString('react@15.6.1')).toEqual({\n      scoped: false,\n      name: 'react',\n      version: '15.6.1',\n    })\n  })\n\n  it('handles regular packages without version correctly', () => {\n    expect(parsePackageString('react')).toEqual({\n      scoped: false,\n      name: 'react',\n      version: null,\n    })\n  })\n\n  it('handles special characters in name properly', () => {\n    expect(parsePackageString('chart.js@5.6.0')).toEqual({\n      scoped: false,\n      name: 'chart.js',\n      version: '5.6.0',\n    })\n  })\n\n  it('handles special characters in version properly', () => {\n    expect(parsePackageString('chart.js@0.7.0-beta')).toEqual({\n      scoped: false,\n      name: 'chart.js',\n      version: '0.7.0-beta',\n    })\n  })\n})\n"
  },
  {
    "path": "bin/generate-sitemap.js",
    "content": "const { SitemapStream, streamToPromise } = require( 'sitemap' )\nconst { Readable } = require( 'stream' )\nconst { writeFileSync } = require('fs')\nconst path = require('path')\n\n// Source: https://analytics.amplitude.com/bundlephobia/chart/3tbq2vm/edit/jmy3u6h\nconst popularPackages = [\n  \"react\",\n  \"moment\",\n  \"lodash\",\n  \"react-dom\",\n  \"axios\",\n  \"@material-ui/core\",\n  \"date-fns\",\n  \"dayjs\",\n  \"vue\",\n  \"redux\",\n  \"react-query\",\n  \"swiper\",\n  \"react-hook-form\",\n  \"styled-components\",\n  \"formik\",\n  \"framer-motion\",\n  \"yup\",\n  \"angular\",\n  \"antd\",\n  \"preact\",\n  \"react-spring\",\n  \"rxjs\",\n  \"jquery\",\n  \"chart.js\",\n  \"firebase\",\n  \"glider-js\",\n  \"chroma-js\",\n  \"react-select\",\n  \"@google/model-viewer\",\n  \"bootstrap\",\n  \"react-redux\",\n  \"@apollo/client\",\n  \"three\",\n  \"luxon\",\n  \"uuid\",\n  \"tailwindcss\",\n  \"swr\",\n  \"mobx\",\n  \"react-slick\",\n  \"d3\",\n  \"react-router-dom\",\n  \"@angular/core\",\n  \"recoil\",\n  \"immer\",\n  \"express\",\n  \"classnames\",\n  \"react-datepicker\",\n  \"recharts\",\n  \"svelte\",\n  \"@chakra-ui/react\",\n  \"react-final-form\",\n  \"xstate\",\n  \"zustand\",\n  \"slick-carousel\",\n  \"next\",\n  \"@reduxjs/toolkit\",\n  \"react-transition-group\",\n  \"ramda\",\n  \"gsap\",\n  \"lodash-es\",\n  \"query-string\",\n  \"emotion\",\n  \"react-beautiful-dnd\",\n  \"react-router\",\n  \"react-dnd\",\n  \"react-bootstrap\",\n  \"react-window\",\n  \"@react-google-maps/api\",\n  \"qs\",\n  \"react-motion\",\n  \"joi\",\n  \"slate\",\n  \"moment-timezone\",\n  \"chartist\",\n  \"react-i18next\",\n  \"react-table\",\n  \"react-virtualized\",\n  \"lottie-web\",\n  \"node-fetch\",\n  \"@emotion/styled\",\n  \"react-toastify\",\n  \"xlsx\",\n  \"i18next\",\n  \"flickity\",\n  \"@emotion/react\",\n  \"@material-ui/styles\",\n  \"animejs\",\n  \"react-intl\",\n  \"@hookstate/core\",\n  \"dompurify\",\n  \"@popperjs/core\",\n  \"graphql\",\n  \"highcharts\",\n  \"js-cookie\",\n  \"vuetify\",\n  \"clsx\",\n  \"@sentry/browser\",\n  \"draft-js\",\n  \"zod\",\n  \"material-ui\",\n  \"superstruct\",\n  \"downshift\",\n  \"redux-saga\",\n  \"@headlessui/react\",\n  \"redux-thunk\",\n  \"nanoid\",\n  \"libphonenumber-js\",\n  \"redux-toolkit\",\n  \"react-markdown\",\n  \"react-day-picker\",\n  \"react-use\",\n  \"urql\",\n  \"quill\",\n  \"@material-ui/icons\",\n  \"react-modal\",\n  \"marked\",\n  \"react-icons\",\n  \"momentjs\",\n  \"ajv\",\n  \"jspdf\",\n  \"react-dropzone\",\n  \"react-popper\",\n  \"jotai\",\n  \"react-tooltip\",\n  \"sanitize-html\",\n  \"crypto-js\",\n  \"react-move\",\n  \"react-helmet\",\n  \"apexcharts\",\n  \"react-chartjs-2\",\n  \"react-multi-carousel\",\n  \"fuse.js\",\n  \"alpinejs\",\n  \"aws-sdk\",\n  \"react-intersection-observer\",\n  \"react-responsive-modal\",\n  \"core-js\",\n  \"tippy.js\",\n  \"react-responsive-carousel\",\n  \"react-dates\",\n  \"popper.js\",\n  \"graphql-request\",\n  \"frappe-charts\",\n  \"exceljs\",\n  \"chartjs\",\n  \"react-form\",\n  \"vuex\",\n  \"uplot\",\n  \"date-fns-tz\",\n  \"phin\",\n  \"react-player\",\n  \"keen-slider\",\n  \"underscore\",\n  \"final-form\",\n  \"@angular/material\",\n  \"react-number-format\",\n  \"immutable\",\n  \"xss\",\n  \"chakra-ui\",\n  \"lodash.debounce\",\n  \"typescript\",\n  \"echarts\",\n  \"bulma\",\n  \"jsonschema\",\n  \"@xstate/react\",\n  \"react-pdf\",\n  \"got\",\n  \"victory\",\n  \"lazysizes\",\n  \"validator\",\n  \"lit-html\",\n  \"effector\",\n  \"numeral\",\n  \"lit-element\",\n  \"mobx-react\",\n  \"polished\"\n]\n\nconst otherPages = ['', '/scan']\n\nconst links = [\n  ...otherPages.map(page => ({\n    url: page,\n    changefreq: 'weekly',\n    priority: 1\n  })),\n  ...popularPackages.map(package => ({\n    url: `/package/${package}`,\n    changefreq: 'weekly',\n    priority: 0.7\n  })),\n]\n\n// Create a stream to write to\nconst stream = new SitemapStream( { hostname: 'https://bundlephobia.com' } )\n\n// Return a promise that resolves with your XML string\nconst sitemapPromise = streamToPromise(Readable.from(links).pipe(stream)).then((data) =>\n  data.toString()\n)\n\nsitemapPromise\n  .then((sitemap) => {\n  writeFileSync(path.join(__dirname, '..', 'client', 'assets', 'public', 'sitemap.xml'), sitemap, 'utf8')\n})\n  .catch((err) => {\n    console.error(err)\n    process.exit(1)\n  })\n"
  },
  {
    "path": "bin/getResults.js",
    "content": "const firebase = require('firebase')\nconst { encodeFirebaseKey, decodeFirebaseKey } = require('../utils/index')\nconst fs = require('fs')\nrequire('dotenv').config()\n\nconst firebaseConfig = {\n  apiKey: process.env.FIREBASE_API_KEY,\n  authDomain: process.env.FIREBASE_AUTH_DOMAIN,\n  databaseURL: process.env.FIREBASE_DATABASE_URL,\n}\n\nfirebase.initializeApp(firebaseConfig)\n\nfunction getFirebaseStoreFromDisk() {\n  try {\n    return require('./data/firebase-modules.json')\n  } catch (err) {\n    console.log('not found on disk')\n    return null\n  }\n}\n\nasync function getFirebaseStoreFromNetwork() {\n  const modulesRef = firebase.database().ref('modules-v2')\n  const lastEntry = Object.keys(\n    await modulesRef\n      .limitToLast(1)\n      .once('value')\n      .then(snapshot => snapshot.val())\n  )[0]\n\n  const firstEntry = Object.keys(\n    await modulesRef\n      .limitToFirst(1)\n      .once('value')\n      .then(snapshot => snapshot.val())\n  )[0]\n\n  let currentLastEntry = firstEntry\n  let allData = {}\n  let counter = 0\n\n  console.log('fetching from ', firstEntry, ' to ', lastEntry)\n\n  while (currentLastEntry !== lastEntry) {\n    counter += 20000\n    const snapshot = await firebase\n      .database()\n      .ref('modules-v2')\n      .orderByKey()\n      .startAt(currentLastEntry)\n      .limitToFirst(20000)\n      .once('value')\n      .then(snapshot => snapshot.val())\n\n    const packageNames = Object.keys(snapshot)\n    currentLastEntry = packageNames[packageNames.length - 1]\n    console.log(\n      'Fetched records till ',\n      counter,\n      currentLastEntry,\n      'total of ',\n      Object.keys(snapshot),\n      ' packages.'\n    )\n    allData = { ...allData, ...snapshot }\n  }\n\n  fs.mkdirSync(__dirname + '/data', { recursive: true })\n\n  fs.writeFileSync(\n    __dirname + '/data/firebase-modules.json',\n    JSON.stringify(allData, null, 2),\n    'utf8'\n  )\n  return allData\n}\n\nasync function getResults() {\n  let firebaseStore = getFirebaseStoreFromDisk()\n  console.log('loaded firebase store')\n  return Object.keys(firebaseStore).flatMap(packageName =>\n    Object.keys(firebaseStore[packageName]).map(\n      version => firebaseStore[packageName][version]\n    )\n  )\n}\n\nasync function getPackages() {\n  let firebaseStore =\n    getFirebaseStoreFromDisk() || (await getFirebaseStoreFromNetwork())\n  const packages = Object.keys(firebaseStore).map(\n    packageName => firebaseStore[packageName]\n  )\n  console.log('fetched ', Object.keys(firebaseStore), ' packages ')\n  return packages\n}\n\nmodule.exports = { getResults, getPackages }\n"
  },
  {
    "path": "bin/updateHistoricalData.js",
    "content": "#!/usr/bin/env node\n\nconst firebase = require('firebase')\nconst FirebaseUtils = require('../utils/firebase.utils')\nconst trending = require('trending-github')\nconst fetch = require('node-fetch')\nconst debug = require('debug')('bp:trending-fetch')\nconst GithubAPI = require('github')\nconst isEmptyObject = require('is-empty-object')\nconst promiseSeries = require('promise.series')\n\nrequire('dotenv').config()\n\nconst github = new GithubAPI({\n  debug: false\n})\n\ngithub.authenticate({\n  type: 'oauth',\n  key: process.env.GITHUB_CLIENT_ID,\n  secret: process.env.GITHUB_CLIENT_SECRET\n})\n\nconst firebaseConfig = {\n  apiKey: process.env.FIREBASE_API_KEY,\n  authDomain: process.env.FIREBASE_AUTH_DOMAIN,\n  databaseURL: process.env.FIREBASE_DATABASE_URL\n}\n\nfirebase.initializeApp(firebaseConfig)\n\nconst firebaseUtils = new FirebaseUtils(firebase)\nconst port = process.env.PORT || 5000\n\nasync function getPackageFromRepo(author, name) {\n  const {\n    data: { content }\n  } = await github.repos.getContent({\n    repo: author,\n    owner: name,\n    path: 'package.json'\n  })\n\n  if (content) {\n    const decodedContent = Buffer.from(content, 'base64').toString('utf8')\n    return JSON.parse(decodedContent).name\n  }\n}\n\nasync function getGithubTrendingPackages() {\n  const repos = await trending('daily', 'javascript')\n  const packages = await Promise.all(\n    repos.map(repo => getPackageFromRepo(repo.author, repo.name))\n  )\n  return packages.filter(pack => pack)\n}\n\nasync function getTrendingSearches() {\n  const limit = 20\n  let trendingSearches = []\n  const searches = await firebaseUtils.getDailySearches()\n\n  if (searches) {\n    trendingSearches = Object.keys(searches)\n      .sort(\n        (packageA, packageB) =>\n          searches[packageB].count - searches[packageA].count\n      )\n      .slice(0, limit)\n  }\n\n  return trendingSearches\n}\n\nasync function updateHistoricalData() {\n  try {\n    const [githubTrendingPackages, searchTrendingPackages] = await Promise.all([\n      getGithubTrendingPackages(),\n      getTrendingSearches()\n    ])\n\n    const popularPackages = [\n      ...new Set(githubTrendingPackages.concat(searchTrendingPackages))\n    ]\n    console.log('popular', popularPackages)\n  } catch (err) {\n    console.log(err)\n  }\n}\n\nasync function getVersionsToBuild(name) {\n  const versionsToBuild = []\n  const res = await fetch(\n    `http://localhost:${port}/api/package-history?package=${name}`\n  )\n  const versionInfo = await res.json()\n\n  Object.keys(versionInfo).forEach(version => {\n    if (isEmptyObject(versionInfo[version])) {\n      versionsToBuild.push(version)\n    }\n  })\n\n  return versionsToBuild\n}\n\nasync function getVersionsToBuild(name) {\n  const versionsToBuild = []\n  const res = await fetch(\n    `http://localhost:${port}/api/package-history?package=${name}`\n  )\n  const versionInfo = await res.json()\n\n  Object.keys(versionInfo).forEach(version => {\n    if (isEmptyObject(versionInfo[version])) {\n      versionsToBuild.push(version)\n    }\n  })\n\n  return versionsToBuild\n}\n\nasync function buildPackage(name, version) {\n  debug('building package %s %s', name, version)\n  const versionsToBuild = []\n  const res = await fetch(\n    `http://localhost:${port}/api/size?package=${name + '@' + version}`\n  )\n  debug('result %s %s %O', name, version, await res.json())\n}\n\nasync function buildPackageFromGithub(name, author) {\n  debug('building repo %s', name)\n  const packageName = await getPackageFromRepo(name, author)\n\n  if (packageName) {\n    const versions = await getVersionsToBuild(packageName)\n    debug('versions to build for %s — %o', packageName, versions)\n    await promiseSeries(\n      versions.map(version => () => buildPackage(packageName, version))\n    )\n  } else {\n    debug('skipped repo %s', name)\n  }\n}\n\nasync function mostPopuplarGithubRepos() {\n  const repos = await github.search.repos({\n    q: 'language:javascript+npm in:readme+size:1000..50000+mirror:false',\n    sort: 'stars',\n    page: 1,\n    per_page: 10\n  })\n\n  debug(\n    'Popular GitHub Repos %o',\n    repos.data.items.map(r => r.name)\n  )\n\n  try {\n    const promises = repos.data.items.map(({ name, owner }) => () =>\n      buildPackageFromGithub(name, owner.login)\n    )\n    await promiseSeries(promises)\n  } catch (err) {\n    console.log(err)\n  }\n}\n\nmostPopuplarGithubRepos()\n"
  },
  {
    "path": "build-service/.yarnrc.yml",
    "content": "compressionLevel: mixed\n\nenableGlobalCache: false\n"
  },
  {
    "path": "build-service/index.js",
    "content": "import 'dotenv-defaults/config.js'\nimport Fastify from 'fastify'\nimport {\n  getPackageStats,\n  getAllPackageExports,\n  getPackageExportSizes,\n  eventQueue,\n} from 'package-build-stats'\nimport Amplitude from '@amplitude/node'\n\nconst fastify = Fastify()\n\nif (process.env.AMPLITUDE_API_KEY) {\n  const client = Amplitude.init(process.env.AMPLITUDE_API_KEY)\n\n  eventQueue.on('*', (event, details) => {\n    client.logEvent({\n      event_type: event,\n      user_id: 'build-service',\n      event_properties: {\n        ...details,\n      },\n    })\n  })\n\n  setInterval(() => {\n    client.flush()\n  }, 5000)\n}\n\nfastify.get('/size', async (req, res) => {\n  const packageString = decodeURIComponent(req.query.p)\n  try {\n    const result = await getPackageStats(packageString, {\n      installTimeout: 60000,\n    })\n    return res.code(200).send(result)\n  } catch (err) {\n    console.log(err)\n    const errorToSend = 'toJSON' in err ? err.toJSON() : err\n    return res.code(500).send(errorToSend)\n  }\n})\n\nfastify.get('/exports-sizes', async (req, res) => {\n  const packageString = decodeURIComponent(req.query.p)\n\n  try {\n    const result = await getPackageExportSizes(packageString, {\n      installTimeout: 60000,\n    })\n    return res.code(200).send(result)\n  } catch (err) {\n    console.log(err)\n    const errorToSend = 'toJSON' in err ? err.toJSON() : err\n    return res.code(500).send(errorToSend)\n  }\n})\n\nfastify.get('/exports', async (req, res) => {\n  const packageString = decodeURIComponent(req.query.p)\n\n  try {\n    const result = await getAllPackageExports(packageString, {\n      installTimeout: 60000,\n    })\n    return res.code(200).send(result)\n  } catch (err) {\n    console.log(err)\n    const errorToSend = 'toJSON' in err ? err.toJSON() : err\n    return res.code(500).send(errorToSend)\n  }\n})\n\nfastify\n  .listen({ port: 7002 })\n  .then(() => {\n    console.log(`server listening on ${fastify.server.address().port}`)\n  })\n  .catch(err => {\n    console.error(err)\n    process.exit(1)\n  })\n"
  },
  {
    "path": "build-service/package.json",
    "content": "{\n  \"name\": \"build-service\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@amplitude/node\": \"^1.10.2\",\n    \"dotenv-defaults\": \"^2.0.2\",\n    \"fastify\": \"^4.25.2\",\n    \"now-logs\": \"^0.0.7\",\n    \"package-build-stats\": \"8.2.5\"\n  },\n  \"engines\": {\n    \"node\": \">= 9.x.x\",\n    \"npm\": \">= 5.5.x\"\n  },\n  \"scripts\": {\n    \"start\": \"DEBUG=bp:* node index\",\n    \"dev\": \"DEBUG=bp:* node index\"\n  }\n}\n"
  },
  {
    "path": "cache-service/.gitignore",
    "content": ""
  },
  {
    "path": "cache-service/cache.utils.js",
    "content": "function encodeFirebaseKey(key) {\n  return key.replace(/[.]/g, ',').replace(/\\//g, '__')\n}\n\nmodule.exports = { encodeFirebaseKey }\n"
  },
  {
    "path": "cache-service/index.js",
    "content": "require('dotenv-defaults').config()\nconst firebase = require('firebase')\nconst fastify = require('fastify')()\nconst {\n  getPackageSizeMiddlware,\n  postPackageSizeMiddlware,\n} = require('./middlewares/package-size.middleware')\nconst {\n  getExportsSizeMiddlware,\n  postExportsSizeMiddleware,\n} = require('./middlewares/exports-size.middleware')\n\nconst firebaseConfig = {\n  apiKey: process.env.FIREBASE_API_KEY,\n  authDomain: process.env.FIREBASE_AUTH_DOMAIN,\n  databaseURL: process.env.FIREBASE_DATABASE_URL,\n}\n\nfirebase.initializeApp(firebaseConfig)\n\nfastify.get('/package-cache', getPackageSizeMiddlware)\nfastify.post('/package-cache', postPackageSizeMiddlware)\n\nfastify.get('/exports-cache', getExportsSizeMiddlware)\nfastify.post('/exports-cache', postExportsSizeMiddleware)\n\nfastify\n  .listen({ port: 7001 })\n  .then(() => {\n    console.log(`server listening on ${fastify.server.address().port}`)\n  })\n  .catch(err => {\n    console.error(err)\n    process.exit(1)\n  })\n"
  },
  {
    "path": "cache-service/middlewares/exports-size.middleware.js",
    "content": "require('dotenv-defaults').config()\nconst LRU = require('lru-cache')\nconst firebase = require('firebase')\nconst debug = require('debug')('bp:cache')\nconst { encodeFirebaseKey } = require('../cache.utils')\n\nconst LRUCache = new LRU({ max: 1500 })\n\n// Configurable Firebase keys for read/write operations\nconst FIREBASE_READ_KEY_EXPORTS =\n  process.env.FIREBASE_READ_KEY_EXPORTS || 'exports-v3'\nconst FIREBASE_WRITE_KEY_EXPORTS =\n  process.env.FIREBASE_WRITE_KEY_EXPORTS || 'exports-v3'\n\ndebug(\n  'Firebase config (exports): READ from %s (with fallback: %s), WRITE to %s',\n  FIREBASE_READ_KEY_EXPORTS,\n  FIREBASE_READ_KEY_EXPORTS === 'exports-v3' ? 'yes, to exports' : 'no',\n  FIREBASE_WRITE_KEY_EXPORTS\n)\n\nasync function getPackageResultFromKey(key, { name, version }) {\n  const ref = firebase\n    .database()\n    .ref()\n    .child(key)\n    .child(encodeFirebaseKey(name))\n    .child(encodeFirebaseKey(version))\n\n  const snapshot = await ref.once('value')\n  return snapshot.val()\n}\n\nasync function getPackageResult({ name, version, readKey }) {\n  const targetReadKey = readKey || FIREBASE_READ_KEY_EXPORTS\n  // Try primary read key first\n  const result = await getPackageResultFromKey(targetReadKey, { name, version })\n\n  if (result) {\n    debug('cache hit: firebase (%s)', targetReadKey)\n    return result\n  }\n\n  // If reading from default v3 and not found, fall back to \"exports\" (v2)\n  if (\n    targetReadKey === 'exports-v3' &&\n    !readKey &&\n    !process.env.DISABLE_FIREBASE_V2_FALLBACK\n  ) {\n    const fallbackResult = await getPackageResultFromKey('exports', {\n      name,\n      version,\n    })\n    if (fallbackResult) {\n      debug('cache hit: firebase (fallback to exports)')\n    }\n    return fallbackResult\n  }\n\n  return null\n}\n\nasync function setPackageResult({ name, version, result }) {\n  const modules = firebase.database().ref().child(FIREBASE_WRITE_KEY_EXPORTS)\n  return modules\n    .child(encodeFirebaseKey(name))\n    .child(encodeFirebaseKey(version))\n    .set(result)\n}\n\nasync function getExportsSizeMiddlware(req, res) {\n  const name = decodeURIComponent(req.query.name)\n  const version = decodeURIComponent(req.query.version)\n  const readKey = req.query.readKey\n\n  if (!name || !version) {\n    return res.code(422).send()\n  }\n  debug('get exports %s@%s (readKey: %s)', name, version, readKey)\n\n  // Use memory cache only if no explicit readKey is provided\n  if (!readKey) {\n    const lruCacheEntry = LRUCache.get(`${name}@${version}`)\n    if (lruCacheEntry) {\n      debug('cache hit: memory')\n      return res.code(200).send(lruCacheEntry)\n    }\n  }\n\n  const result = await getPackageResult({ name, version, readKey })\n  if (result) {\n    debug('cache hit: firebase')\n    if (!readKey) {\n      LRUCache.set(`${name}@${version}`, result)\n    }\n    return res.code(200).send(result)\n  }\n\n  return res.code(404).send()\n}\n\nasync function postExportsSizeMiddleware(req, res) {\n  const { name, version, result } = req.body\n\n  if (!name || !version || !result) return res.code(422).send()\n\n  debug('set exports %O to %O', { name, version }, result)\n  LRUCache.set(`${name}@${version}`, result)\n  try {\n    await setPackageResult({ name, version, result })\n    return res.code(201).send()\n  } catch (err) {\n    console.log(err)\n    return res.code(500).send({ error: err })\n  }\n}\n\nmodule.exports = { getExportsSizeMiddlware, postExportsSizeMiddleware }\n"
  },
  {
    "path": "cache-service/middlewares/package-size.middleware.js",
    "content": "require('dotenv-defaults').config()\nconst firebase = require('firebase')\nconst LRU = require('lru-cache')\nconst debug = require('debug')('bp:cache')\nconst { encodeFirebaseKey } = require('../cache.utils')\nconst LRUCache = new LRU({ max: 3000 })\n\n// Configurable Firebase keys for read/write operations\n// This allows safe migration from modules-v2 (old) to modules-v3 (new package-build-stats 8.x)\n// When FIREBASE_READ_KEY is 'modules-v3', it will try v3 first, then fall back to v2\n// When FIREBASE_READ_KEY is 'modules-v2', it will only read from v2\nconst FIREBASE_READ_KEY = process.env.FIREBASE_READ_KEY || 'modules-v3'\nconst FIREBASE_WRITE_KEY = process.env.FIREBASE_WRITE_KEY || 'modules-v3'\n\ndebug(\n  'Firebase config: READ from %s (with fallback: %s), WRITE to %s',\n  FIREBASE_READ_KEY,\n  FIREBASE_READ_KEY === 'modules-v3' ? 'yes, to modules-v2' : 'no',\n  FIREBASE_WRITE_KEY\n)\n\nasync function getPackageResultFromKey(key, { name, version }) {\n  const ref = firebase\n    .database()\n    .ref()\n    .child(key)\n    .child(encodeFirebaseKey(name))\n    .child(encodeFirebaseKey(version))\n\n  const snapshot = await ref.once('value')\n  return snapshot.val()\n}\n\nasync function getPackageResult({ name, version, readKey }) {\n  const targetReadKey = readKey || FIREBASE_READ_KEY\n  // Try primary read key first\n  const result = await getPackageResultFromKey(targetReadKey, { name, version })\n\n  if (result) {\n    debug('cache hit: firebase (%s)', targetReadKey)\n    return result\n  }\n\n  // If reading from default v3 and not found, fall back to v2\n  if (\n    targetReadKey === 'modules-v3' &&\n    !readKey &&\n    !process.env.DISABLE_FIREBASE_V2_FALLBACK\n  ) {\n    const fallbackResult = await getPackageResultFromKey('modules-v2', {\n      name,\n      version,\n    })\n    if (fallbackResult) {\n      debug('cache hit: firebase (fallback to modules-v2)')\n    }\n    return fallbackResult\n  }\n\n  return null\n}\n\nasync function setPackageResult({ name, version, result }) {\n  const modules = firebase.database().ref().child(FIREBASE_WRITE_KEY)\n  return modules\n    .child(encodeFirebaseKey(name))\n    .child(encodeFirebaseKey(version))\n    .set(result)\n}\n\nasync function getPackageSizeMiddlware(req, res) {\n  const name = decodeURIComponent(req.query.name)\n  const version = decodeURIComponent(req.query.version)\n  const readKey = req.query.readKey\n\n  if (!name || !version) {\n    return res.code(422).send()\n  }\n  debug('get package %s@%s (readKey: %s)', name, version, readKey)\n\n  // Use memory cache only if no explicit readKey is provided\n  if (!readKey) {\n    const lruCacheEntry = LRUCache.get(`${name}@${version}`)\n    if (lruCacheEntry) {\n      debug('cache hit: memory')\n      return res.code(200).send(lruCacheEntry)\n    }\n  }\n\n  const result = await getPackageResult({ name, version, readKey })\n  if (result) {\n    debug('cache hit: firebase')\n    if (!readKey) {\n      LRUCache.set(`${name}@${version}`, result)\n    }\n    return res.code(200).send(result)\n  }\n\n  return res.code(404).send()\n}\n\nasync function postPackageSizeMiddlware(req, res) {\n  const { name, version, result } = req.body\n\n  if (!name || !version || !result) return res.code(422).send()\n\n  debug('set package %O to %O', { name, version }, result)\n  LRUCache.set(`${name}@${version}`, result)\n  try {\n    await setPackageResult({ name, version, result })\n    return res.code(201).send()\n  } catch (err) {\n    console.log(err)\n    return res.code(500).send({ error: err })\n  }\n}\n\nmodule.exports = { getPackageSizeMiddlware, postPackageSizeMiddlware }\n"
  },
  {
    "path": "cache-service/package.json",
    "content": "{\n  \"name\": \"cache\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"debug\": \"^4.3.4\",\n    \"dotenv\": \"^8.6.0\",\n    \"dotenv-defaults\": \"^2.0.2\",\n    \"fastify\": \"^4.25.2\",\n    \"firebase\": \"^8.10.1\",\n    \"lru-cache\": \"^5.1.1\"\n  },\n  \"scripts\": {\n    \"start\": \"DEBUG=bp* node index\",\n    \"dev\": \"DEBUG=bp* node index\"\n  }\n}\n"
  },
  {
    "path": "client/analytics.ts",
    "content": "type HasPackageName = {\n  packageName: string\n}\n\ntype HasTimeTaken = {\n  timeTaken: number\n}\n\ntype HasIsDisabled = {\n  isDisabled: boolean\n}\n\ntype HasSuccessRatio = {\n  successRatio: number\n}\n\ntype HasPackageNameAndTimeTaken = HasPackageName & HasTimeTaken\n\nexport default class Analytics {\n  static pageView(pageType: string) {\n    amplitude.getInstance().logEvent(`Viewed ${pageType}`, {\n      path: window.location.pathname,\n    })\n  }\n\n  static performedSearch(packageName: string) {\n    amplitude.getInstance().logEvent('Search Performed', {\n      package: packageName,\n    })\n  }\n\n  static searchSuccess({ packageName, timeTaken }: HasPackageNameAndTimeTaken) {\n    amplitude.getInstance().logEvent('Search Successful', {\n      package: packageName,\n      timeTaken,\n    })\n  }\n\n  static searchFailure({ packageName, timeTaken }: HasPackageNameAndTimeTaken) {\n    amplitude.getInstance().logEvent('Search Failed', {\n      package: packageName,\n      timeTaken,\n    })\n  }\n\n  static graphBarClicked({\n    packageName,\n    isDisabled,\n  }: HasPackageName & HasIsDisabled) {\n    amplitude.getInstance().logEvent('Bar Graph Clicked', {\n      package: packageName,\n      isDisabled,\n    })\n  }\n\n  static scanPackageJsonDropped(itemCount: number) {\n    amplitude.getInstance().logEvent('Scan packageJSON dropped', {\n      itemCount,\n    })\n  }\n\n  static performedScan() {\n    amplitude.getInstance().logEvent('Scan Performed')\n  }\n\n  static scanParseError() {\n    amplitude.getInstance().logEvent('Scan Parse Error')\n  }\n\n  static scanCompleted({\n    timeTaken,\n    successRatio,\n  }: HasTimeTaken & HasSuccessRatio) {\n    amplitude.getInstance().logEvent('Scan Parse Completed', {\n      successRatio,\n      timeTaken,\n    })\n  }\n\n  static performedExportsAnalysis(packageName: string) {\n    amplitude.getInstance().logEvent('Exports Analysis Performed', {\n      package: packageName,\n    })\n  }\n\n  static exportsAnalysisSuccess({\n    packageName,\n    timeTaken,\n  }: HasPackageNameAndTimeTaken) {\n    amplitude.getInstance().logEvent('Exports Analysis Successful', {\n      package: packageName,\n      timeTaken,\n    })\n  }\n\n  static exportsAnalysisFailure({\n    packageName,\n    timeTaken,\n  }: HasPackageNameAndTimeTaken) {\n    amplitude.getInstance().logEvent('Exports Analysis Failed', {\n      package: packageName,\n      timeTaken,\n    })\n  }\n\n  static exportsSizesSuccess({\n    packageName,\n    timeTaken,\n  }: HasPackageNameAndTimeTaken) {\n    amplitude.getInstance().logEvent('Exports Size Calculated', {\n      package: packageName,\n      timeTaken,\n    })\n  }\n\n  static exportsSizesFailure({\n    packageName,\n    timeTaken,\n  }: HasPackageNameAndTimeTaken) {\n    amplitude.getInstance().logEvent('Exports Size Failed', {\n      package: packageName,\n      timeTaken,\n    })\n  }\n}\n"
  },
  {
    "path": "client/api.ts",
    "content": "import fetch from 'unfetch'\n\ntype PackageSuggestion = {\n  searchScore: number\n  score: { detail: { popularity: number } }\n}\n\ntype RecentSearch = {\n  [key: string]: {\n    name: string\n    version: string\n    lastSearched: number\n    count: number\n  }\n}\n\nexport default class API {\n  static get<T = unknown>(url: string, isInternal = true): Promise<T> {\n    const headers: Record<string, string> = {\n      Accept: 'application/json',\n    }\n\n    if (isInternal) {\n      headers['X-Bundlephobia-User'] = 'bundlephobia website'\n    }\n    return fetch(url, { headers }).then(res => {\n      if (!res.ok) {\n        try {\n          return res.json().then(err => Promise.reject(err))\n        } catch (e) {\n          if (res.status === 503) {\n            return Promise.reject({\n              error: {\n                code: 'TimeoutError',\n                message:\n                  'This is taking unusually long. Check back in a couple of minutes?',\n              },\n            })\n          }\n\n          return Promise.reject({\n            error: {\n              code: 'BuildError',\n              message:\n                \"Oops, something went wrong and we don't have an appropriate error for this. Open an issue maybe?\",\n            },\n          })\n        }\n      }\n      return res.json()\n    })\n  }\n\n  static getInfo(packageString: string) {\n    return API.get(`/api/size?package=${packageString}&record=true`)\n  }\n\n  static getExports(packageString: string) {\n    return API.get(`/api/exports?package=${packageString}`)\n  }\n\n  static getExportsSizes(packageString: string) {\n    return API.get(`/api/exports-sizes?package=${packageString}`)\n  }\n\n  static getHistory(packageString: string, limit: number) {\n    return API.get(\n      `/api/package-history?package=${packageString}&limit=${limit}`\n    )\n  }\n\n  static getRecentSearches(limit: number) {\n    return API.get<RecentSearch[]>(`/api/recent?limit=${limit}`)\n  }\n\n  static getSimilar(packageName: string) {\n    return API.get(`/api/similar-packages?package=${packageName}`)\n  }\n\n  static getSuggestions(query: string) {\n    const suggestionSort = (\n      packageA: PackageSuggestion,\n      packageB: PackageSuggestion\n    ) => {\n      // Rank closely matching packages followed\n      // by most popular ones\n      if (\n        Math.abs(\n          Math.log(packageB.searchScore) - Math.log(packageA.searchScore)\n        ) > 1\n      ) {\n        return packageB.searchScore - packageA.searchScore\n      } else {\n        return (\n          packageB.score.detail.popularity - packageA.score.detail.popularity\n        )\n      }\n    }\n\n    return API.get<PackageSuggestion[]>(\n      `https://api.npms.io/v2/search/suggestions?q=${query}`,\n      false\n    ).then(result => result.sort(suggestionSort))\n\n    //backup when npms.io is down\n\n    //return API.get(`/-/search?text=${query}`)\n    //  .then(result => result.objects\n    //    .sort(suggestionSort)\n    //    .map(suggestion => {\n    //      const name = suggestion.package.name\n    //      const hasMatch = name.includes(query)\n    //      const startIndex = name.indexOf(query)\n    //      const endIndex = startIndex + query.length\n    //      let highlight\n    //\n    //      if (hasMatch) {\n    //        highlight =\n    //          name.substring(0, startIndex) +\n    //          '<em>' + name.substring(startIndex, endIndex) + '</em>' +\n    //          name.substring(endIndex)\n    //      } else {\n    //        highlight = name\n    //      }\n    //\n    //      return {\n    //        ...suggestion,\n    //        highlight,\n    //      }\n    //    }),\n    //  )\n  }\n}\n"
  },
  {
    "path": "client/assets/public/browserconfig.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n    <msapplication>\n        <tile>\n            <square150x150logo src=\"/mstile-150x150.png\"/>\n            <TileColor>#2b5797</TileColor>\n        </tile>\n    </msapplication>\n</browserconfig>\n"
  },
  {
    "path": "client/assets/public/manifest.json",
    "content": "{\n  \"name\": \"Bundlephobia\",\n  \"icons\": [\n    {\n      \"src\": \"/android-chrome-192x192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"/android-chrome-256x256.png\",\n      \"sizes\": \"256x256\",\n      \"type\": \"image/png\"\n    }\n  ],\n  \"theme_color\": \"#ffffff\",\n  \"background_color\": \"#ffffff\",\n  \"display\": \"standalone\",\n  \"orientation\": \"portrait\"\n}\n"
  },
  {
    "path": "client/assets/public/open-search-description.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\" xmlns:moz=\"http://www.mozilla.org/2006/browser/search/\">\n    <ShortName>bundlephobia</ShortName>\n    <Description>Search npm packages on bundlephobia</Description>\n    <InputEncoding>UTF-8</InputEncoding>\n    <OutputEncoding>UTF-8</OutputEncoding>\n    <Language>en-us</Language>\n    <Image width=\"32\" height=\"32\" type=\"image/png\">https://bundlephobia.com/favicon-32x32.png</Image>\n    <Tags>bundlephobia</Tags>\n    <Url type=\"text/html\"\n         template=\"https://bundlephobia.com/package/{searchTerms}\"/>\n    <moz:SearchForm>https://bundlephobia.com</moz:SearchForm>\n</OpenSearchDescription>\n"
  },
  {
    "path": "client/assets/public/robots.txt",
    "content": "# *\nUser-agent: *\nAllow: /\nDisallow: /scan-results\n\n# Host\nHost: https://bundlephobia.com\n\n# Sitemaps\nSitemap: https://bundlephobia.com/sitemap.xml\n"
  },
  {
    "path": "client/assets/public/sitemap.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\"\n        xmlns:xhtml=\"http://www.w3.org/1999/xhtml\" xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"\n        xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\">\n    <url>\n        <loc>https://bundlephobia.com/</loc>\n        <changefreq>weekly</changefreq>\n        <priority>1.0</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/scan</loc>\n        <changefreq>weekly</changefreq>\n        <priority>1.0</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/moment</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/lodash</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-dom</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/axios</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@material-ui/core</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/date-fns</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/dayjs</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/vue</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/redux</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-query</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/swiper</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-hook-form</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/styled-components</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/formik</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/framer-motion</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/yup</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/angular</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/antd</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/preact</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-spring</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/rxjs</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/jquery</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/chart.js</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/firebase</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/glider-js</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/chroma-js</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-select</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@google/model-viewer</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/bootstrap</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-redux</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@apollo/client</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/three</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/luxon</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/uuid</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/tailwindcss</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/swr</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/mobx</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-slick</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/d3</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-router-dom</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@angular/core</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/recoil</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/immer</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/express</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/classnames</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-datepicker</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/recharts</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/svelte</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@chakra-ui/react</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-final-form</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/xstate</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/zustand</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/slick-carousel</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/next</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@reduxjs/toolkit</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-transition-group</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/ramda</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/gsap</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/lodash-es</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/query-string</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/emotion</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-beautiful-dnd</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-router</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-dnd</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-bootstrap</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-window</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@react-google-maps/api</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/qs</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-motion</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/joi</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/slate</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/moment-timezone</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/chartist</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-i18next</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-table</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-virtualized</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/lottie-web</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/node-fetch</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@emotion/styled</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-toastify</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/xlsx</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/i18next</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/flickity</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@emotion/react</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@material-ui/styles</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/animejs</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-intl</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@hookstate/core</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/dompurify</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@popperjs/core</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/graphql</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/highcharts</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/js-cookie</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/vuetify</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/clsx</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@sentry/browser</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/draft-js</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/zod</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/material-ui</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/superstruct</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/downshift</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/redux-saga</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@headlessui/react</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/redux-thunk</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/nanoid</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/libphonenumber-js</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/redux-toolkit</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-markdown</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-day-picker</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-use</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/urql</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/quill</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@material-ui/icons</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-modal</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/marked</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-icons</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/momentjs</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/ajv</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/jspdf</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-dropzone</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-popper</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/jotai</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-tooltip</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/sanitize-html</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/crypto-js</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-move</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-helmet</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/apexcharts</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-chartjs-2</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-multi-carousel</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/fuse.js</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/alpinejs</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/aws-sdk</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-intersection-observer</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-responsive-modal</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/core-js</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/tippy.js</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-responsive-carousel</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-dates</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/popper.js</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/graphql-request</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/frappe-charts</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/exceljs</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/chartjs</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-form</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/vuex</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/uplot</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/date-fns-tz</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/phin</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-player</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/keen-slider</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/underscore</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/final-form</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@angular/material</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-number-format</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/immutable</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/xss</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/chakra-ui</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/lodash.debounce</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/typescript</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/echarts</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/bulma</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/jsonschema</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/@xstate/react</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/react-pdf</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/got</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/victory</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/lazysizes</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/validator</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/lit-html</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/effector</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/numeral</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/lit-element</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/mobx-react</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n    <url>\n        <loc>https://bundlephobia.com/package/polished</loc>\n        <changefreq>weekly</changefreq>\n        <priority>0.7</priority>\n    </url>\n</urlset>\n"
  },
  {
    "path": "client/components/AnnouncementBanner/AnnouncementBanner.scss",
    "content": "@import '../../../stylesheets/colors';\n@import '../../../stylesheets/variables';\n\n.announcement-banner {\n  background: #fff;\n  color: $dark-gulf-blue;\n  padding: 0;\n  position: fixed;\n  bottom: 24px;\n  right: 24px;\n  left: auto;\n  top: auto;\n  transform: none;\n  width: 300px;\n  margin: 0;\n  border-radius: 8px;\n  z-index: 1000;\n  border: 1.5px solid $raven;\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n\n.announcement-banner__content {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  max-width: 1200px;\n  margin: 0 auto;\n  padding: 10px 48px 10px 20px;\n  gap: 12px;\n\n  @media (max-width: 600px) {\n    padding: 10px 40px 10px 12px;\n    gap: 8px;\n  }\n}\n\n.announcement-banner__icon {\n  font-size: 18px;\n  flex-shrink: 0;\n}\n\n.announcement-banner__text {\n  margin: 0;\n  font-size: 14px;\n  line-height: 1.5;\n  line-height: 1.5;\n  text-align: left;\n  font-family: $font-family-body;\n\n  @media (max-width: 600px) {\n    font-size: 13px;\n    text-align: left;\n  }\n\n  strong {\n    font-weight: 600;\n    color: $gulf-blue;\n  }\n\n  a {\n    color: $cornflower-blue;\n    font-weight: 500;\n    text-decoration: underline;\n    text-decoration-color: rgba($cornflower-blue, 0.4);\n    text-underline-offset: 2px;\n    transition: all 0.2s ease;\n\n    &:hover {\n      text-decoration-color: $cornflower-blue;\n      color: darken($cornflower-blue, 10%);\n    }\n  }\n}\n\n.announcement-banner__close {\n  position: absolute;\n  right: 8px;\n  top: 8px;\n  transform: none;\n  background: transparent;\n  border: none;\n  color: $raven;\n  font-size: 24px;\n  font-weight: 300;\n  width: 28px;\n  height: 28px;\n  border-radius: 4px;\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  transition: all 0.2s ease;\n  line-height: 1;\n  padding: 0;\n\n  &:hover {\n    color: $dark-gulf-blue;\n    background: rgba($raven, 0.1);\n  }\n\n  &:focus {\n    outline: none;\n    color: $dark-gulf-blue;\n  }\n}\n"
  },
  {
    "path": "client/components/AnnouncementBanner/AnnouncementBanner.tsx",
    "content": "import React, { useState, useEffect } from 'react'\n\nconst STORAGE_KEY = 'bundlephobia_rspack_banner_dismissed'\nconst EXPIRY_DATE = new Date('2026-07-18T00:00:00Z') // 6 months from January 18, 2026\n\nexport const AnnouncementBanner: React.FC = () => {\n  const [isVisible, setIsVisible] = useState(false)\n\n  useEffect(() => {\n    // Check if banner should be shown\n    const now = new Date()\n\n    // Don't show after expiry date\n    if (now >= EXPIRY_DATE) {\n      return\n    }\n\n    // Check if already dismissed\n    try {\n      const dismissed = localStorage.getItem(STORAGE_KEY)\n      if (dismissed === 'true') {\n        return\n      }\n    } catch (e) {\n      // localStorage not available\n    }\n\n    setIsVisible(true)\n  }, [])\n\n  const handleDismiss = () => {\n    setIsVisible(false)\n    try {\n      localStorage.setItem(STORAGE_KEY, 'true')\n    } catch (e) {\n      // localStorage not available\n    }\n  }\n\n  if (!isVisible) {\n    return null\n  }\n\n  return (\n    <div className=\"announcement-banner\">\n      <div className=\"announcement-banner__content\">\n        <span className=\"announcement-banner__icon\">🚀</span>\n        <p className=\"announcement-banner__text\">\n          <strong>New:</strong> Now uses{' '}\n          <a\n            href=\"https://rspack.dev\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            Rspack\n          </a>{' '}\n          — much faster results, better tree-shaking, accuracy and reliability !\n        </p>\n        <button\n          className=\"announcement-banner__close\"\n          onClick={handleDismiss}\n          aria-label=\"Dismiss announcement\"\n        >\n          ×\n        </button>\n      </div>\n    </div>\n  )\n}\n\nexport default AnnouncementBanner\n"
  },
  {
    "path": "client/components/AnnouncementBanner/index.ts",
    "content": "export { AnnouncementBanner } from './AnnouncementBanner'\nexport { default } from './AnnouncementBanner'\n"
  },
  {
    "path": "client/components/AutocompleteInput/AutocompleteInput.scss",
    "content": "@use \"sass:math\";\n\n@import '../../../stylesheets/colors';\n@import '../../../stylesheets/variables';\n\n.autocomplete-input__container {\n  position: relative;\n  width: 100%;\n}\n\n.autocomplete-input {\n  width: 40vw;\n  border: none;\n  border-radius: 50px;\n  color: rgba(0, 0, 0, 0);\n}\n\n.autocomplete-input,\n.autocomplete-input__dummy-input {\n  @include font-size-lg;\n  padding: $global-spacing * 1.5 (25px + $global-spacing * 2) $global-spacing *\n    1.5 $global-spacing * 3;\n  font-family: $font-family-code;\n  font-weight: $font-weight-thin;\n  width: 100%;\n  box-sizing: border-box;\n  letter-spacing: -0.7px;\n  margin: 0;\n\n  @media screen and (max-width: 40em) {\n    padding: $global-spacing (20px + $global-spacing) $global-spacing\n      $global-spacing;\n  }\n}\n\n.autocomplete-input__dummy-input {\n  position: absolute;\n  left: 0;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  pointer-events: none;\n  display: flex;\n  align-items: center;\n  white-space: nowrap;\n  overflow: hidden;\n}\n\n.dummy-input__package-name {\n  color: #1d1d1d;\n  font-size: inherit;\n  font-weight: inherit;\n  margin: 0;\n}\n\n.dummy-input__package-version {\n  color: #636363;\n}\n\n.dummy-input__at-separator {\n  color: $pastel-green;\n}\n\n.autocomplete-input__suggestions-menu {\n  border: 1px solid $autocomplete-border-color;\n  border-top: 0;\n  background: rgba(255, 255, 255, 0.96);\n  font-size: 90%;\n  position: absolute;\n  overflow: auto;\n  z-index: 10;\n  max-height: 35vh;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n  border-bottom-left-radius: 10px;\n  border-bottom-right-radius: 10px;\n  animation: unroll 0.2s cubic-bezier(0.305, 0.42, 0.205, 1.2);\n  // align borders, negating nesting due to border on parent\n  left: -1px;\n  // hide rounded borders of the input\n  margin-top: -5px;\n  width: 100%;\n  width: calc(100% + 2px);\n\n  &:empty {\n    border: 0;\n  }\n}\n\n@keyframes unroll {\n  from {\n    opacity: 0;\n    transform: translateY(-5px);\n  }\n\n  to {\n    opacity: 1;\n    transform: translateY(0px);\n  }\n}\n\n.autocomplete-input__suggestion {\n  padding: 10px 32px;\n  color: #333;\n  font-size: 15px;\n  cursor: pointer;\n  font-family: 'Source Code Pro', monospace;\n  font-weight: $font-weight-light;\n  letter-spacing: -0.5px;\n\n  &:not(:last-of-type) {\n    border-bottom: 1px solid #f5f5f5;\n  }\n\n  @media screen and (max-width: 40em) {\n    padding: math.div($global-spacing, 1.5) $global-spacing * 1.5;\n  }\n\n  em {\n    font-weight: $font-weight-bold;\n    font-style: normal;\n    color: #444;\n  }\n}\n\n.autocomplete-input__suggestion--highlight {\n  background: rgb(212, 243, 255);\n}\n\n.autocomplete-input__suggestion-description {\n  @include font-size-xs;\n  width: 100%;\n  min-width: 260px;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  font-family: $font-family-body;\n  font-weight: $font-weight-thin;\n  color: #666;\n  padding-top: 5px;\n  letter-spacing: 0;\n\n  @media screen and (max-width: 40em) {\n    font-weight: $font-weight-light;\n  }\n}\n\n.autocomplete-input__form {\n  display: flex;\n  align-items: baseline;\n  position: relative;\n}\n\n.autocomplete-input__search-icon {\n  position: absolute;\n  right: $global-spacing * 2.5;\n  z-index: 1;\n  cursor: pointer;\n  top: 0;\n  bottom: 0;\n  margin: auto;\n  width: 25px;\n  height: 25px;\n  border: none;\n  background: none;\n  padding: 0;\n\n  @media screen and (max-width: 40em) {\n    width: 16px;\n    height: 16px;\n    right: $global-spacing * 1.5;\n  }\n\n  svg {\n    width: 100%;\n    height: 100%;\n\n    path {\n      transition: all 0.2s;\n      fill: #666;\n    }\n  }\n\n  &:hover {\n    path {\n      fill: $pastel-green;\n      stroke: $pastel-green;\n      stroke-width: 4px;\n    }\n  }\n}\n"
  },
  {
    "path": "client/components/AutocompleteInput/AutocompleteInput.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport AutoComplete from 'react-autocomplete'\n\nimport SearchIcon from '../Icons/SearchIcon'\nimport { parsePackageString } from '../../../utils/common.utils'\nimport { useAutocompleteInput } from './hooks/useAutocompleteInput'\nimport { SuggestionItem } from './components/SuggestionItem'\nimport { useFontSize } from './hooks/useFontSize'\n\ntype AutocompleteInputProps = {\n  initialValue?: string\n  renderAsH1?: boolean\n  className?: string\n  containerClass?: string\n  autoFocus?: boolean\n  onSearchSubmit: (value: string) => void\n}\n\nexport const AutocompleteInput = ({\n  initialValue = '',\n  renderAsH1 = false,\n  className,\n  containerClass,\n  autoFocus,\n  onSearchSubmit,\n}: AutocompleteInputProps) => {\n  const searchInput = React.useRef<AutoComplete | null>(null)\n  const {\n    value,\n    isMenuVisible,\n    suggestions,\n    handleSubmit,\n    handleInputChange,\n    setIsMenuVisible,\n    setSuggestions,\n  } = useAutocompleteInput({ initialValue, onSubmit: onSearchSubmit })\n  const { searchFontSize } = useFontSize({ value })\n\n  const { name, version } = React.useMemo(\n    () => parsePackageString(value),\n    [value]\n  )\n\n  return (\n    <form\n      className={cx(containerClass, 'autocomplete-input__form')}\n      onSubmit={handleSubmit}\n    >\n      <div\n        className={cx('autocomplete-input__container', className, {\n          'autocomplete-input__container--menu-visible':\n            isMenuVisible && !!suggestions.length,\n        })}\n      >\n        <AutoComplete\n          getItemValue={item => item.package.name}\n          inputProps={{\n            placeholder: 'find package',\n            className: 'autocomplete-input',\n            autoCorrect: 'off',\n            autoFocus: autoFocus,\n            autoCapitalize: 'off',\n            spellCheck: false,\n            style: { fontSize: searchFontSize! },\n          }}\n          onMenuVisibilityChange={isOpen => setIsMenuVisible(isOpen)}\n          onChange={handleInputChange}\n          ref={searchInput}\n          value={value}\n          items={suggestions}\n          onSelect={(value, item) => {\n            setSuggestions([item])\n            onSearchSubmit(value)\n          }}\n          renderMenu={(items, value, inbuiltStyles) => {\n            return (\n              <div\n                style={{ minWidth: inbuiltStyles.minWidth }}\n                className=\"autocomplete-input__suggestions-menu\"\n              >\n                {items as any}\n              </div>\n            )\n          }}\n          wrapperStyle={{\n            display: 'inline-block',\n            width: '100%',\n            position: 'relative',\n          }}\n          renderItem={(item, isHighlighted) => (\n            <div key={item.package.name}>\n              <SuggestionItem item={item} isHighlighted={isHighlighted} />\n            </div>\n          )}\n        />\n        <div\n          style={{ fontSize: searchFontSize! }}\n          className=\"autocomplete-input__dummy-input\"\n        >\n          <PackageNameElement\n            isHeading={renderAsH1}\n            className=\"dummy-input__package-name\"\n          >\n            {name}\n          </PackageNameElement>\n          {version !== null && (\n            <>\n              <span className=\"dummy-input__at-separator\">@</span>\n              <span className=\"dummy-input__package-version\">{version}</span>\n            </>\n          )}\n        </div>\n      </div>\n      <button type=\"submit\" className=\"autocomplete-input__search-icon\">\n        <SearchIcon className=\"\" />\n      </button>\n    </form>\n  )\n}\n\ntype PackageNameElementProps = React.HTMLAttributes<HTMLElement> & {\n  isHeading?: boolean\n}\n\nexport function PackageNameElement({\n  isHeading,\n  ...props\n}: PackageNameElementProps) {\n  return isHeading ? <h1 {...props} /> : <span {...props} />\n}\n"
  },
  {
    "path": "client/components/AutocompleteInput/components/SuggestionItem.tsx",
    "content": "import cx from 'classnames'\n\ninterface SuggestionItemProps {\n  item: {\n    highlight: string | null\n    package: {\n      name: string\n      description: string\n    }\n  }\n  isHighlighted: boolean\n}\n\nexport function SuggestionItem({ item, isHighlighted }: SuggestionItemProps) {\n  return (\n    <div\n      key={item.package.name}\n      className={cx('autocomplete-input__suggestion', {\n        'autocomplete-input__suggestion--highlight': isHighlighted,\n      })}\n    >\n      {item.highlight != null ? (\n        <div dangerouslySetInnerHTML={{ __html: item.highlight }} />\n      ) : (\n        <div>{item.package.name}</div>\n      )}\n\n      <div className=\"autocomplete-input__suggestion-description\">\n        {item.package.description}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "client/components/AutocompleteInput/hooks/useAutocompleteInput.ts",
    "content": "import React from 'react'\nimport debounce from 'debounce'\n\nimport { parsePackageString } from '../../../../utils/common.utils'\nimport API from '../../../api'\n\ninterface UseAutocompleteInputArgs {\n  initialValue: string\n  onSubmit: (value: string) => void\n}\n\nexport function useAutocompleteInput({\n  initialValue,\n  onSubmit,\n}: UseAutocompleteInputArgs) {\n  const [value, setValue] = React.useState(initialValue)\n  const [suggestions, setSuggestions] = React.useState<any[]>([])\n  const [isMenuVisible, setIsMenuVisible] = React.useState(false)\n\n  const getSuggestions = React.useMemo(\n    () =>\n      debounce((value: string) => {\n        API.getSuggestions(value).then(result => {\n          setSuggestions(result)\n        })\n      }, 150),\n    []\n  )\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault()\n    onSubmit(value)\n  }\n\n  const handleInputChange = (\n    e: React.ChangeEvent<HTMLInputElement>,\n    value: string\n  ) => {\n    setValue(e.target.value)\n    const trimmedValue = e.target.value.trim()\n    const { name } = parsePackageString(trimmedValue)\n\n    if (trimmedValue.length > 1) {\n      getSuggestions(name)\n    }\n\n    if (!trimmedValue) {\n      setSuggestions([])\n    }\n  }\n\n  return {\n    value,\n    suggestions,\n    isMenuVisible,\n    handleSubmit,\n    handleInputChange,\n    setIsMenuVisible,\n    setSuggestions,\n  }\n}\n"
  },
  {
    "path": "client/components/AutocompleteInput/hooks/useFontSize.ts",
    "content": "import React from 'react'\n\nexport function useFontSize({ value }: { value: string }) {\n  const searchFontSize = React.useMemo(() => {\n    const baseFontSize =\n      typeof window !== 'undefined' && window.innerWidth < 640 ? 22 : 35\n    const maxFullSizeChars =\n      typeof window !== 'undefined' && window.innerWidth < 640 ? 15 : 20\n    const searchFontSize =\n      value.length < maxFullSizeChars\n        ? null\n        : `${baseFontSize - (value.length - maxFullSizeChars) * 0.8}px`\n\n    return searchFontSize\n  }, [value])\n\n  return { searchFontSize }\n}\n"
  },
  {
    "path": "client/components/AutocompleteInput/index.ts",
    "content": "export { AutocompleteInput } from './AutocompleteInput'\n"
  },
  {
    "path": "client/components/AutocompleteInputBox/AutocompleteInputBox.scss",
    "content": "@import '../../../stylesheets/colors';\n@import '../../../stylesheets/variables';\n\n.autocomplete-input-box {\n  border: 1px solid $autocomplete-border-color;\n  border-radius: 10px;\n  background: transparent;\n  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);\n  max-width: 700px;\n  min-width: 550px;\n\n  @media screen and (max-width: 48em) {\n    width: 85vw;\n    max-width: 550px;\n    min-width: auto;\n  }\n\n  @media screen and (max-width: 40em) {\n    width: 85vw;\n    min-width: auto;\n  }\n}\n\n.autocomplete-input-box__footer {\n  position: relative;\n\n  &::after {\n    content: '';\n    position: absolute;\n    width: 80%;\n    margin: auto;\n    height: 1px;\n  }\n}\n"
  },
  {
    "path": "client/components/AutocompleteInputBox/AutocompleteInputBox.tsx",
    "content": "import React, { Component } from 'react'\nimport cx from 'classnames'\n\nimport { WithClassName } from '../../../types'\n\ntype AutocompleteInputBoxProps = React.PropsWithChildren &\n  WithClassName & {\n    footer?: React.ReactNode\n  }\n\nclass AutocompleteInputBox extends Component<AutocompleteInputBoxProps> {\n  render() {\n    const { children, footer, className } = this.props\n    return (\n      <div className={cx('autocomplete-input-box', className)}>\n        {children}\n        {footer && (\n          <div className=\"autocomplete-input-box__footer\">{footer}</div>\n        )}\n      </div>\n    )\n  }\n}\n\nexport default AutocompleteInputBox\n"
  },
  {
    "path": "client/components/AutocompleteInputBox/index.ts",
    "content": "import AutocompleteInputBox from './AutocompleteInputBox'\n\nexport default AutocompleteInputBox\n"
  },
  {
    "path": "client/components/BarGraph/BarGraph.scss",
    "content": "@use \"sass:math\";\n\n@import '../../../stylesheets/colors';\n@import '../../../stylesheets/variables';\n\n$bar-width: 1.6vw;\n$min-bar-width: 20px;\n$bar-grow-duration: 0.4s;\n\n.bar-graph-container {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  width: 100%;\n  height: 48vh;\n}\n\n.bar-graph {\n  height: 40vh;\n  padding-bottom: 6vh;\n  display: flex;\n  margin: 0;\n  justify-content: center;\n}\n\n.bar-graph__bar-group {\n  position: relative;\n  height: 100%;\n  margin: 0 3px;\n  display: flex;\n  width: $bar-width;\n  min-width: $min-bar-width;\n  justify-content: flex-end;\n  flex-direction: column;\n  animation: grow $bar-grow-duration cubic-bezier(0.305, 0.42, 0.205, 1.2);\n  transform-origin: 100% 100%;\n}\n\n.bar-graph__bar-symbols {\n  display: flex;\n  flex-direction: column;\n  margin-top: -500%; // don't know why this works :/\n}\n\n.bar-graph__bar-symbol {\n  text-align: center;\n\n  svg {\n    height: $global-spacing * 1.8;\n    width: auto;\n  }\n\n  & + & {\n    margin-top: math.div($global-spacing, 3);\n  }\n}\n\n.bar-graph__bar,\n.bar-graph__bar2,\n.bar-graph__bar[data-balloon],\n.bar-graph__bar2[data-balloon] {\n  width: 100%;\n  left: 0;\n  bottom: 0;\n  transition: background 0.2s;\n  cursor: pointer;\n}\n\n.bar-graph__bar,\n.bar-graph__bar[data-balloon] {\n  background: $maya-blue;\n  border-radius: 5px 5px 0 0;\n\n  .bar-graph__bar-group:not(.bar-graph__bar-group--disabled):hover & {\n    background: darken($maya-blue, 5%);\n  }\n\n  .bar-graph__bar-group--disabled & {\n    background: lighten($raven, 45%);\n    border-radius: 5px;\n\n    &:hover {\n      background: lighten($raven, 35%);\n    }\n  }\n}\n\n.bar-graph__bar2 {\n  background: $cornflower-blue;\n  z-index: 1;\n  pointer-events: none;\n  border-radius: 0 0 5px 5px;\n\n  .bar-graph__bar-group:hover & {\n    background: darken($cornflower-blue, 5%);\n  }\n}\n\n.bar-graph__bar-version,\n.bar-graph__bar-symbols,\n.bar-graph__legend {\n  animation: fade-in 0.5s $bar-grow-duration * 0.9 both\n    cubic-bezier(0.305, 0.42, 0.205, 1.2);\n}\n\n.bar-graph__legend {\n  @include font-size-xs;\n  padding-top: $global-spacing;\n  display: flex;\n  text-transform: uppercase;\n  justify-content: center;\n  color: lighten($raven, 20%);\n}\n\n.bar-graph__legend__colorbox {\n  width: $global-spacing * 1.5;\n  height: $global-spacing * 1.5;\n  margin-right: $global-spacing;\n  border-radius: 3px;\n\n  .bar-graph__legend__bar1 & {\n    background: $maya-blue;\n  }\n\n  .bar-graph__legend__bar2 & {\n    background: $cornflower-blue;\n  }\n}\n\n.bar-graph__legend__bar1,\n.bar-graph__legend__bar2 {\n  display: flex;\n  align-items: center;\n}\n\n.bar-graph__legend__bar1 {\n  margin-right: $global-spacing * 4;\n}\n\n@keyframes grow {\n  from {\n    transform: scaleY(0);\n  }\n\n  to {\n    transform: scaleY(1);\n  }\n}\n\n@keyframes fade-in {\n  from {\n    opacity: 0;\n  }\n}\n"
  },
  {
    "path": "client/components/BarGraph/BarGraph.tsx",
    "content": "import React, { PureComponent } from 'react'\n\nimport { formatSize } from '../../../utils'\nimport TreeShakeIcon from '../Icons/TreeShakeIcon'\nimport SideEffectIcon from '../Icons/SideEffectIcon'\nimport { BarVersion } from '../BarVersion/BarVersion'\n\nexport type Reading = {\n  version: string\n  size: number\n  gzip: number\n  disabled: boolean\n  hasSideEffects: boolean\n  hasJSModule: boolean\n  hasJSNext: boolean\n  isModuleType: boolean\n}\n\ntype BarGraphProps = {\n  readings: Reading[]\n  onBarClick: (reading: Reading) => void\n}\n\nexport default class BarGraph extends PureComponent<BarGraphProps> {\n  getScale = () => {\n    const { readings } = this.props\n\n    const gzipValues = readings\n      .filter(reading => !reading.disabled)\n      .map(reading => reading.gzip)\n\n    const sizeValues = readings\n      .filter(reading => !reading.disabled)\n      .map(reading => reading.size)\n\n    const maxValue = Math.max(...[...gzipValues, ...sizeValues])\n    return 100 / maxValue\n  }\n\n  getFirstSideEffectFreeIndex = () => {\n    const { readings } = this.props\n    const sideEffectFreeIntroducedRecently = !readings.every(\n      reading => !reading.hasSideEffects\n    )\n    const firstSideEffectFreeIndex = readings.findIndex(\n      reading => !(reading.disabled || reading.hasSideEffects)\n    )\n\n    return sideEffectFreeIntroducedRecently ? firstSideEffectFreeIndex : -1\n  }\n\n  getFirstTreeshakeableIndex = () => {\n    const { readings } = this.props\n    const treeshakingIntroducedRecently = !readings.every(\n      reading => reading.hasJSModule\n    )\n    const firstTreeshakingIndex = readings.findIndex(\n      reading =>\n        !reading.disabled &&\n        (reading.hasJSModule || reading.hasJSNext || reading.isModuleType)\n    )\n\n    return treeshakingIntroducedRecently ? firstTreeshakingIndex : -1\n  }\n\n  renderDisabledBar = (reading: Reading) => (\n    <div\n      key={reading.version}\n      className=\"bar-graph__bar-group bar-graph__bar-group--disabled\"\n      onClick={() => this.props.onBarClick(reading)}\n    >\n      <div\n        className=\"bar-graph__bar\"\n        style={{ height: `${50}%` }}\n        data-balloon=\"Unknown | Click 👆 to build\"\n      />\n      <BarVersion version={reading.version} />\n    </div>\n  )\n\n  renderActiveBar = (\n    reading: Reading,\n    scale: number,\n    options: { isFirstTreeshakeable: boolean; isFirstSideEffectFree: boolean }\n  ) => {\n    const getTooltipMessage = (reading: Reading) => {\n      const formattedSize = formatSize(reading.size)\n      const formattedGzip = formatSize(reading.gzip)\n      return `Minified: ${parseFloat(formattedSize.size).toFixed(1)}${\n        formattedSize.unit\n      } | Gzipped: ${parseFloat(formattedGzip.size).toFixed(1)}${\n        formattedGzip.unit\n      }`\n    }\n\n    return (\n      <div\n        onClick={() => this.props.onBarClick(reading)}\n        key={reading.version}\n        className=\"bar-graph__bar-group\"\n      >\n        <div className=\"bar-graph__bar-symbols\">\n          {options.isFirstTreeshakeable && (\n            <div\n              data-balloon={`ES2015 exports introduced. ${\n                reading.hasSideEffects\n                  ? 'Not side-effect free yet, hence limited tree-shake ability.'\n                  : ''\n              }`}\n              className=\"bar-graph__bar-symbol\"\n            >\n              <TreeShakeIcon />\n            </div>\n          )}\n          {options.isFirstSideEffectFree && (\n            <div\n              data-balloon={`Was marked side-effect free. ${\n                reading.hasJSNext || reading.hasJSModule || reading.isModuleType\n                  ? 'Supports ES2015 exports also, hence fully tree-shakeable'\n                  : \"Doesn't export ESM yet, limited tree-shake ability\"\n              }`}\n              className=\"bar-graph__bar-symbol\"\n            >\n              <SideEffectIcon />\n            </div>\n          )}\n        </div>\n\n        <div\n          className=\"bar-graph__bar\"\n          style={{ height: `${(reading.size - reading.gzip) * scale}%` }}\n          data-balloon={getTooltipMessage(reading)}\n        />\n        <div\n          className=\"bar-graph__bar2\"\n          style={{ height: `${reading.gzip * scale}%` }}\n          data-balloon={getTooltipMessage(reading)}\n        />\n        <BarVersion version={reading.version} />\n      </div>\n    )\n  }\n\n  render() {\n    const { readings } = this.props\n    const graphScale = this.getScale()\n    const firstTreeshakeableIndex = this.getFirstTreeshakeableIndex()\n    const firstSideEffectFreeIndex = this.getFirstSideEffectFreeIndex()\n\n    return (\n      <div className=\"bar-graph-container\">\n        <figure className=\"bar-graph\">\n          {readings.map((reading, index) =>\n            reading.disabled\n              ? this.renderDisabledBar(reading)\n              : this.renderActiveBar(reading, graphScale, {\n                  isFirstTreeshakeable: index === firstTreeshakeableIndex,\n                  isFirstSideEffectFree: index === firstSideEffectFreeIndex,\n                })\n          )}\n        </figure>\n        <div className=\"bar-graph__legend\">\n          <div className=\"bar-graph__legend__bar1\">\n            <div className=\"bar-graph__legend__colorbox\" />\n            Min\n          </div>\n          <div className=\"bar-graph__legend__bar2\">\n            <div className=\"bar-graph__legend__colorbox\" />\n            GZIP\n          </div>\n        </div>\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "client/components/BarGraph/index.ts",
    "content": "import BarGraph from './BarGraph'\n\nexport default BarGraph\n"
  },
  {
    "path": "client/components/BarVersion/BarVersion.scss",
    "content": "@import '../../../stylesheets/colors';\n@import '../../../stylesheets/variables';\n\n.bar-graph__bar-version {\n  @include font-size-xs;\n  z-index: 33;\n  font-weight: $font-weight-light;\n  transform: rotate(-90deg) translateX(#{-$global-spacing * 1.5});\n  font-variant-numeric: tabular-nums;\n  color: lighten($raven, 15%);\n  transition: opacity 0.2s, color 0.2s;\n  font-family: $font-family-code;\n  letter-spacing: -1px;\n  line-height: 1;\n  cursor: pointer;\n  height: $bar-width;\n  text-align: end;\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n\n  .bar-graph-container:hover & {\n    color: $raven;\n  }\n\n  .bar-graph__bar-group:hover & {\n    color: darken($raven, 20%);\n  }\n}\n"
  },
  {
    "path": "client/components/BarVersion/BarVersion.tsx",
    "content": "import truncate from 'truncate'\n\ninterface Props {\n  version: string\n}\n\nexport function BarVersion({ version }: Props) {\n  return (\n    <div className=\"bar-graph__bar-version\">\n      <div>\n        <span title={version}>{truncate(version, 7)}</span>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "client/components/BlogLayout/BlogLayout.scss",
    "content": "@import '../../../stylesheets/colors';\n@import '../../../stylesheets/variables';\n\n.page-container.blog {\n  .page-content {\n    justify-content: normal;\n  }\n}\n\n.blog-layout__container {\n  max-width: 80ch;\n  padding: 0 2rem;\n  width: 100%;\n\n  h1 {\n    margin-bottom: 0;\n  }\n\n  img,\n  iframe {\n    max-width: 100%;\n    object-fit: contain;\n  }\n}\n\n.blog-post__preview-read-more {\n  @include font-size-xs();\n  letter-spacing: 1px;\n  color: $gulf-blue;\n  font-weight: $font-weight-bold;\n  text-transform: uppercase;\n\n  &:hover {\n    color: lighten($gulf-blue, 20%);\n  }\n}\n\n.blog-post__preview {\n  h2 {\n    font-weight: $dark-gulf-blue;\n    color: #5c5c66;\n    margin: 0;\n\n    &:hover {\n      color: lighten($gulf-blue, 10%);\n    }\n  }\n\n  & + & {\n    margin-top: 4rem;\n  }\n}\n\n.blog-post__preview-content {\n  color: darken($dark-raven, 10%);\n  line-height: 1.6;\n  @include font-size-reg();\n\n  a {\n    color: darken($maya-blue, 20%);\n    border-bottom: 1px solid $cornflower-blue;\n    transition: all 0.2s;\n\n    &:hover {\n      color: darken($maya-blue, 40%);\n      border-bottom: 1px dashed $cornflower-blue;\n    }\n  }\n}\n\n.blog-post__preview-date {\n  @include font-size-sm;\n  margin: 0.7rem 0 0;\n  color: $raven;\n  font-weight: $font-weight-light;\n}\n"
  },
  {
    "path": "client/components/BlogLayout/BlogLayout.tsx",
    "content": "import React from 'react'\n\nimport { WithClassName } from '../../../types'\nimport ResultLayout from '../ResultLayout'\n\ntype BlogLayoutProps = React.PropsWithChildren & WithClassName\n\nconst BlogLayout = ({ className, children }: BlogLayoutProps) => (\n  <ResultLayout className={className}>\n    <div className=\"blog-layout__container\">{children}</div>\n  </ResultLayout>\n)\n\nexport default BlogLayout\n"
  },
  {
    "path": "client/components/BlogLayout/index.ts",
    "content": "export { default } from './BlogLayout'\n"
  },
  {
    "path": "client/components/BuildProgressIndicator/BuildProgressIndicator.scss",
    "content": "@import '../../../stylesheets/colors';\n@import '../../../stylesheets/variables';\n\n.build-progress-indicator {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  flex-direction: column;\n  flex-grow: 1;\n  padding: 0 $global-spacing * 2;\n  text-align: center;\n}\n\n.build-progress-indicator__text {\n  font-size: 0.7rem;\n  margin-top: $global-spacing * 3;\n  color: lighten($raven, 15%);\n  text-transform: uppercase;\n  font-weight: $font-weight-bold;\n  letter-spacing: 2px;\n  line-height: 1.5;\n}\n"
  },
  {
    "path": "client/components/BuildProgressIndicator/BuildProgressIndicator.tsx",
    "content": "import React, { Component } from 'react'\n\nimport ProgressHex from '../ProgressHex'\n\nconst OptimisticLoadTimeout = 700\n\ntype BuildProgressIndicatorProps = {\n  isDone: boolean\n  onDone: () => void\n}\n\ntype BuildProgressIndicatorState = {\n  started: boolean\n  progressText?: string\n}\n\nconst order = ['resolving', 'building', 'minifying', 'calculating'] as const\n\nexport default class BuildProgressIndicator extends Component<\n  BuildProgressIndicatorProps,\n  BuildProgressIndicatorState\n> {\n  stage: number\n  timeoutId?: ReturnType<typeof setTimeout>\n\n  constructor(props: BuildProgressIndicatorProps) {\n    super(props)\n    this.stage = 0\n    this.state = {\n      started: false,\n    }\n  }\n\n  componentDidMount() {\n    setTimeout(() => {\n      if (!this.props.isDone) {\n        this.setState({ started: true })\n        this.setMessage()\n      }\n    }, OptimisticLoadTimeout)\n  }\n\n  componentWillReceiveProps(nextProps: BuildProgressIndicatorProps) {\n    if (nextProps.isDone) {\n      this.stage = 3\n      this.props.onDone()\n    }\n  }\n\n  shouldComponentUpdate(\n    props: BuildProgressIndicatorProps,\n    nextState: BuildProgressIndicatorState\n  ) {\n    return this.state.progressText !== nextState.progressText\n  }\n\n  componentWillUnmount() {\n    clearTimeout(this.timeoutId)\n  }\n\n  getProgressText = (stage: typeof order[number]) => {\n    const progressText = {\n      resolving: 'Resolving version and dependencies',\n      building: 'Bundling package',\n      minifying: 'Minifying, GZipping',\n      calculating: 'Calculating file sizes',\n    }\n    return progressText[stage]\n  }\n\n  setMessage = (stage = 0) => {\n    const timings = {\n      resolving: 3 + Math.random() * 2,\n      building: 5 + Math.random() * 3,\n      minifying: 3 + Math.random() * 2,\n      calculating: 20,\n    }\n\n    if (this.stage === order.length) {\n      //this.props.onDone()\n      return\n    }\n\n    this.setState({\n      progressText: this.getProgressText(order[this.stage]),\n    })\n\n    this.timeoutId = setTimeout(() => {\n      if (this.stage < order.length) {\n        this.stage += 1\n      }\n\n      this.setMessage(this.stage)\n    }, timings[order[stage]] * 1000)\n  }\n\n  render() {\n    const { progressText, started } = this.state\n    if (!started) {\n      return null\n    }\n\n    return (\n      <div className=\"build-progress-indicator\">\n        <ProgressHex compact />\n        <p className=\"build-progress-indicator__text\">{progressText}</p>\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "client/components/BuildProgressIndicator/index.ts",
    "content": "import BuildProgressIndicator from './BuildProgressIndicator'\n\nexport default BuildProgressIndicator\n"
  },
  {
    "path": "client/components/Header/Header.tsx",
    "content": "import React, { Component } from 'react'\nimport Sidebar from 'react-sidebar'\nimport Link from 'next/link'\n\nimport { WithClassName } from '../../../types'\nimport GithubLogo from '../../assets/github-logo.svg'\n\ntype HeaderProps = WithClassName\n\ntype HeaderState = {\n  sidebarDocked: boolean\n  sidebarOpen: boolean\n}\n\nexport default class Header extends Component<HeaderProps, HeaderState> {\n  mql!: MediaQueryList\n\n  constructor(props: HeaderProps) {\n    super(props)\n    this.state = {\n      sidebarDocked: false,\n      sidebarOpen: false,\n    }\n  }\n\n  componentDidMount() {\n    this.mql = window.matchMedia(`(min-width: 800px)`)\n    this.setState({ sidebarDocked: this.mql.matches })\n    this.mql.addListener(this.mediaQueryChanged)\n  }\n\n  componentWillUnmount() {\n    this.mql.removeListener(this.mediaQueryChanged)\n  }\n\n  onSetSidebarOpen(open: boolean) {\n    this.setState({ sidebarOpen: open })\n  }\n\n  mediaQueryChanged() {\n    this.setState({ sidebarDocked: this.mql.matches, sidebarOpen: false })\n  }\n\n  render() {\n    return (\n      <Sidebar\n        sidebar={<b>Sidebar content</b>}\n        open={this.state.sidebarOpen}\n        docked={this.state.sidebarDocked}\n        onSetOpen={this.onSetSidebarOpen}\n      >\n        <header className=\"page-header\">\n          <section className=\"result-header--left-section\">\n            <Link href=\"/\">\n              <div className=\"logo-small\">\n                <span>Bundle</span>\n                <span className=\"logo-small__alt\">Phobia</span>\n              </div>\n            </Link>\n          </section>\n          <section className=\"page-header--right-section\">\n            <ul className=\"page-header__quicklinks\">\n              <li>\n                <a\n                  target=\"_blank\"\n                  rel=\"noreferrer noopener\"\n                  href=\"https://badgen.net/#bundlephobia\"\n                >\n                  Badges\n                </a>\n              </li>\n              <li>\n                <a\n                  target=\"_blank\"\n                  rel=\"noreferrer noopener\"\n                  href=\"https://github.com/sponsors/pastelsky\"\n                >\n                  Sponsor\n                </a>\n              </li>\n              <li>\n                <Link href=\"/scan\">\n                  Scan package.json <sup>β</sup>\n                </Link>\n              </li>\n            </ul>\n            <a target=\"_blank\" href=\"https://github.com/pastelsky/bundlephobia\">\n              <GithubLogo />\n            </a>\n          </section>\n        </header>\n        <b>Main content</b>\n      </Sidebar>\n    )\n  }\n}\n"
  },
  {
    "path": "client/components/Header/index.ts",
    "content": "import Header from './Header'\n\nexport default Header\n"
  },
  {
    "path": "client/components/Icons/SearchIcon.tsx",
    "content": "import React from 'react'\n\nimport { WithClassName } from '../../../types'\n\nexport default function SearchIcon({ className }: WithClassName) {\n  return (\n    <svg\n      width=\"90\"\n      height=\"90\"\n      viewBox=\"0 0 90 90\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={className}\n    >\n      <path d=\"M89.32 86.5L64.25 61.4C77.2 47 76.75 24.72 62.87 10.87 55.93 3.92 46.7.1 36.87.1s-19.06 3.82-26 10.77C3.92 17.8.1 27.05.1 36.87s3.82 19.06 10.77 26c6.94 6.95 16.18 10.77 26 10.77 9.15 0 17.8-3.32 24.55-9.4l25.08 25.1c.38.4.9.57 1.4.57.52 0 1.03-.2 1.42-.56.78-.78.78-2.05 0-2.83zM36.87 69.63c-8.75 0-16.98-3.4-23.17-9.6-6.2-6.2-9.6-14.42-9.6-23.17 0-8.75 3.4-16.98 9.6-23.17 6.2-6.2 14.42-9.6 23.17-9.6 8.75 0 16.98 3.4 23.18 9.6 12.77 12.75 12.77 33.55 0 46.33-6.2 6.2-14.43 9.6-23.18 9.6z\" />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "client/components/Icons/SideEffectIcon.scss",
    "content": "svg.sideeffect-icon-animated {\n  overflow: hidden;\n\n  .side-effect-icon-svg__circle,\n  .side-effect-icon-svg__arrows {\n    transform-origin: 50% 50%;\n    transition: all 0.2s;\n  }\n\n  &:hover {\n    .side-effect-icon-svg__arrows {\n      transform: scale(1.3);\n      stroke-width: 0.3px;\n    }\n\n    .side-effect-icon-svg__circle {\n      transform: scale(1.2);\n      stroke-width: 0.6px;\n    }\n  }\n}\n\n@keyframes shrink-arrows {\n  from {\n    transform: scale(2);\n  }\n\n  to {\n    transform: scale(1);\n  }\n}\n\n@keyframes grow-circle {\n  from {\n    transform: scale(1.5);\n  }\n\n  to {\n    transform: scale(1);\n  }\n}\n"
  },
  {
    "path": "client/components/Icons/SideEffectIcon.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\n\nimport { WithClassName } from '../../../types'\nimport SideEffectIconSVG from '../../assets/side-effect.svg'\n\nexport default function TreeShakeIcon({ className }: WithClassName) {\n  return (\n    <SideEffectIconSVG className={cx(className, 'sideeffect-icon-animated')} />\n  )\n}\n"
  },
  {
    "path": "client/components/Icons/TreeShakeIcon.scss",
    "content": "svg.treeshake-icon-animated {\n  .tree-shake-icon-svg__bush {\n    transition: transform 0.3s;\n    transform-origin: 50% 100%;\n  }\n\n  &:hover {\n    .tree-shake-icon-svg__shake {\n      transform-origin: 50% 50%;\n      animation: move-to-sides 0.3s, shake 0.3s 0.15s;\n    }\n\n    .tree-shake-icon-svg__bush {\n      transform: scaleY(1.2);\n    }\n  }\n}\n\n@keyframes move-to-sides {\n  from {\n    transform: scale(0);\n  }\n\n  to {\n    transform: scale(1);\n  }\n}\n\n@keyframes shake {\n  10%,\n  100% {\n    transform: translate3d(-0.5px, 0, 0);\n  }\n\n  80% {\n    transform: translate3d(1px, 0, 0);\n  }\n\n  30%,\n  70% {\n    transform: translate3d(-1px, 0, 0);\n  }\n\n  60% {\n    transform: translate3d(1px, 0, 0);\n  }\n}\n"
  },
  {
    "path": "client/components/Icons/TreeShakeIcon.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\n\nimport { WithClassName } from '../../../types'\nimport TreeShakeIconSVG from '../../assets/tree-shake.svg'\n\nexport default function TreeShakeIcon({ className }: WithClassName) {\n  return (\n    <TreeShakeIconSVG className={cx(className, 'treeshake-icon-animated')} />\n  )\n}\n"
  },
  {
    "path": "client/components/JumpingDots/JumpingDots.scss",
    "content": "@import '../../../stylesheets/variables';\n\n.jumping-dots {\n  position: relative;\n  text-align: center;\n  padding: 0 $global-spacing * 0.5;\n}\n\n.jumping-dots__dot {\n  display: inline-block;\n  width: 2px;\n  height: 2px;\n  border-radius: 50%;\n  margin-right: 3px;\n  background: #303131;\n  animation: dots-wave 1s linear infinite;\n\n  &:nth-child(2) {\n    animation-delay: -0.9s;\n  }\n\n  &:nth-child(3) {\n    animation-delay: -0.8s;\n  }\n}\n\n@keyframes dots-wave {\n  0%,\n  60%,\n  100% {\n    transform: initial;\n  }\n\n  30% {\n    transform: translateY(-8px);\n  }\n}\n"
  },
  {
    "path": "client/components/JumpingDots/JumpingDots.tsx",
    "content": "import React from 'react'\n\nexport default function JumpingDots() {\n  return (\n    <span className=\"jumping-dots\">\n      <span className=\"jumping-dots__dot\" />\n      <span className=\"jumping-dots__dot\" />\n      <span className=\"jumping-dots__dot\" />\n    </span>\n  )\n}\n"
  },
  {
    "path": "client/components/JumpingDots/index.ts",
    "content": "export { default } from './JumpingDots'\n"
  },
  {
    "path": "client/components/Layout/Layout.scss",
    "content": "@use \"sass:math\";\n\n@import '../../../stylesheets/variables';\n@import '../../../stylesheets/base';\n@import '../../../stylesheets/colors';\n\n$footer-content-max-width: 800px;\n\n.layout {\n  max-width: 100%;\n}\n\nfooter {\n  background: #222;\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 0 0 $global-spacing * 5;\n  color: lighten($raven, 27%);\n  flex-direction: column;\n\n  a {\n    color: lighten($raven, 27%);\n    transition: color 0.2s;\n\n    &:hover {\n      color: lighten($raven, 40%);\n    }\n  }\n}\n\n.footer__recent-search-bar {\n  width: 100%;\n  background: darken($raven, 22%);\n  padding: 0 $global-spacing * 2;\n}\n\n.footer__recent-search-bar__wrap {\n  max-width: $footer-content-max-width;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  margin: auto;\n\n  h4 {\n    @include font-size-xs;\n    color: lighten($raven, 27%);\n    margin: 0;\n    line-height: 1.2;\n  }\n}\n\n.footer__recent-search-list {\n  @include font-size-xs;\n\n  display: flex;\n  padding: 0;\n  margin: 0 0 0 $global-spacing * 1.5;\n  flex-grow: 1;\n  max-width: 960px;\n\n  @media screen and (max-width: 40em) {\n    margin: 0;\n  }\n\n  li {\n    list-style: none;\n    position: relative;\n    flex-grow: 1;\n    text-align: center;\n    font-family: $font-family-code;\n    letter-spacing: 0.5px;\n\n    a {\n      padding: $global-spacing $global-spacing * 2;\n\n      @media screen and (max-width: 48em) {\n        padding: $global-spacing $global-spacing * 0.5;\n      }\n    }\n\n    &:not(:first-of-type) {\n      &::after {\n        content: '';\n        width: 1px;\n        height: 60%;\n        background: rgba(255, 255, 255, 0.1);\n        position: absolute;\n        left: 0;\n        top: 0;\n        bottom: 0;\n        margin: auto;\n      }\n    }\n\n    @media screen and (max-width: 48em) {\n      &:nth-child(n + 5) {\n        display: none;\n      }\n    }\n\n    @media screen and (max-width: 40em) {\n      &:nth-child(n + 4) {\n        display: none;\n      }\n    }\n  }\n}\n\n.footer__split {\n  display: flex;\n  max-width: $footer-content-max-width;\n  padding: $global-spacing * 3 $global-spacing;\n  margin: auto;\n\n  @media screen and (max-width: 40em) {\n    padding: $global-spacing * 3 $global-spacing * 2.5;\n    flex-direction: column;\n  }\n}\n\n.footer__hosting-credits {\n  @include font-size-xs;\n  border-top: 1px solid darken($raven, 25%);\n  color: lighten($raven, 70%);\n  text-transform: uppercase;\n  letter-spacing: 2px;\n  padding-top: $global-spacing;\n  padding-right: $global-spacing;\n  font-size: 12px;\n  width: $global-spacing * 20;\n\n  @media screen and (max-width: 40em) {\n    text-align: center;\n    padding-top: $global-spacing * 1.5;\n    margin: $global-spacing * 3 auto auto;\n  }\n}\n\n.footer__zeit-logo {\n  width: $global-spacing;\n  height: $global-spacing;\n  margin: 0 $global-spacing * 0.5;\n}\n\n.footer__credits {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  flex-basis: 33%;\n\n  p {\n    margin: 0;\n  }\n\n  @media screen and (max-width: 40em) {\n    margin-top: $global-spacing * 2;\n  }\n}\n\n.footer__description {\n  @include font-size-xs;\n  color: lighten($raven, 20%);\n  flex-basis: 66%;\n\n  p {\n    text-align: left;\n    line-height: 1.4;\n\n    a {\n      display: inline;\n    }\n\n    code {\n      font-family: $font-family-code;\n      padding: 0 math.div($global-spacing, 3) 0 $global-spacing;\n      opacity: 0.9;\n    }\n  }\n}\n\n.footer__credits__heart {\n  width: 8vw;\n  height: 8vw;\n  max-width: 100px;\n\n  path {\n    fill: mix($raven, $maya-blue);\n  }\n\n  &:hover {\n    path {\n      animation: pulse 10s infinite both;\n    }\n  }\n}\n\n@keyframes pulse {\n  0% {\n    fill: mix($raven, $maya-blue);\n  }\n\n  25% {\n    fill: mix($raven, $cornflower-blue);\n  }\n\n  50% {\n    fill: $raven;\n  }\n\n  75% {\n    fill: mix($raven, $maya-blue);\n  }\n}\n\n.footer__credits-fork-button {\n  @include font-size-xs;\n  cursor: pointer;\n  margin-top: $global-spacing;\n  border: 2px solid lighten($raven, 30%);\n  background: transparent;\n  border-radius: 10px;\n  padding: math.div($global-spacing, 1.5) $global-spacing;\n  display: block;\n  transition: background 0.2s;\n  color: lighten($raven, 30%);\n  text-transform: uppercase;\n  letter-spacing: 1.5px;\n  font-size: 10px;\n  font-weight: $font-weight-bold;\n\n  &:hover {\n    background: lighten($raven, 30%);\n    color: #212121;\n  }\n\n  @media screen and (max-width: 48em) {\n    padding: $global-spacing $global-spacing * 2;\n    margin-top: $global-spacing * 2;\n  }\n}\n\n.footer__credits-profile {\n  margin-top: -$global-spacing * 1.5;\n  margin-bottom: $global-spacing * 0.5;\n}\n\n.footer__sponsor-logo {\n  margin-top: $global-spacing;\n}\n\nfooter {\n  p {\n    text-align: center;\n  }\n\n  a {\n    display: block;\n    text-decoration: none;\n  }\n}\n"
  },
  {
    "path": "client/components/Layout/Layout.tsx",
    "content": "import React, { Component } from 'react'\nimport Link from 'next/link'\n\nimport API from '../../api'\nimport Heart from '../../assets/heart.svg'\nimport DigitalOceanLogo from '../../assets/digital-ocean-logo.svg'\nimport { AnnouncementBanner } from '../AnnouncementBanner'\nimport { WithClassName } from '../../../types'\n\ntype LayoutProps = React.PropsWithChildren & WithClassName\n\ntype LayoutState = {\n  recentSearches: string[]\n}\n\nexport default class Layout extends Component<LayoutProps, LayoutState> {\n  state = {\n    recentSearches: [],\n  }\n\n  componentDidMount() {\n    API.getRecentSearches(5).then(searches => {\n      this.setState({\n        recentSearches: Object.keys(searches),\n      })\n    })\n  }\n\n  render() {\n    const { children, className } = this.props\n    const { recentSearches } = this.state\n\n    return (\n      <section className=\"layout\">\n        <AnnouncementBanner />\n        <section className={className}>{children}</section>\n\n        <footer>\n          <div className=\"footer__recent-search-bar\">\n            <div className=\"footer__recent-search-bar__wrap\">\n              <h4>Recent searches</h4>\n              <ul className=\"footer__recent-search-list\">\n                {recentSearches.map(search => (\n                  <li key={search}>\n                    <Link href={`/package/${search}`}>{search}</Link>\n                  </li>\n                ))}\n              </ul>\n            </div>\n          </div>\n          <section className=\"footer__split\">\n            <div className=\"footer__description\">\n              <h3> What does Bundlephobia do? </h3>\n              <p>\n                JavaScript bloat is more real today than it ever was. Sites\n                continuously get bigger as more (often redundant) libraries are\n                thrown to solve new problems. Until of-course, the{' '}\n                <i> big rewrite </i>\n                happens.\n              </p>\n              <p>\n                Bundlephobia lets you understand the performance cost of\n                <code>npm&nbsp;install</code> ing a new npm package before it\n                becomes a part of your bundle. Analyze size, compositions and\n                exports\n              </p>\n              <p>\n                Credits to{' '}\n                <a href=\"https://twitter.com/thekitze\" target=\"_blank\">\n                  {' '}\n                  @thekitze{' '}\n                </a>\n                for the name.\n              </p>\n              <div className=\"footer__hosting-credits\">\n                Hosted on\n                <a href=\"https://digitalocean.com\" target=\"_blank\">\n                  <DigitalOceanLogo className=\"footer__sponsor-logo\" />\n                </a>\n              </div>\n            </div>\n            <div className=\"footer__credits\">\n              <Heart className=\"footer__credits__heart\" />️\n              <a\n                className=\"footer__credits-profile\"\n                target=\"_blank\"\n                href=\"https://github.com/pastelsky\"\n              >\n                @pastelsky\n              </a>\n              <a\n                target=\"_blank\"\n                href=\"https://github.com/pastelsky/bundlephobia\"\n              >\n                <button className=\"footer__credits-fork-button\">\n                  Star on GitHub\n                </button>\n              </a>\n            </div>\n          </section>\n        </footer>\n      </section>\n    )\n  }\n}\n"
  },
  {
    "path": "client/components/Layout/index.ts",
    "content": "import Layout from './Layout'\n\nexport default Layout\n"
  },
  {
    "path": "client/components/MetaTags.tsx",
    "content": "import React from 'react'\nimport Head from 'next/head'\n\nexport const DEFAULT_DESCRIPTION_START =\n  'Bundlephobia helps you find the performance impact of npm packages.'\n\ntype MetaTagsProps = {\n  title: string\n  canonicalPath: string\n  description?: string\n  twitterDescription?: string\n  image?: string\n  isLargeImage?: boolean\n}\n\nexport default function MetaTags({\n  description,\n  twitterDescription,\n  title,\n  canonicalPath,\n  image,\n  isLargeImage,\n}: MetaTagsProps) {\n  const defaultDescription = `${DEFAULT_DESCRIPTION_START} Find the size of any javascript package and its effect on your frontend bundle.`\n  const defaultImage = 'https://bundlephobia.com/android-chrome-256x256.png'\n  const origin =\n    typeof window === 'undefined'\n      ? 'https://bundlephobia.com'\n      : window.location.origin\n\n  return (\n    <Head>\n      <title>{title}</title>\n      <meta name=\"description\" content={description || defaultDescription} />\n      <meta property=\"og:title\" key=\"og:title\" content={title} />\n      <meta\n        property=\"og:description\"\n        key=\"og:description\"\n        content={description || defaultDescription}\n      />\n      <meta property=\"og:type\" key=\"og:type\" content=\"website\" />\n      <meta property=\"og:url\" key=\"og:url\" content={origin + canonicalPath} />\n      <meta\n        property=\"og:image\"\n        key=\"og:image\"\n        content={image || defaultImage}\n      />\n      <meta\n        property=\"twitter:creator\"\n        key=\"twitter:creator\"\n        content=\"@_pastelsky\"\n      />\n      {twitterDescription && (\n        <meta\n          property=\"twitter:description\"\n          key=\"twitter:description\"\n          content={twitterDescription}\n        />\n      )}\n      {isLargeImage ? (\n        <meta\n          name=\"twitter:card\"\n          key=\"twitter:card\"\n          content=\"summary_large_image\"\n        />\n      ) : (\n        <meta name=\"twitter:card\" content=\"summary\" key=\"summary\" />\n      )}\n      <link rel=\"canonical\" href={origin + canonicalPath} />\n    </Head>\n  )\n}\n"
  },
  {
    "path": "client/components/PageNav/PageNav.tsx",
    "content": "import Link from 'next/link'\nimport React from 'react'\nimport GithubLogo from '../../assets/github-logo.svg'\n\ntype PageNavProps = {\n  minimal?: boolean\n}\n\nconst PageNav = ({ minimal }: PageNavProps) => (\n  <header className=\"page-header\">\n    {!minimal && (\n      <section className=\"result-header--left-section\">\n        <Link href=\"/\">\n          <div className=\"logo-small\">\n            <span>Bundle</span>\n            <span className=\"logo-small__alt\">Phobia</span>\n          </div>\n        </Link>\n      </section>\n    )}\n    <section className=\"page-header--right-section\">\n      <ul className=\"page-header__quicklinks\">\n        <li>\n          <a\n            target=\"_blank\"\n            rel=\"noreferrer noopener\"\n            href=\"https://badgen.net/#bundlephobia\"\n          >\n            Badges\n          </a>\n        </li>\n        <li>\n          <a\n            target=\"_blank\"\n            rel=\"noreferrer noopener\"\n            href=\"https://github.com/sponsors/pastelsky\"\n          >\n            Sponsor\n          </a>\n        </li>\n        <li>\n          <Link href=\"/blog\">Blog</Link>\n        </li>\n        {!minimal && (\n          <li>\n            <Link href=\"/scan\">Scan package.json</Link>\n          </li>\n        )}\n      </ul>\n      <a target=\"_blank\" href=\"https://github.com/pastelsky/bundlephobia\">\n        <GithubLogo />\n      </a>\n    </section>\n  </header>\n)\n\nexport default PageNav\n"
  },
  {
    "path": "client/components/PageNav/index.ts",
    "content": "export { default } from './PageNav'\n"
  },
  {
    "path": "client/components/ProgressHex/ProgressHex.scss",
    "content": ".progress-hex {\n  width: 8rem;\n  height: 8rem;\n  contain: strict;\n  will-change: transform;\n\n  circle {\n    fill: #212121;\n    transform-box: view-box;\n    transform-origin: 50% 50%;\n  }\n}\n\n.progress-hex__trail {\n  stroke-width: 1px;\n}\n"
  },
  {
    "path": "client/components/ProgressHex/ProgressHex.tsx",
    "content": "import React, { Component } from 'react'\nimport ProgressHexAnimator from './progress-hex-timeline'\n\ntype ProgressHexProps = {\n  compact?: boolean\n}\n\nclass ProgressHex extends Component<ProgressHexProps> {\n  svgRef: React.RefObject<SVGSVGElement>\n  animator?: ProgressHexAnimator\n  timeline?: ReturnType<ProgressHexAnimator['createTimeline']>\n\n  constructor(props: ProgressHexProps) {\n    super(props)\n    this.svgRef = React.createRef()\n  }\n\n  componentDidMount() {\n    this.animator = new ProgressHexAnimator({ svg: this.svgRef.current! })\n    this.timeline = this.animator.createTimeline()\n    this.timeline.play()\n  }\n\n  componentWillUnmount() {\n    this.timeline?.pause()\n  }\n\n  render() {\n    const { compact } = this.props\n    return (\n      <svg\n        className=\"progress-hex\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        width=\"112\"\n        height=\"120\"\n        viewBox=\"0 0 112 120\"\n        ref={this.svgRef}\n      >\n        {!compact && (\n          <g id=\"ring-5\">\n            <circle cx=\"28.99\" cy=\"108.24\" r=\"1\" />\n            <circle cx=\"42.98\" cy=\"116\" r=\"1\" />\n            <circle cx=\"14.99\" cy=\"100.49\" r=\"1\" />\n            <circle cx=\"1\" cy=\"92.73\" r=\"1\" />\n            <circle cx=\"58.39\" cy=\"124.54\" r=\"1\" />\n            <circle cx=\"1\" cy=\"77.73\" r=\"1\" />\n            <circle cx=\"72.72\" cy=\"117.48\" r=\"1\" />\n            <circle cx=\"1\" cy=\"62.73\" r=\"1\" />\n            <circle cx=\"86.71\" cy=\"110.24\" r=\"1\" />\n            <circle cx=\"1.01\" cy=\"45.97\" r=\"1\" />\n            <circle cx=\"100.71\" cy=\"101.24\" r=\"1\" />\n            <circle cx=\"1.01\" cy=\"30.97\" r=\"1\" />\n            <circle cx=\"114.71\" cy=\"94\" r=\"1\" />\n            <circle cx=\"15\" cy=\"23.73\" r=\"1\" />\n            <circle cx=\"114.71\" cy=\"79\" r=\"1\" />\n            <circle cx=\"28.99\" cy=\"15.49\" r=\"1\" />\n            <circle cx=\"114.71\" cy=\"63\" r=\"1\" />\n            <circle cx=\"42.99\" cy=\"8.24\" r=\"1\" />\n            <circle cx=\"114.71\" cy=\"48\" r=\"1\" />\n            <circle cx=\"56.98\" cy=\"1\" r=\"1\" />\n            <circle cx=\"114.71\" cy=\"33\" r=\"1\" />\n            <circle cx=\"100.71\" cy=\"25.24\" r=\"1\" />\n            <circle cx=\"86.72\" cy=\"17.48\" r=\"1\" />\n            <circle cx=\"72.39\" cy=\"9.54\" r=\"1\" />\n          </g>\n        )}\n        <g id=\"ring-4\">\n          <circle cx=\"28.99\" cy=\"93.24\" r=\"1\" />\n          <circle cx=\"42.99\" cy=\"101.00\" r=\"1\" />\n          <circle cx=\"15.00\" cy=\"85.49\" r=\"1\" />\n          <circle cx=\"58.40\" cy=\"109.54\" r=\"1\" />\n          <circle cx=\"15.00\" cy=\"70.49\" r=\"1\" />\n          <circle cx=\"72.72\" cy=\"102.48\" r=\"1\" />\n          <circle cx=\"15.01\" cy=\"53.73\" r=\"1\" />\n          <circle cx=\"86.72\" cy=\"93.48\" r=\"1\" />\n          <circle cx=\"15.01\" cy=\"38.73\" r=\"1\" />\n          <circle cx=\"100.72\" cy=\"86.24\" r=\"1\" />\n          <circle cx=\"29.00\" cy=\"31.49\" r=\"1\" />\n          <circle cx=\"100.72\" cy=\"71.24\" r=\"1\" />\n          <circle cx=\"42.99\" cy=\"23.24\" r=\"1\" />\n          <circle cx=\"100.72\" cy=\"55.24\" r=\"1\" />\n          <circle cx=\"56.99\" cy=\"16.00\" r=\"1\" />\n          <circle cx=\"100.72\" cy=\"40.24\" r=\"1\" />\n          <circle cx=\"86.72\" cy=\"32.48\" r=\"1\" />\n          <circle cx=\"72.40\" cy=\"24.54\" r=\"1\" />\n        </g>\n        <g id=\"ring-3\">\n          <circle cx=\"29.00\" cy=\"78.24\" r=\"1\" />\n          <circle cx=\"42.99\" cy=\"86.00\" r=\"1\" />\n          <circle cx=\"58.41\" cy=\"94.54\" r=\"1\" />\n          <circle cx=\"29.01\" cy=\"61.49\" r=\"1\" />\n          <circle cx=\"72.41\" cy=\"85.54\" r=\"1\" />\n          <circle cx=\"29.01\" cy=\"46.49\" r=\"1\" />\n          <circle cx=\"86.73\" cy=\"78.48\" r=\"1\" />\n          <circle cx=\"43.00\" cy=\"39.24\" r=\"1\" />\n          <circle cx=\"86.73\" cy=\"63.48\" r=\"1\" />\n          <circle cx=\"56.99\" cy=\"31.00\" r=\"1\" />\n          <circle cx=\"86.73\" cy=\"47.48\" r=\"1\" />\n          <circle cx=\"72.41\" cy=\"39.54\" r=\"1\" />\n        </g>\n        <g id=\"ring-2\">\n          <circle cx=\"43.00\" cy=\"68.24\" r=\"1\" />\n          <circle cx=\"56.99\" cy=\"76.00\" r=\"1\" />\n          <circle cx=\"43.00\" cy=\"53.24\" r=\"1\" />\n          <circle cx=\"72.41\" cy=\"69.54\" r=\"1\" />\n          <circle cx=\"56.99\" cy=\"46.00\" r=\"1\" />\n          <circle cx=\"72.41\" cy=\"54.54\" r=\"1\" />\n        </g>\n        <g id=\"ring-1\">\n          <circle cx=\"56\" cy=\"60\" r=\"1\" />\n        </g>\n      </svg>\n    )\n  }\n}\n\nexport default ProgressHex\n"
  },
  {
    "path": "client/components/ProgressHex/index.ts",
    "content": "export { default } from './ProgressHex'\n"
  },
  {
    "path": "client/components/ProgressHex/progress-hex-timeline.ts",
    "content": "import anime, { AnimeAnimParams } from 'animejs'\n\nimport colors from '../../config/colors'\nimport { randomFromArray, zeroToN } from '../../../utils'\n\nconst DURATION = 1000\n\ntype Circle = { cx: number; cy: number; ringNumber: number }\n\ntype CirclesMap = Map<SVGCircleElement, Circle>\n\ntype ProgressHexAnimatorProps = {\n  svg: SVGSVGElement\n}\n\nexport default class ProgressHexAnimator {\n  circlesMap: CirclesMap\n  circles: NodeListOf<SVGCircleElement>\n  rings: NodeListOf<SVGGElement>\n  trailBlaze: Trailblaze\n  width: number\n  height: number\n\n  constructor({ svg }: ProgressHexAnimatorProps) {\n    this.circlesMap = new Map()\n    this.circles = svg.querySelectorAll('circle')\n    this.rings = svg.querySelectorAll('g')\n    this.trailBlaze = new Trailblaze({\n      linesCount: 55,\n      svg,\n      circlesMap: this.circlesMap,\n      ringsCount: this.rings.length,\n    })\n\n    Array.from(this.circles).forEach(circle => {\n      const cx = parseFloat(circle.getAttribute('cx')!)\n      const cy = parseFloat(circle.getAttribute('cy')!)\n      circle.style.transformOrigin = `${cx}px ${cy}px`\n      this.circlesMap.set(circle, {\n        cx,\n        cy,\n        ringNumber: parseInt(circle.parentElement!.id.match(/.+(\\d+)/)![1]) - 1,\n      })\n    })\n\n    this.width = parseFloat(svg.getAttribute('width')!)\n    this.height = parseFloat(svg.getAttribute('height')!)\n  }\n\n  getTranslation(circle: SVGCircleElement, distance: number) {\n    const { cx, cy } = this.circlesMap.get(circle)!\n    const { x, y } = this.pointAtDistance(\n      cx,\n      cy,\n      this.width / 2,\n      this.height / 2,\n      distance\n    )\n\n    return { x: x - cx, y: y - cy }\n  }\n\n  pointAtDistance(x1: number, y1: number, x2: number, y2: number, d: number) {\n    const curDistanceBetweenPoints = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)\n    if (curDistanceBetweenPoints === 0) return { x: x1, y: y1 }\n\n    const t = d / curDistanceBetweenPoints\n    const x = (x1 - t * x2) / (1 - t)\n    const y = (y1 - t * y2) / (1 - t)\n    return { x, y }\n  }\n\n  createTimeline() {\n    const fadeInTimeline = anime.timeline({\n      duration: DURATION,\n      autoplay: false,\n      loop: false,\n    })\n\n    const quakeTimeline = anime.timeline({\n      duration: DURATION,\n      autoplay: false,\n      loop: true,\n    })\n\n    const fadeInRings = {\n      targets: this.rings,\n      opacity: [0, 1],\n      delay: anime.stagger(DURATION / 5, { from: 'last' }),\n      duration: DURATION / 2,\n      easing: 'linear',\n    }\n\n    const quakeCircles = {\n      targets: this.circles,\n      scale: (el: SVGCircleElement) =>\n        this.circlesMap.get(el)!.ringNumber === 0 ? 3 : 1.5,\n      translateY: (circle: SVGCircleElement) =>\n        this.getTranslation(circle, 4).y,\n      translateX: (circle: SVGCircleElement) =>\n        this.getTranslation(circle, 4).x,\n      delay: ((el: SVGCircleElement) =>\n        (Math.pow(this.circlesMap.get(el)!.ringNumber, 0.6) * DURATION) / 4 +\n        (this.circlesMap.get(el)!.ringNumber > 0\n          ? DURATION / 2.5\n          : 0)) as unknown as AnimeAnimParams['delay'],\n      duration: DURATION,\n      easing: () => (t: number) => Math.sin(t * Math.PI),\n      changeBegin: () => this.trailBlaze.start(),\n    }\n\n    fadeInTimeline.add(fadeInRings)\n    quakeTimeline.add(quakeCircles)\n\n    return {\n      ...quakeTimeline,\n      play: () => {\n        fadeInTimeline.play()\n        setTimeout(() => {\n          quakeTimeline.play()\n        }, DURATION)\n      },\n    }\n  }\n}\n\ntype TrailblazeProps = {\n  svg: SVGSVGElement\n  circlesMap: CirclesMap\n  linesCount: number\n  ringsCount: number\n}\n\nclass Trailblaze {\n  circlesMap: CirclesMap\n  ringsCount: number\n  lines: SVGLineElement[]\n\n  constructor({ linesCount, svg, circlesMap, ringsCount }: TrailblazeProps) {\n    this.lines = []\n    this.circlesMap = circlesMap\n    this.ringsCount = ringsCount\n    for (let i = 0; i < linesCount; i++) {\n      const line = this.createTrail()\n      this.lines.push(line)\n      svg.insertBefore(line, svg.children[0])\n    }\n  }\n\n  createTrail() {\n    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line')\n    line.setAttribute('stroke-width', '0.5')\n    line.setAttribute('class', 'progress-hex__trail')\n    return line\n  }\n\n  setLineCoords(line: SVGLineElement, x1 = 0, x2 = 0, y1 = 0, y2 = 0) {\n    line.setAttribute('x1', `${x1}`)\n    line.setAttribute('x2', `${x2}`)\n    line.setAttribute('y1', `${y1}`)\n    line.setAttribute('y2', `${y2}`)\n  }\n\n  getCirclesInRing(ringNumber: number) {\n    const circles: Circle[] = []\n    this.circlesMap.forEach(value => {\n      if (value.ringNumber === ringNumber) {\n        circles.push(value)\n      }\n    })\n    return circles\n  }\n\n  distanceBetweenCircles(c1: Circle, c2: Circle) {\n    return Math.sqrt((c2.cx - c1.cx) ** 2 + (c2.cy - c1.cy) ** 2)\n  }\n\n  getRandomConnection() {\n    const rings = zeroToN(this.ringsCount)\n    const sourceRingNumber = randomFromArray(rings.slice(0, -1))\n    const destinationRingNumber = sourceRingNumber + 1\n\n    const eligibleSourceCircles = this.getCirclesInRing(sourceRingNumber)\n    const sourceCircle = randomFromArray(eligibleSourceCircles)\n\n    const eligibleDestinationCircles = this.getCirclesInRing(\n      destinationRingNumber\n    )\n\n    const destinationCircleDistances = eligibleDestinationCircles.map(\n      (circle, index) => ({\n        index,\n        distance: this.distanceBetweenCircles(sourceCircle, circle),\n      })\n    )\n\n    const eligibleDistancesMin = Math.min(\n      ...destinationCircleDistances.map(a => a.distance)\n    )\n    const eligibleDestinationIndexes = destinationCircleDistances\n      .filter(c => Math.abs(eligibleDistancesMin - c.distance) < 2)\n      .map(d => d.index)\n\n    const destinationCircle =\n      eligibleDestinationCircles[randomFromArray(eligibleDestinationIndexes)]\n\n    return {\n      source: sourceCircle,\n      destination: destinationCircle,\n    }\n  }\n\n  getDashOffset = (element: SVGElement | HTMLElement | null) => {\n    if (!element) return 0\n    try {\n      return anime.setDashoffset(element)\n    } catch (err) {\n      // Called before the element was rendered\n      console.error(err)\n      return 0\n    }\n  }\n\n  start() {\n    const lineMap = new WeakMap<\n      SVGLineElement,\n      { source: Circle; destination: Circle }\n    >()\n\n    this.lines.forEach(line => {\n      const { source, destination } = this.getRandomConnection()\n      lineMap.set(line, { source, destination })\n      line.setAttribute('stroke', randomFromArray(colors))\n      this.setLineCoords(\n        line,\n        source.cx,\n        destination.cx,\n        source.cy,\n        destination.cy\n      )\n    })\n\n    anime({\n      targets: this.lines,\n      opacity: [1, 0.9, 0],\n      strokeDashoffset: [(el: SVGLineElement) => this.getDashOffset(el), 0],\n      x1: (el: SVGLineElement) => lineMap.get(el)!.source.cx,\n      x2: (el: SVGLineElement) => lineMap.get(el)!.destination.cx,\n      y1: (el: SVGLineElement) => lineMap.get(el)!.source.cy,\n      y2: (el: SVGLineElement) => lineMap.get(el)!.destination.cy,\n      duration: 500,\n      delay: () => anime.random(0, DURATION / 5),\n      easing: 'easeOutCubic',\n    })\n  }\n}\n"
  },
  {
    "path": "client/components/QuickStatsBar/QuickStatsBar.scss",
    "content": "@use \"sass:math\";\n\n@import '../../../stylesheets/colors';\n@import '../../../stylesheets/variables';\n\n.quick-stats-bar {\n  display: flex;\n  align-content: center;\n  @include font-size-xs;\n  color: lighten($raven, 15%);\n  background: lighten($raven, 55%);\n  border-radius: 0 0 10px 10px;\n  overflow: hidden;\n}\n\n.quick-stats-bar__stat {\n  padding: math.div($global-spacing, 1.5) $global-spacing * 1.5;\n  display: flex;\n  align-content: center;\n  position: relative;\n  justify-content: center;\n  flex: 1 1 auto;\n  white-space: nowrap;\n  margin: auto 0;\n\n  & > * {\n    margin: auto 0;\n  }\n\n  &:not(:first-of-type)::before {\n    content: '';\n    width: 1px;\n    height: 60%;\n    position: absolute;\n    background: transparentize(black, 0.95);\n    top: 0;\n    bottom: 0;\n    margin: auto;\n    left: 0;\n  }\n\n  &:first-of-type,\n  &:last-of-type {\n    //padding-left: $global-spacing;\n  }\n}\n\n.quick-stats-bar__stat--optional {\n  @media screen and (max-width: 48em) {\n    display: none;\n  }\n\n  @media screen and (max-width: 40em) {\n    display: none;\n  }\n}\n\n.quick-stats-bar__stat--description {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  display: block;\n  flex-grow: 1;\n\n  @media screen and (max-width: 40em) {\n    display: none;\n  }\n}\n\n.quick-stats-bar__stat--description-content {\n  margin-left: $global-spacing;\n}\n\n.quick-stats-bar__stat-icon {\n  margin-right: $global-spacing;\n}\n\n.quick-stats-bar__logo-icon {\n  vertical-align: middle;\n\n  &.quick-stats-bar__logo-icon--npm {\n    width: 36px;\n  }\n\n  &.quick-stats-bar__logo-icon--github {\n    height: 18px;\n    width: 18px;\n  }\n\n  path {\n    transition: fill 0.2s;\n    fill: lighten($raven, 15%);\n  }\n}\n\n.quick-stats-bar__link {\n  margin: auto $global-spacing * 0.5;\n\n  &:hover {\n    .quick-stats-bar__logo-icon--github {\n      path {\n        fill: #333;\n      }\n    }\n    .quick-stats-bar__logo-icon--npm {\n      path {\n        fill: #cb3837;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/components/QuickStatsBar/QuickStatsBar.tsx",
    "content": "import React, { Component } from 'react'\n\nimport { sanitizeHTML } from '../../../utils/common.utils'\nimport TreeShakeIcon from '../../assets/tree-shake.svg'\nimport SideEffectIcon from '../../assets/side-effect.svg'\nimport DependencyIcon from '../../assets/dependency.svg'\nimport GithubIcon from '../../assets/github-logo.svg'\nimport NPMIcon from '../../assets/npm-logo.svg'\nimport InfoIcon from '../../assets/info.svg'\nimport { PackageInfo } from '../../../types'\n\ntype QuickStatsBarProps = Pick<\n  PackageInfo,\n  | 'name'\n  | 'description'\n  | 'repository'\n  | 'dependencyCount'\n  | 'isTreeShakeable'\n  | 'hasSideEffects'\n>\n\nclass QuickStatsBar extends Component<QuickStatsBarProps> {\n  static defaultProps = {\n    description: '',\n  }\n\n  getStatItemCount = () => {\n    const { isTreeShakeable, hasSideEffects } = this.props\n    let statItemCount = 0\n\n    if (isTreeShakeable) statItemCount += 1\n    if (hasSideEffects !== true) statItemCount += 1\n    return statItemCount\n  }\n\n  getTrimmedDescription = () => {\n    const { description } = this.props\n    const trimmed = description.trim()\n\n    if (trimmed.endsWith('.')) {\n      return trimmed.substring(0, trimmed.length - 1)\n    } else {\n      return trimmed\n    }\n  }\n\n  render() {\n    const {\n      isTreeShakeable,\n      hasSideEffects,\n      dependencyCount,\n      name,\n      repository,\n    } = this.props\n    const statItemCount = this.getStatItemCount()\n    const description = this.getTrimmedDescription()\n\n    return (\n      <div className=\"quick-stats-bar\">\n        <div\n          className=\"quick-stats-bar__stat quick-stats-bar__stat--description \"\n          title={description}\n        >\n          <InfoIcon />\n          {statItemCount < 2 && (\n            <span\n              className=\"quick-stats-bar__stat--description-content\"\n              dangerouslySetInnerHTML={{ __html: sanitizeHTML(description) }}\n              style={{\n                maxWidth: `${500 - statItemCount * 280}px`,\n              }}\n            />\n          )}\n        </div>\n\n        {isTreeShakeable && (\n          <div className=\"quick-stats-bar__stat\">\n            <TreeShakeIcon className=\"quick-stats-bar__stat-icon\" />{' '}\n            <span>tree-shakeable</span>\n          </div>\n        )}\n\n        {!(hasSideEffects === true) && (\n          <div className=\"quick-stats-bar__stat\">\n            <SideEffectIcon className=\"quick-stats-bar__stat-icon\" />{' '}\n            <span>\n              {!(hasSideEffects === false) && hasSideEffects.length\n                ? 'some side-effects'\n                : 'side-effect free'}\n            </span>\n          </div>\n        )}\n        <div className=\"quick-stats-bar__stat quick-stats-bar__stat--optional\">\n          <DependencyIcon className=\"quick-stats-bar__stat-icon\" />\n          <span>\n            {dependencyCount === 0 ? (\n              'no dependencies'\n            ) : (\n              <span>\n                {dependencyCount}{' '}\n                {dependencyCount > 1 ? 'dependencies' : 'dependency'}\n              </span>\n            )}\n          </span>\n        </div>\n        <div className=\"quick-stats-bar__stat\">\n          <a\n            className=\"quick-stats-bar__link\"\n            href={'https://npmjs.com/package/' + name}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            <NPMIcon className=\"quick-stats-bar__logo-icon quick-stats-bar__logo-icon--npm\" />\n          </a>\n          {repository && (\n            <a\n              className=\"quick-stats-bar__link\"\n              href={repository}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              <GithubIcon className=\"quick-stats-bar__logo-icon quick-stats-bar__logo-icon quick-stats-bar__logo-icon--github\" />\n            </a>\n          )}\n        </div>\n      </div>\n    )\n  }\n}\n\nexport default QuickStatsBar\n"
  },
  {
    "path": "client/components/QuickStatsBar/index.ts",
    "content": "import QuickStatsBar from './QuickStatsBar'\n\nexport default QuickStatsBar\n"
  },
  {
    "path": "client/components/ResultLayout/ResultLayout.scss",
    "content": "@import '../../../stylesheets/variables';\n@import '../../../stylesheets/colors';\n\n.page-header {\n  padding: $global-spacing * 3;\n  padding-bottom: $global-spacing * 2;\n  display: flex;\n  align-items: center;\n\n  @media screen and (max-width: 40em) {\n    padding: $global-spacing * 2;\n  }\n}\n\n.page-header--right-section {\n  margin-left: auto;\n  display: flex;\n  align-items: center;\n}\n\n.github-logo {\n  width: 30px;\n  height: 30px;\n\n  @media screen and (max-width: 40em) {\n    width: 20px;\n    height: 20px;\n  }\n\n  path {\n    fill: #666;\n    transition: fill 0.2s;\n  }\n\n  &:hover {\n    path {\n      fill: black;\n    }\n  }\n}\n\n.logo-small {\n  @include font-size-reg;\n  text-transform: uppercase;\n  font-weight: $font-weight-very-bold;\n  letter-spacing: 3px;\n  user-select: none;\n  cursor: pointer;\n  color: #212121;\n}\n\n.logo-small__alt {\n  color: #888;\n}\n\n.page-container {\n  display: flex;\n  flex-direction: column;\n  min-height: 100vh;\n  min-height: calc(100vh - 6px);\n  flex-gorw: 1;\n}\n\n.page-content {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-direction: column;\n  flex-grow: 1;\n\n  @media screen and (max-width: 40em) {\n    padding: 0 $global-spacing * 2;\n  }\n}\n\n.page-header__quicklinks {\n  list-style: none;\n  margin: 0 2rem 0 0;\n  font-weight: $font-weight-light;\n  display: flex;\n\n  a {\n    @include font-size-xs;\n    text-transform: uppercase;\n    letter-spacing: 0.3px;\n    font-weight: $font-weight-bold;\n    opacity: 0.55;\n    color: $raven;\n    transition: opacity 0.2s;\n\n    &:hover {\n      opacity: 1;\n    }\n  }\n\n  li + li {\n    margin-left: $global-spacing * 2;\n  }\n\n  @media screen and (max-width: 40em) {\n    max-width: 40vw;\n    overflow: scroll;\n    align-items: center;\n    justify-content: flex-end;\n    overflow: scroll;\n  }\n}\n"
  },
  {
    "path": "client/components/ResultLayout/ResultLayout.tsx",
    "content": "import React, { Component } from 'react'\nimport cx from 'classnames'\n\nimport Layout from '../../components/Layout'\nimport PageNav from '../PageNav'\nimport { WithClassName } from '../../../types'\n\nexport default class ResultLayout extends Component<\n  React.PropsWithChildren & WithClassName\n> {\n  render() {\n    const { children, className } = this.props\n    return (\n      <Layout>\n        <div className={cx('page-container', className)}>\n          <PageNav />\n          <div className=\"page-content\">{children}</div>\n        </div>\n      </Layout>\n    )\n  }\n}\n"
  },
  {
    "path": "client/components/ResultLayout/index.ts",
    "content": "import ResultLayout from './ResultLayout'\n\nexport default ResultLayout\n"
  },
  {
    "path": "client/components/Separator.tsx",
    "content": "import React from 'react'\n\ntype SeparatorProps = {\n  text?: string\n  align?: React.CSSProperties['justifyContent']\n  showLeft?: boolean\n  containerStyles?: React.CSSProperties\n}\n\nexport default function Separator({\n  text = 'or',\n  align = 'center',\n  showLeft = true,\n  containerStyles,\n}: SeparatorProps) {\n  const commonStyles = {\n    display: 'flex',\n    justifyContent: align,\n    alignItems: 'center',\n  }\n\n  return (\n    <div\n      style={{\n        width: '100%',\n        position: 'relative',\n        margin: '6px 0',\n        opacity: '0.7',\n        ...containerStyles,\n      }}\n    >\n      <div\n        style={{\n          width: '100%',\n          ...commonStyles,\n        }}\n      >\n        {showLeft && (\n          <div style={commonStyles}>\n            <svg width=\"49\" height=\"39\" xmlns=\"http://www.w3.org/2000/svg\">\n              <g fill=\"#211915\" fillRule=\"evenodd\">\n                <path d=\"M28.4 21c5.2-2.9 12.2-2.3 18.1-1-1.9.8-3.7 1.8-5.8 2.1a24.8 24.8 0 0 1-12.3-1zm-7.2 14.7c3 4.3 11.2 3.8 11.7-2.1.4-4.7-5.8-8-8.5-3.6-.7 1.3-.9 3 .3 4 1.5 1.3 3.5.5 4.1-1.2.3-.7-.8-.7-1.1-.4-1.1 1-2-.1-2-1 0-1.6 2-2 3.1-1.4 3 1.5 2.8 5.8-.4 6.7-2.6.7-5-1-6-3.3-1.2-2.3-.6-4.9.6-7 1-1.7 2.1-3 3.4-4l2.3.7c3.6 1 7.5 1.3 11.2.8 2.8-.4 6.4-1.3 8.4-3.5l.2-.3c.2-.2.3-.5 0-.6-6.8-3-15.3-3.2-21.7 1a15 15 0 0 1-6.3-4.8C17.5 12 17 4.4 22.7 2.6c2.4-.8 5.3.3 5.7 3 .3 2-1.1 4-3.3 4.2-.8 0-1.9-.3-2.2-1a.9.9 0 0 1 1.4-1.1c.5.4 1.3-.3 1-.8-1.4-1.8-3.4-1-3.9 1-.5 2.6 2 3.7 4 3.3 5.1-1 6.6-8.4 1.4-10.2-5.4-1.8-10 3.7-10.3 8.6-.4 5.7 3.7 9.9 8.5 12.2-4 3.4-7.1 9.1-3.8 14z\" />\n                <path d=\"M5.7 21.7zm12.5 1.8c-1.7 3-3.7 5.4-7 6.6 1-3.3 3.6-5.7 7-6.6zm-.9-3.6a17.7 17.7 0 0 1-7.3-5c2.7 1 5.3 3 7.3 5zM.6 22.3c2.4 1 5 1.2 7.6 1.3h6a12.2 12.2 0 0 0-5.1 7.8c-.1.6.3.8.9.7 4.5-1 9.9-5 10.3-10v-.2c0-.1.1-.3 0-.4a20.5 20.5 0 0 0-10.9-8.6c-.3-.1-.8.1-1 .4-1.3 2 1 3.7 2.4 4.9 1.1 1 2.3 1.8 3.6 2.4-4.4-.2-8.8-1.1-13.1.1-.5.1-1.5 1.2-.7 1.6z\" />\n              </g>\n            </svg>\n          </div>\n        )}\n        <div style={commonStyles}>\n          <svg width=\"249\" height=\"3\" xmlns=\"http://www.w3.org/2000/svg\">\n            <path\n              d=\"M1.5 2.3c31.4.7 62.9.2 94.3.3h144.8c1.3 0 .5-1.7-.5-1.7h-.3c-1.3 0-.4 1.7.5 1.7h8c1.3-.1.3-2-.8-1.6h.1c-1.2 0-.4 1.5.5 1.5.1 0 .2 0 0 0 1.2.4 1-1.5-.2-1.6h-8c-1.4 0-.5 1.7.4 1.7h.3c1.3 0 .5-1.7-.5-1.7H55.4c-18 0-36.2-.1-54.2.3-.9 0-.3 1.1.3 1.1\"\n              fill=\"#211915\"\n              fillRule=\"evenodd\"\n            />\n          </svg>\n        </div>\n        {!showLeft && (\n          <div style={{ ...commonStyles, marginLeft: '-20px' }}>\n            <svg width=\"249\" height=\"3\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path\n                d=\"M1.5 2.3c31.4.7 62.9.2 94.3.3h144.8c1.3 0 .5-1.7-.5-1.7h-.3c-1.3 0-.4 1.7.5 1.7h8c1.3-.1.3-2-.8-1.6h.1c-1.2 0-.4 1.5.5 1.5.1 0 .2 0 0 0 1.2.4 1-1.5-.2-1.6h-8c-1.4 0-.5 1.7.4 1.7h.3c1.3 0 .5-1.7-.5-1.7H55.4c-18 0-36.2-.1-54.2.3-.9 0-.3 1.1.3 1.1\"\n                fill=\"#211915\"\n                fillRule=\"evenodd\"\n              />\n            </svg>\n          </div>\n        )}\n        <div style={commonStyles}>\n          <svg width=\"49\" height=\"39\" xmlns=\"http://www.w3.org/2000/svg\">\n            <g fill=\"#211915\" fillRule=\"evenodd\">\n              <path d=\"M16 22.2c-2.5.3-5 .3-7.4 0-2.1-.4-4-1.4-5.8-2.2 6-1.3 12.9-1.9 18.1 1-1.6.6-3.3 1-5 1.2zm8.2-.4c4.9-2.3 9-6.5 8.6-12.2C32.5 4.7 28-.8 22.5 1c-5.2 1.8-3.7 9.3 1.3 10.2 2.1.4 4.6-.7 4-3.2-.4-2.1-2.4-2.9-3.8-1-.3.4.5 1 1 .7a.9.9 0 0 1 1.4 1c-.3.8-1.4 1.2-2.2 1.1-2.2-.1-3.6-2.2-3.3-4.2.4-2.7 3.3-3.8 5.7-3 5.7 1.8 5.1 9.3 2.2 13.1a15 15 0 0 1-6.3 4.8c-6.4-4.2-15-4-21.7-1-.3 0-.2.4 0 .6l.2.3c2 2.3 5.6 3.1 8.4 3.5A27.8 27.8 0 0 0 23 22.4c1.3 1 2.4 2.3 3.4 4 1.2 2.1 1.7 4.7.6 7-1 2.2-3.4 4-6 3.3-3.2-.9-3.4-5.2-.5-6.7 1.3-.6 3.1-.2 3.1 1.4 0 .9-.8 2-1.9 1-.4-.3-1.4-.3-1.1.4.6 1.7 2.6 2.5 4 1.2 1.3-1 1.1-2.7.4-4-2.7-4.4-9-1-8.5 3.6.5 6 8.7 6.4 11.7 2.1 3.3-4.8.1-10.5-3.9-14z\" />\n              <path d=\"M39.6 21.9zM38.1 30a12.7 12.7 0 0 1-7-6.6c3.4.9 6 3.3 7 6.6zm1.2-15.2c-1 1.1-3.4 3-3.7 3.1-1.1.8-2.4 1.4-3.7 2 2.1-2.2 4.7-4.1 7.4-5.1zm8.7 5.8c-4.4-1.2-8.7-.3-13.1 0a23 23 0 0 0 3.6-2.5c1.4-1.2 3.7-3 2.4-4.9-.2-.3-.7-.5-1-.4-4.3 1-8.7 5-11 8.6l.1.4v.2c.4 5 5.8 9 10.3 10 .6.1 1-.1 1-.7-.7-3.3-2.6-6-5.3-7.8h6.1c2.6-.1 5.2-.2 7.5-1.3.8-.4-.1-1.5-.6-1.6z\" />\n            </g>\n          </svg>\n        </div>\n      </div>\n      {text && (\n        <span\n          style={{\n            position: 'absolute',\n            left: 0,\n            right: 0,\n            top: 0,\n            bottom: 0,\n            margin: 'auto',\n            width: '42px',\n            height: '20px',\n            background: 'white',\n            padding: '0 15px',\n            borderRadius: '50%',\n          }}\n        >\n          {text}\n        </span>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "client/components/SimilarPackageCard/SimilarPackageCard.scss",
    "content": "@use \"sass:math\";\n\n@import '../../../stylesheets/colors';\n@import '../../../stylesheets/variables';\n\n.similar-package-card {\n  border: 1px solid $autocomplete-border-color;\n  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);\n  border-radius: 10px;\n  width: calc(25% - #{$global-spacing * 2});\n  display: flex;\n  flex-direction: column;\n  margin: $global-spacing;\n  color: inherit;\n  transition: all 0.2s;\n\n  &:hover {\n    transform: scale(1.01);\n    box-shadow: 0 5px 10px rgba(0, 0, 0, 0.06);\n    border: 1px solid fade-in($autocomplete-border-color, 0.02);\n  }\n\n  &.similar-package-card--empty {\n    border-style: dashed;\n    background: transparentize($raven, 0.97);\n  }\n\n  @media screen and (max-width: 64em) {\n    margin: math.div($global-spacing, 1.5);\n    width: calc(33.3% - #{$global-spacing * 2});\n  }\n\n  @media screen and (max-width: 48em) {\n    margin: math.div($global-spacing, 1.5);\n    width: calc(50% - #{$global-spacing * 1.5});\n  }\n\n  @media screen and (max-width: 40em) {\n    width: 100%;\n  }\n}\n\n.similar-package-card--empty {\n  color: transparentize($raven, 0.5);\n  align-items: center;\n}\n\n.similar-package-card__wrap {\n  padding: $global-spacing * 2;\n  flex-grow: 1;\n\n  .similar-package-card--empty & {\n    padding: $global-spacing * 5 $global-spacing * 2;\n    align-items: center;\n    text-align: center;\n  }\n}\n\n.similar-package-card__header {\n  display: flex;\n}\n\n.similar-package-card__name {\n  margin: 0;\n  font-family: $font-family-code;\n  font-weight: $font-weight-light;\n  flex-grow: 1;\n  word-break: break-word;\n}\n\n.similar-package-card__description {\n  @include font-size-sm;\n  color: $dark-raven;\n  line-height: 1.5;\n  margin: $global-spacing 0 0 0;\n  word-break: break-word;\n  img {\n    height: auto;\n    max-width: 100%;\n  }\n\n  .similar-package-card--empty & {\n    text-transform: uppercase;\n    letter-spacing: 1px;\n    font-weight: $font-weight-bold;\n    color: transparentize($raven, 0.4);\n  }\n}\n\n.similar-package-card__footer {\n  background: lighten($raven, 54%);\n  display: flex;\n  padding: $global-spacing $global-spacing * 2;\n  align-items: center;\n  border-radius: 0 0 10px 10px;\n}\n\n.similar-package-card__stat {\n  & + & {\n    margin-left: $global-spacing * 2;\n  }\n}\n\n.similar-package-card__number {\n  @include font-size-md;\n  font-weight: $font-weight-very-bold;\n}\n\n.similar-package-card__comparison--positive {\n  color: $pastel-green;\n}\n\n.similar-package-card__comparison--negative {\n  color: $carrot-orange;\n}\n\n.similar-package-card__comparison--similar {\n  color: $raven;\n}\n\n.similar-package-card__label {\n  @include font-size-xs;\n  text-transform: uppercase;\n  letter-spacing: 1px;\n  line-height: 1.5;\n\n  .similar-package-card__size & {\n    color: $raven;\n  }\n}\n\n.similar-package-card__treeshake {\n  height: $global-spacing * 2.5;\n  width: auto;\n  margin-left: auto;\n}\n\n.similar-package-card__shrink {\n  font-size: 75%;\n  .similar-package-card__size & {\n    color: $raven;\n  }\n}\n\n.similar-package-card__github-icon {\n  height: $global-spacing * 2;\n  width: auto;\n  opacity: 0.5;\n  transition: all 0.2s;\n  vertical-align: middle;\n\n  &:hover {\n    opacity: 1;\n  }\n}\n\n.similar-package-card__plus {\n  width: 35%;\n  height: auto;\n  margin-bottom: $global-spacing * 1.5;\n  path {\n    fill: transparentize($raven, 0.7);\n  }\n}\n"
  },
  {
    "path": "client/components/SimilarPackageCard/SimilarPackageCard.tsx",
    "content": "import React, { Component } from 'react'\nimport cx from 'classnames'\nimport Link from 'next/link'\nimport queryString from 'query-string'\n\nimport { formatSize } from '../../../utils'\nimport { sanitizeHTML } from '../../../utils/common.utils'\nimport TreeShakeIcon from '../../assets/tree-shake.svg'\nimport PlusIcon from '../../assets/plus.svg'\nimport GithubIcon from '../../assets/github-logo.svg'\nimport GitIcon from '../../assets/git-logo.svg'\n\ntype SimilarPackageCardProps = { category?: string } & (\n  | { pack: any; comparisonSizePercent: number }\n  | { isEmpty: true }\n)\n\nexport default class SimilarPackageCard extends Component<SimilarPackageCardProps> {\n  getSuggestionIssueUrl = () => {\n    const params = queryString.stringify({\n      labels: 'similar suggestion',\n      template: '2-similar-package-suggestion.md',\n      title: `Package suggestion: <package-name> for \\`${this.props.category}\\``,\n    })\n\n    return `https://github.com/pastelsky/bundlephobia/issues/new?${params}`\n  }\n\n  render() {\n    if ('isEmpty' in this.props) {\n      return (\n        <a\n          className=\"similar-package-card similar-package-card--empty\"\n          href={this.getSuggestionIssueUrl()}\n          target=\"_blank\"\n          rel=\"noreferrer noopener\"\n        >\n          <div className=\"similar-package-card__wrap\">\n            <PlusIcon className=\"similar-package-card__plus\" />\n            <p className=\"similar-package-card__description\">Suggest another</p>\n          </div>\n        </a>\n      )\n    }\n\n    const { pack, comparisonSizePercent } = this.props\n    const { size, unit } = formatSize(pack.gzip)\n    const sizeDiff = Math.abs(\n      (comparisonSizePercent / 100) * pack.gzip - pack.gzip\n    )\n\n    const getComparisonNumber = (comparisonSizePercent: number) => {\n      if (sizeDiff < 1500) {\n        return (\n          <div>\n            <div className=\"similar-package-card__label\">\n              Similar <br /> size\n            </div>\n          </div>\n        )\n      } else if (Math.abs(comparisonSizePercent) > 100) {\n        return (\n          <div>\n            <div className=\"similar-package-card__number\">\n              {(1 + Math.abs(comparisonSizePercent) / 100).toFixed(1)}{' '}\n              <span className=\"similar-package-card__shrink\">×</span>\n            </div>\n            <div className=\"similar-package-card__label\">\n              {comparisonSizePercent > 0 ? 'Larger' : 'Smaller'}\n            </div>\n          </div>\n        )\n      } else {\n        return (\n          <div>\n            <div className=\"similar-package-card__number\">\n              {Math.abs(comparisonSizePercent).toFixed(0)}{' '}\n              <span className=\"similar-package-card__shrink\">%</span>\n            </div>\n            <div className=\"similar-package-card__label\">\n              {comparisonSizePercent > 0 ? 'Larger' : 'Smaller'}\n            </div>\n          </div>\n        )\n      }\n    }\n\n    const footer = (\n      <div className=\"similar-package-card__footer\">\n        <div\n          className={cx('similar-package-card__stat', {\n            'similar-package-card__comparison--similar': sizeDiff < 1500,\n            'similar-package-card__comparison--positive':\n              comparisonSizePercent < 0,\n            'similar-package-card__comparison--negative':\n              comparisonSizePercent > 0,\n          })}\n        >\n          {getComparisonNumber(comparisonSizePercent)}\n        </div>\n        <div className=\"similar-package-card__stat similar-package-card__size\">\n          <div className=\"similar-package-card__number\">\n            {size.toFixed(2)}\n            <span className=\"similar-package-card__shrink\"> {unit} </span>\n          </div>\n          <div className=\"similar-package-card__label\">Min + Gzip</div>\n        </div>\n\n        {(pack.hasJSModule || pack.hasJSNext || pack.isModuleType) && (\n          <TreeShakeIcon className=\"similar-package-card__treeshake\" />\n        )}\n      </div>\n    )\n\n    return (\n      <Link href={`/package/${pack.name}`} className=\"similar-package-card\">\n        <div className=\"similar-package-card__wrap\">\n          <div className=\"similar-package-card__header\">\n            <h3 className=\"similar-package-card__name\">{pack.name}</h3>\n            {pack.repository && (\n              <a\n                href={pack.repository}\n                onClick={e => {\n                  e.stopPropagation()\n                  window.location = pack.repository\n                }}\n              >\n                {pack.repository.includes('github.com') ? (\n                  <GithubIcon className=\"similar-package-card__github-icon\" />\n                ) : (\n                  <GitIcon className=\"similar-package-card__github-icon\" />\n                )}\n              </a>\n            )}\n          </div>\n          <p\n            className=\"similar-package-card__description\"\n            dangerouslySetInnerHTML={{\n              __html: sanitizeHTML(pack.description),\n            }}\n          />\n        </div>\n        {footer}\n      </Link>\n    )\n  }\n}\n"
  },
  {
    "path": "client/components/SimilarPackageCard/index.ts",
    "content": "import SimilarPackageCard from './SimilarPackageCard'\n\nexport default SimilarPackageCard\n"
  },
  {
    "path": "client/components/Stat/Stat.scss",
    "content": "@import '../../../stylesheets/variables';\n@import '../../../stylesheets/colors';\n\n.stat-container {\n  margin: 0 24px;\n\n  @media screen and (max-width: 40em) {\n    margin: 0 $global-spacing;\n  }\n}\n\n.stat-container--compact {\n  margin: 0;\n}\n\n.stat-container__value-container {\n  display: flex;\n  align-items: baseline;\n  justify-content: center;\n  padding: 5px 15px;\n\n  .stat-container--compact & {\n    padding-top: 0;\n  }\n}\n\n.stat-container__value {\n  @include font-size-xxl;\n  font-weight: bold;\n  color: #212121;\n  background: inherit;\n  position: relative;\n\n  .stat-container--compact & {\n    @include font-size-lg;\n    font-weight: $font-weight-light;\n  }\n}\n\n.stat-container__value.time::before {\n  content: attr(data-value);\n  position: absolute;\n  z-index: 2;\n  overflow: hidden;\n  color: $pastel-green;\n  white-space: nowrap;\n  width: 0%;\n  transition: width 0.3s;\n}\n\n.stat-container__value.time:hover::before {\n  width: 100%;\n  transition-duration: inherit;\n}\n\n.stat-container__unit {\n  @include font-size-xl;\n  color: $raven;\n  font-weight: bold;\n  margin-left: 4px;\n\n  .stat-container--compact & {\n    @include font-size-sm;\n    font-weight: $font-weight-thin;\n  }\n}\n\n.stat-container__footer {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  margin-top: 10px;\n\n  .stat-container--compact & {\n    margin-top: 0;\n  }\n}\n\n.stat-container__label {\n  @include font-size-reg;\n  color: $raven;\n  text-transform: uppercase;\n  letter-spacing: 2px;\n  text-align: center;\n\n  .stat-container--compact & {\n    @include font-size-sm;\n    letter-spacing: 1px;\n  }\n}\n\n.stat-container__info-text {\n  margin-left: $global-spacing * 0.5;\n  border: 1px solid rgba(40, 40, 40, 0.5);\n  color: rgba(40, 40, 40, 0.5);\n  width: 12px;\n  height: 13px;\n  font-family: $font-family-code;\n  font-size: 12px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 2px;\n  transition: background 0.2s;\n  cursor: help;\n\n  &:hover {\n    background: rgba(40, 40, 40, 0.6);\n    color: white;\n  }\n\n  &::after,\n  &::before {\n    font-family: $font-family-body;\n  }\n\n  @media screen and (max-width: 40em) {\n    display: none;\n  }\n}\n"
  },
  {
    "path": "client/components/Stat/Stat.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\n\nimport { formatSize, formatTime } from '../../../utils'\nimport { WithClassName } from '../../../types'\n\nconst Type = {\n  SIZE: 'size',\n  TIME: 'time',\n} as const\n\ntype StatProps = WithClassName & {\n  value: number\n  type: 'size' | 'time'\n  label: string\n  infoText?: string\n  compact?: boolean\n}\n\nexport default function Stat({\n  value,\n  label,\n  type,\n  infoText,\n  compact,\n  className,\n}: StatProps) {\n  const roundedValue =\n    type === Type.SIZE\n      ? parseFloat(formatSize(value).size.toFixed(1))\n      : parseFloat(formatTime(value).size.toFixed(2))\n\n  return (\n    <div\n      className={cx('stat-container', className, {\n        'stat-container--compact': compact,\n      })}\n    >\n      <div className=\"stat-container__value-container\">\n        <div className=\"stat-container__value-wrap\">\n          <div\n            className={cx('stat-container__value', type)}\n            style={{ transitionDuration: `${value}s` }}\n            data-value={roundedValue}\n          >\n            {roundedValue}\n          </div>\n        </div>\n        <div className=\"stat-container__unit\">\n          {type === Type.SIZE ? formatSize(value).unit : formatTime(value).unit}{' '}\n        </div>\n      </div>\n      <div className=\"stat-container__divider\" />\n      <div className=\"stat-container__footer\">\n        <div className=\"stat-container__label\">{label}</div>\n        {infoText && (\n          <div\n            className=\"stat-container__info-text\"\n            data-balloon-pos=\"right\"\n            data-balloon={infoText}\n          >\n            i\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n\nStat.type = Type\n"
  },
  {
    "path": "client/components/Stat/index.ts",
    "content": "export { default } from './Stat'\n"
  },
  {
    "path": "client/components/Treemap/Treemap.tsx",
    "content": "import React, { Component } from 'react'\n\nimport squarify from './squarify'\n\ntype TreeMapProps = {\n  width: number\n  height: number\n} & React.PropsWithChildren &\n  React.HTMLAttributes<HTMLDivElement>\n\nclass TreeMap extends Component<TreeMapProps> {\n  render() {\n    const { width, height, children, ...others } = this.props\n\n    const values = React.Children.map(children, square =>\n      React.isValidElement(square) ? square.props.value : square\n    )\n\n    const squared = squarify(values, width, height, 0, 0)\n    const getBorderRadius = (index: number) => {\n      const topLeftRadius =\n        squared[index][0] || squared[index][1] ? '0px' : '10px'\n      const topRightRadius =\n        squared[index][1] === 0 && squared[index][2] === width ? '10px' : '0px'\n      const bottomLeftRadius =\n        squared[index][3] === height && squared[index][0] === 0 ? '10px' : '0px'\n      const bottomRightRadius =\n        Math.round(squared[index][3]) === height &&\n        Math.round(squared[index][2]) === width\n          ? '10px'\n          : '0px'\n\n      return `${topLeftRadius} ${topRightRadius} ${bottomRightRadius} ${bottomLeftRadius}`\n    }\n\n    return (\n      <div style={{ width: '100%', height, position: 'relative' }} {...others}>\n        {React.Children.map(children, (child, index) => {\n          if (!React.isValidElement(child)) {\n            return child\n          }\n\n          const childProps = {\n            left: `${(squared[index][0] / width) * 100}%`,\n            top: `${(squared[index][1] / height) * 100}%`,\n            width: `${\n              ((squared[index][2] - squared[index][0]) / width) * 100\n            }%`,\n            height: `${\n              ((squared[index][3] - squared[index][1]) / height) * 100\n            }%`,\n            borderRadius: getBorderRadius(index),\n            data: squared[index],\n          }\n\n          return React.cloneElement(child, childProps)\n        })}\n      </div>\n    )\n  }\n}\n\nexport default TreeMap\n"
  },
  {
    "path": "client/components/Treemap/TreemapSquare.tsx",
    "content": "import React from 'react'\n\ntype TreemapSquareProps = {\n  style: React.CSSProperties\n  data?: any\n} & React.PropsWithChildren &\n  Pick<\n    React.CSSProperties,\n    'left' | 'top' | 'width' | 'height' | 'borderRadius'\n  >\n\nfunction TreemapSquare({\n  children,\n  left,\n  top,\n  width,\n  height,\n  borderRadius,\n  data,\n  style,\n  ...other\n}: TreemapSquareProps) {\n  return (\n    <div\n      data-vals={data.toString() + '...' + width + '...' + height}\n      style={{\n        position: 'absolute',\n        left,\n        top,\n        width,\n        height,\n        borderRadius,\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n        padding: '10px',\n        wordBreak: 'break-word',\n        flexDirection: 'column',\n        ...style,\n      }}\n      {...other}\n    >\n      {children}\n    </div>\n  )\n}\n\nexport default TreemapSquare\n"
  },
  {
    "path": "client/components/Treemap/index.ts",
    "content": "import Treemap from './Treemap'\nimport TreemapSquare from './TreemapSquare'\n\nexport { Treemap, TreemapSquare }\n"
  },
  {
    "path": "client/components/Treemap/squarify.js",
    "content": "/*\n * treemap-squarify.js - open source implementation of squarified treemaps\n *\n * Treemap Squared 0.5 - Treemap Charting library\n *\n * https://github.com/imranghory/treemap-squared/\n *\n * Copyright (c) 2012 Imran Ghory (imranghory@gmail.com)\n * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.\n *\n *\n * Implementation of the squarify treemap algorithm described in:\n *\n * Bruls, Mark; Huizing, Kees; van Wijk, Jarke J. (2000), \"Squarified treemaps\"\n * in de Leeuw, W.; van Liere, R., Data Visualization 2000:\n * Proc. Joint Eurographics and IEEE TCVG Symp. on Visualization, Springer-Verlag, pp. 33–42.\n *\n * Paper is available online at: http://www.win.tue.nl/~vanwijk/stm.pdf\n *\n * The code in this file is completeley decoupled from the drawing code so it should be trivial\n * to port it to any other vector drawing library. Given an array of datapoints this library returns\n * an array of cartesian coordinates that represent the rectangles that make up the treemap.\n *\n * The library also supports multidimensional data (nested treemaps) and performs normalization on the data.\n *\n * See the README file for more details.\n */\n\nfunction Container(xoffset, yoffset, width, height) {\n  this.xoffset = xoffset // offset from the the top left hand corner\n  this.yoffset = yoffset // ditto\n  this.height = height\n  this.width = width\n\n  this.shortestEdge = function () {\n    return Math.min(this.height, this.width)\n  }\n\n  // getCoordinates - for a row of boxes which we've placed\n  //                  return an array of their cartesian coordinates\n  this.getCoordinates = function (row) {\n    const coordinates = []\n    var subxoffset = this.xoffset,\n      subyoffset = this.yoffset //our offset within the container\n    var areawidth = sumArray(row) / this.height\n    var areaheight = sumArray(row) / this.width\n    var i\n\n    if (this.width >= this.height) {\n      for (i = 0; i < row.length; i++) {\n        coordinates.push([\n          subxoffset,\n          subyoffset,\n          subxoffset + areawidth,\n          subyoffset + row[i] / areawidth,\n        ])\n        subyoffset = subyoffset + row[i] / areawidth\n      }\n    } else {\n      for (i = 0; i < row.length; i++) {\n        coordinates.push([\n          subxoffset,\n          subyoffset,\n          subxoffset + row[i] / areaheight,\n          subyoffset + areaheight,\n        ])\n        subxoffset = subxoffset + row[i] / areaheight\n      }\n    }\n    return coordinates\n  }\n\n  // cutArea - once we've placed some boxes into an row we then need to identify the remaining area,\n  //           this function takes the area of the boxes we've placed and calculates the location and\n  //           dimensions of the remaining space and returns a container box defined by the remaining area\n  this.cutArea = function (area) {\n    var newcontainer\n\n    if (this.width >= this.height) {\n      var areawidth = area / this.height\n      var newwidth = this.width - areawidth\n      newcontainer = new Container(\n        this.xoffset + areawidth,\n        this.yoffset,\n        newwidth,\n        this.height\n      )\n    } else {\n      var areaheight = area / this.width\n      var newheight = this.height - areaheight\n      newcontainer = new Container(\n        this.xoffset,\n        this.yoffset + areaheight,\n        this.width,\n        newheight\n      )\n    }\n    return newcontainer\n  }\n}\n\n// normalize - the Bruls algorithm assumes we're passing in areas that nicely fit into our\n//             container box, this method takes our raw data and normalizes the data values into\n//             area values so that this assumption is valid.\nfunction normalize(data, area) {\n  var normalizeddata = []\n  var sum = sumArray(data)\n  var multiplier = area / sum\n  var i\n\n  for (i = 0; i < data.length; i++) {\n    normalizeddata[i] = data[i] * multiplier\n  }\n  return normalizeddata\n}\n\n// treemapSingledimensional - simple wrapper around squarify\nexport default function treemapSingledimensional(\n  data,\n  width,\n  height,\n  xoffset,\n  yoffset\n) {\n  xoffset = typeof xoffset === 'undefined' ? 0 : xoffset\n  yoffset = typeof yoffset === 'undefined' ? 0 : yoffset\n\n  var rawtreemap = squarify(\n    normalize(data, width * height),\n    [],\n    new Container(xoffset, yoffset, width, height),\n    []\n  )\n  return flattenTreemap(rawtreemap)\n}\n\n// flattenTreemap - squarify implementation returns an array of arrays of coordinates\n//                  because we have a new array everytime we switch to building a new row\n//                  this converts it into an array of coordinates.\nfunction flattenTreemap(rawtreemap) {\n  var flattreemap = []\n  var i, j\n\n  for (i = 0; i < rawtreemap.length; i++) {\n    for (j = 0; j < rawtreemap[i].length; j++) {\n      flattreemap.push(rawtreemap[i][j])\n    }\n  }\n  return flattreemap\n}\n\n// squarify  - as per the Bruls paper\n//             plus coordinates stack and containers so we get\n//             usable data out of it\nfunction squarify(data, currentrow, container, stack) {\n  var length\n  var nextdatapoint\n  var newcontainer\n\n  if (data.length === 0) {\n    stack.push(container.getCoordinates(currentrow))\n    return\n  }\n\n  length = container.shortestEdge()\n  nextdatapoint = data[0]\n\n  if (improvesRatio(currentrow, nextdatapoint, length)) {\n    currentrow.push(nextdatapoint)\n    squarify(data.slice(1), currentrow, container, stack)\n  } else {\n    newcontainer = container.cutArea(sumArray(currentrow), stack)\n    stack.push(container.getCoordinates(currentrow))\n    squarify(data, [], newcontainer, stack)\n  }\n  return stack\n}\n\n// improveRatio - implements the worse calculation and comparision as given in Bruls\n//                (note the error in the original paper; fixed here)\nfunction improvesRatio(currentrow, nextnode, length) {\n  var newrow\n\n  if (currentrow.length === 0) {\n    return true\n  }\n\n  newrow = currentrow.slice()\n  newrow.push(nextnode)\n\n  var currentratio = calculateRatio(currentrow, length)\n  var newratio = calculateRatio(newrow, length)\n\n  // the pseudocode in the Bruls paper has the direction of the comparison\n  // wrong, this is the correct one.\n  return currentratio >= newratio\n}\n\n// calculateRatio - calculates the maximum width to height ratio of the\n//                  boxes in this row\nfunction calculateRatio(row, length) {\n  var min = Math.min.apply(Math, row)\n  var max = Math.max.apply(Math, row)\n  var sum = sumArray(row)\n  return Math.max(\n    (Math.pow(length, 2) * max) / Math.pow(sum, 2),\n    Math.pow(sum, 2) / (Math.pow(length, 2) * min)\n  )\n}\n\n// isArray - checks if arr is an array\nfunction isArray(arr) {\n  return arr && arr.constructor === Array\n}\n\n// sumArray - sums a single dimensional array\nfunction sumArray(arr) {\n  var sum = 0\n  var i\n\n  for (i = 0; i < arr.length; i++) {\n    sum += arr[i]\n  }\n  return sum\n}\n"
  },
  {
    "path": "client/components/Warning/Warning.scss",
    "content": "@import '../../../stylesheets/colors';\n@import '../../../stylesheets/variables';\n\n.warning-bar {\n  @include font-size-xs;\n  background: lighten($dandelion, 5%);\n  padding: $global-spacing * 0.5 $global-spacing;\n  border-radius: 4px;\n  margin-top: 2vh;\n  color: darken(desaturate($dandelion, 35%), 35%);\n\n  a {\n    @include font-size-xxs;\n    color: inherit;\n    font-weight: $font-weight-bold;\n    opacity: 0.8;\n    padding-left: $global-spacing;\n    text-transform: uppercase;\n  }\n}\n"
  },
  {
    "path": "client/components/Warning/Warning.tsx",
    "content": "import React, { Component } from 'react'\n\nclass Warning extends Component<React.PropsWithChildren> {\n  render() {\n    return <div className=\"warning-bar\">{this.props.children}</div>\n  }\n}\n\nexport default Warning\n"
  },
  {
    "path": "client/components/Warning/index.ts",
    "content": "export { default } from './Warning'\n"
  },
  {
    "path": "client/config/colors.ts",
    "content": "export default [\n  '#718af0',\n  '#6e98e6',\n  '#79c0f2',\n  '#7dd6fa',\n  '#6ed0db',\n  '#59b3aa',\n  '#7ebf80',\n  '#9bc26b',\n  '#dee675',\n  '#fff080',\n  '#ffd966',\n  '#ffbf66',\n  '#ff8a66',\n  '#ed7872',\n  '#db6b8f',\n  '#bd66cc',\n  '#cae0eb',\n] as const\n"
  },
  {
    "path": "client/config/scanBlacklist.ts",
    "content": "/**\n * Packages that are unlikely to be useful in\n * determining size of frontend bundles.\n */\nexport default [\n  /*** Config ****/\n  /dotenv/,\n\n  /*** CLI Tools ***/\n  /gulp/,\n  /cli/,\n\n  /*** Build Tools ****/\n  /webpack/,\n  /react-native/,\n  /babel/,\n  /rollup/,\n  /autoprefixer/,\n  /css-nano/,\n  /node-sass/,\n  /next/,\n  /create-react-app/,\n  /react-scripts/,\n  /-loader/,\n  /extract-plugin/,\n\n  /**** Testing ****/\n  /jest/,\n  /enzyme/,\n  /mocha/,\n  /ava/,\n  /nightwatch/,\n\n  /**** Server libraries ****/\n  /koa/,\n  /express/,\n  /pm2/,\n  /nodemon/,\n  /supervisor/,\n\n  /**** Common dev dependencies ****/\n  /prop-types/,\n  /devtools/,\n] as const\n"
  },
  {
    "path": "index.js",
    "content": "const { register } = require('esbuild-register/dist/node')\nconst tsconfig = require('./tsconfig.server.json')\nregister({\n  tsconfigRaw: tsconfig,\n  target: tsconfig.compilerOptions.target,\n})\nrequire('./index.ts')\n"
  },
  {
    "path": "index.ts",
    "content": "require('dotenv-defaults').config()\n\nimport next from 'next'\nimport exec from 'execa'\nimport { parse } from 'url'\n\nimport Koa, { Context } from 'koa'\nimport proxy from 'koa-proxy'\nimport serve from 'koa-static'\nimport Router from '@koa/router'\nimport compress from 'koa-compress'\nimport cacheControl from 'koa-cache-control'\nimport requestId from 'koa-requestid'\nimport auth from 'koa-basic-auth'\nimport bodyParser from 'koa-bodyparser'\nimport invariant from 'ts-invariant'\n\nimport Cache from './utils/cache.utils'\nimport { parsePackageString } from './utils/common.utils'\nimport firebaseUtils from './utils/firebase.utils'\nimport logger from './server/Logger'\n\nimport limit from './server/middlewares/rateLimit.middleware'\nimport exportsMiddlware from './server/middlewares/exports.middleware'\nimport exportsSizesMiddlware from './server/middlewares/exportsSizes.middleware'\nimport resolvePackageMiddleware from './server/middlewares/results/resolvePackage.middleware'\nimport cachedResponseMiddleware from './server/middlewares/results/cachedResponse.middleware'\nimport buildMiddleware from './server/middlewares/results/build.middleware'\nimport errorMiddleware from './server/middlewares/results/error.middleware'\nimport blockBlacklistMiddleware from './server/middlewares/results/blockBlacklist.middleware'\nimport requestLoggerMiddleware from './server/middlewares/requestLogger.middleware'\nimport similarPackagesMiddleware from './server/middlewares/similar-packages/similarPackages.middleware'\nimport generateImgMiddleware from './server/middlewares/generateImg.middleware'\n\nimport jsonCacheMiddleware from './server/middlewares/jsonCache.middleware'\n\nimport config from './server/config'\n\nfunction getEnv(env: Record<string, string | undefined | null>) {\n  invariant(\n    env.BASIC_AUTH_PASSWORD,\n    'Environment variable BASIC_AUTH_PASSWORD is required'\n  )\n  invariant(env.NODE_ENV, 'Environment variable NODE_ENV is required')\n\n  return {\n    basicAuthPassword: env.BASIC_AUTH_PASSWORD,\n    port: env.PORT ? parseInt(env.PORT) : config.DEFAULT_DEV_PORT,\n    nodeEnv: env.NODE_ENV,\n  }\n}\n\nconst env = getEnv(process.env)\n\nconst cache = new Cache()\nconst port = env.port\nconst dev = env.nodeEnv !== 'production'\nconst app = next({ dev })\nconst handle = app.getRequestHandler()\n\napp.prepare().then(() => {\n  const server = new Koa()\n  const router = new Router()\n\n  server.use(requestId())\n  server.use(bodyParser())\n  server.use(requestLoggerMiddleware)\n  server.use(cacheControl())\n\n  if (!dev) {\n    server.use(\n      limit({\n        duration: 1000 * 60 * 5, //  5 mins\n        max: 60,\n        whiteList: ['127.0.0.1', '::1'],\n      })\n    )\n  }\n\n  server.use(async (ctx, next) => {\n    try {\n      await next()\n    } catch (err: any) {\n      if (401 == err.status) {\n        ctx.status = 401\n        ctx.set('WWW-Authenticate', 'Basic')\n        ctx.body = 'Permission denied'\n      } else {\n        throw err\n      }\n    }\n  })\n\n  server.use(\n    compress({\n      filter: function (contentType) {\n        return /(text|json|javascript|svg)/.test(contentType)\n      },\n      threshold: 2048,\n      gzip: {\n        flush: require('zlib').Z_SYNC_FLUSH,\n      },\n    })\n  )\n\n  server.use(\n    serve('./client/assets/public', {\n      maxage: config.CACHE.PUBLIC_ASSETS * 1000,\n    })\n  )\n\n  server.use(\n    proxy({\n      match: /^\\/-\\/search/,\n      host: 'https://www.npmjs.com',\n    })\n  )\n\n  type Key = {\n    name: string\n    version: string\n  }\n\n  router.get(\n    '/api/size',\n    jsonCacheMiddleware({\n      get: (key: Key) => cache.getPackageSize(key),\n      set: (key: Key, value: string) => cache.setPackageSize(key, value),\n      hash: (ctx: Context) => ({\n        name: ctx.state.resolved.name,\n        version: ctx.state.resolved.version,\n      }),\n    }),\n    errorMiddleware,\n    resolvePackageMiddleware,\n    blockBlacklistMiddleware,\n    cachedResponseMiddleware,\n    buildMiddleware\n  )\n\n  router.get(\n    '/api/exports',\n    errorMiddleware,\n    resolvePackageMiddleware,\n    blockBlacklistMiddleware,\n    exportsMiddlware\n  )\n\n  router.get(\n    '/api/exports-sizes',\n    jsonCacheMiddleware({\n      get: (key: Key) => cache.getExportsSize(key),\n      set: (key: Key, value: string) => cache.setExportsSize(key, value),\n      hash: (ctx: Context) => ({\n        name: ctx.state.resolved.name,\n        version: ctx.state.resolved.version,\n      }),\n    }),\n    errorMiddleware,\n    resolvePackageMiddleware,\n    blockBlacklistMiddleware,\n    cachedResponseMiddleware,\n    exportsSizesMiddlware\n  )\n\n  router.get('/api/recent', async ctx => {\n    try {\n      ctx.cacheControl = {\n        maxAge: config.CACHE.RECENTS_API,\n      }\n      ctx.body = await firebaseUtils.getRecentSearches(Number(ctx.query.limit))\n    } catch (err: any) {\n      console.error('in /api/recent', err)\n      logger.error('RECENT', err, 'RECENT FAILED: failed')\n      ctx.status = 422\n      ctx.body = { type: err.name, message: err.message }\n    }\n  })\n\n  router.get('/api/package-history', async ctx => {\n    const { name } = parsePackageString(ctx.query.package)\n    try {\n      ctx.cacheControl = {\n        maxAge: config.CACHE.PACKAGE_HISTORY_API,\n      }\n      ctx.body = await firebaseUtils.getPackageHistory(\n        name,\n        Number(ctx.query.limit)\n      )\n    } catch (err: any) {\n      console.error(err)\n      logger.error(\n        'HISTORY',\n        err,\n        'HISTORY FAILED: for package' + ctx.query.package\n      )\n      ctx.status = 422\n      ctx.body = { type: err.name, message: err.message }\n    }\n  })\n\n  router.get('/api/similar-packages', similarPackagesMiddleware)\n\n  router.get('/api/stats-image', generateImgMiddleware)\n\n  router.get(\n    '/admin/restart',\n    auth({ name: 'bundlephobia', pass: env.basicAuthPassword }),\n    async (ctx, next) => {\n      try {\n        const { stdout, stderr } = await exec.command('pm2 reload all')\n        ctx.body = 'Server restarted' + stdout\n      } catch (err) {\n        console.error('Failed to restart', err)\n        ctx.status = 500\n        ctx.body = err\n      }\n    }\n  )\n\n  router.post('/admin/restart', async ctx => {\n    const { name, pass } = <{ name?: string; pass?: string }>ctx.request.body\n    if (name !== 'bundlephobia' || pass !== env.basicAuthPassword) {\n      console.error('Failed to restart')\n      ctx.status = 500\n      ctx.body = 'Failed to restart'\n    } else {\n      const { stdout, stderr } = await exec.command('pm2 reload all')\n      ctx.body = 'Server restarted' + stdout\n      console.error(stderr)\n    }\n  })\n\n  router.get(\n    '/admin/clear-cache',\n    auth({ name: 'bundlephobia', pass: env.basicAuthPassword }),\n    async (ctx, next) => {\n      try {\n        const { stdout } = await exec.command(\n          'rm -rf /tmp/tmp-build/cache/_cacache /tmp/tmp-build/packages/'\n        )\n        ctx.body = 'Cache cleared' + stdout\n      } catch (err) {\n        console.error('Failed to clear cache', err)\n        ctx.status = 500\n        ctx.body = err\n      }\n    }\n  )\n\n  router.get('/result', async ctx => {\n    invariant(ctx.query.p, 'p parameter is required')\n    const packageString =\n      typeof ctx.query.p === 'string' ? ctx.query.p : ctx.query.p.join('/')\n\n    ctx.redirect(`/package/${packageString.trim()}`)\n    ctx.status = 301\n  })\n\n  router.get('(.*)', async ctx => {\n    invariant(ctx.req.url, 'url is missing')\n    const parsedUrl = parse(ctx.req.url, true)\n    await handle(ctx.req, ctx.res, parsedUrl)\n    ctx.respond = false\n  })\n\n  server.use(async (ctx, next) => {\n    ctx.res.statusCode = 200\n    await next()\n  })\n\n  server.use(router.routes())\n  server.listen(port, () => {\n    console.log(`> Ready on http://localhost:${port}`)\n  })\n})\n"
  },
  {
    "path": "next.config.js",
    "content": "const path = require('path')\n\nmodule.exports = {\n  pageExtensions: ['page.js', 'page.tsx'],\n  sassOptions: {\n    includePaths: [path.join(__dirname, 'stylesheets')],\n  },\n  env: {\n    RELEASE_DATE: new Date().toDateString(),\n  },\n  webpack(config) {\n    config.module.rules.push({\n      test: /\\.svg$/i,\n      issuer: /\\.[jt]sx?$/,\n      use: [\n        {\n          loader: '@svgr/webpack',\n          options: {\n            svgoConfig: {\n              plugins: [\n                {\n                  name: 'removeViewBox',\n                  active: false,\n                },\n              ],\n            },\n          },\n        },\n      ],\n    })\n\n    return config\n  },\n}\n"
  },
  {
    "path": "nodemon.json",
    "content": "{\n  \"exec\": \"ts-node --project tsconfig.server.json ./index.ts\",\n  \"ext\": \"js ts\"\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"bundlephobia\",\n  \"version\": \"1.0.1\",\n  \"description\": \"Find the cost of adding new frontend dependencies\",\n  \"main\": \"index.ts\",\n  \"author\": \"Shubham Kanodia <shubham.kanodia10@gmail.com>\",\n  \"license\": \"MIT\",\n  \"private\": true,\n  \"workspaces\": [\n    \"build-service\",\n    \"cache-service\",\n    \"scripts\"\n  ],\n  \"engines\": {\n    \"node\": \">= 24.0.0\",\n    \"npm\": \">= 5.4.x\"\n  },\n  \"dependencies\": {\n    \"@contentful/rich-text-react-renderer\": \"^15.17.0\",\n    \"@contentful/rich-text-types\": \"^15.15.1\",\n    \"@koa/router\": \"^12.0.1\",\n    \"@next/font\": \"^13.5.6\",\n    \"@types/koa__router\": \"^12.0.4\",\n    \"animejs\": \"^3.2.2\",\n    \"array-to-sentence\": \"^2.0.0\",\n    \"axios\": \"^0.21.4\",\n    \"balloon-css\": \"^0.4.0\",\n    \"bfj\": \"^9.0.2\",\n    \"big-json\": \"^3.2.0\",\n    \"canvas\": \"^3.2.1\",\n    \"classnames\": \"^2.5.1\",\n    \"date-fns\": \"^2.30.0\",\n    \"debounce\": \"^1.2.1\",\n    \"debug\": \"^4.3.4\",\n    \"dompurify\": \"^2.4.7\",\n    \"dotenv\": \"^8.6.0\",\n    \"dotenv-defaults\": \"^2.0.2\",\n    \"esbuild\": \"^0.18.20\",\n    \"esbuild-register\": \"^3.5.0\",\n    \"execa\": \"^5.1.1\",\n    \"fabric\": \"^6.0.0\",\n    \"firebase\": \"^8.10.1\",\n    \"firebase-admin\": \"^12.6.0\",\n    \"flatten\": \"^1.0.3\",\n    \"git-url-parse\": \"^13.1.1\",\n    \"github\": \"^10.1.0\",\n    \"got\": \"^9.6.0\",\n    \"hot-shots\": \"^6.8.7\",\n    \"ipchecker\": \"^0.0.2\",\n    \"is-empty-object\": \"^1.1.1\",\n    \"jsdom\": \"^24.0.0\",\n    \"jsonstream\": \"^1.0.3\",\n    \"koa\": \"^2.15.0\",\n    \"koa-basic-auth\": \"^4.0.0\",\n    \"koa-better-ratelimit\": \"^2.1.2\",\n    \"koa-bodyparser\": \"^4.4.1\",\n    \"koa-cache-control\": \"^2.0.0\",\n    \"koa-cash\": \"3.0.4\",\n    \"koa-compress\": \"^3.1.0\",\n    \"koa-proxy\": \"^0.9.0\",\n    \"koa-requestid\": \"^2.2.1\",\n    \"koa-send\": \"^5.0.1\",\n    \"koa-static\": \"^5.0.0\",\n    \"lodash\": \"^4.17.21\",\n    \"lodash.isequal\": \"^4.5.0\",\n    \"lodash.sortby\": \"^4.7.0\",\n    \"lru-cache\": \"^6.0.0\",\n    \"memory-fs\": \"^0.5.0\",\n    \"mkdir-promise\": \"^1.0.0\",\n    \"natural\": \"^5.2.4\",\n    \"next\": \"^13.5.6\",\n    \"node-fetch\": \"^2.7.0\",\n    \"normalize.css\": \"^8.0.1\",\n    \"p-queue\": \"^3.2.0\",\n    \"package-build-stats\": \"8.2.5\",\n    \"pacote\": \"^11.3.5\",\n    \"performance-now\": \"^2.1.0\",\n    \"progress-stream\": \"^2.0.0\",\n    \"promise-queue-plus\": \"^1.2.2\",\n    \"promise.series\": \"^0.2.0\",\n    \"query-string\": \"^7.1.3\",\n    \"react\": \"18.2.0\",\n    \"react-autocomplete\": \"^1.8.1\",\n    \"react-contentful\": \"^2.0.31\",\n    \"react-dom\": \"18.2.0\",\n    \"react-dropzone\": \"^7.0.1\",\n    \"react-flip-move\": \"^3.0.5\",\n    \"react-sidebar\": \"^3.0.2\",\n    \"remark\": \"^9.0.0\",\n    \"sass\": \"^1.70.0\",\n    \"semver\": \"^7.6.3\",\n    \"stream-chain\": \"^3.3.2\",\n    \"stream-json\": \"^1.8.0\",\n    \"strip-markdown\": \"^4.2.0\",\n    \"trending-github\": \"^1.2.0\",\n    \"truncate\": \"^3.0.0\",\n    \"ts-invariant\": \"^0.10.3\",\n    \"uglifyjs-webpack-plugin\": \"^2.2.0\",\n    \"unfetch\": \"^4.2.0\",\n    \"webpack\": \"^4.47.0\",\n    \"winston\": \"^3.11.0\",\n    \"workerpool\": \"^6.5.1\"\n  },\n  \"scripts\": {\n    \"dev\": \"DEBUG='bp:*' NODE_ENV=development nodemon --watch ./server --watch ./utils ./index.ts\",\n    \"build\": \"NODE_ENV=production next build\",\n    \"prebuild\": \"node bin/generate-sitemap.js\",\n    \"prestart\": \"yarn build\",\n    \"start\": \"DEBUG='bp:*' NODE_ENV=production node ./index.js\",\n    \"prod\": \"yarn run build && yarn start\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watch\",\n    \"prettier\": \"prettier --write '**/*.{html,js,json,css,scss,jsx,flow,md,yml,yaml}'\",\n    \"lint\": \"next lint\",\n    \"deploy\": \"git branch -D gh-pages && git checkout -b gh-pages && yarn run build && yarn run export && cp -R out/* . \"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"next lint --fix && pretty-quick --staged\"\n    }\n  },\n  \"prettier\": {\n    \"semi\": false,\n    \"arrowParens\": \"avoid\",\n    \"singleQuote\": true\n  },\n  \"resolutions\": {\n    \"canvas\": \"^3.2.1\",\n    \"@types/react\": \"18.2.48\"\n  },\n  \"devDependencies\": {\n    \"@svgr/webpack\": \"^6.5.1\",\n    \"@swc/core\": \"^1.3.107\",\n    \"@swc/helpers\": \"^0.5.3\",\n    \"@types/animejs\": \"^3.1.12\",\n    \"@types/debounce\": \"^1.2.4\",\n    \"@types/jsonstream\": \"^0.8.33\",\n    \"@types/koa\": \"^2.14.0\",\n    \"@types/koa-basic-auth\": \"^2.0.6\",\n    \"@types/koa-bodyparser\": \"^4.3.12\",\n    \"@types/koa-cache-control\": \"^2.0.5\",\n    \"@types/koa-cash\": \"^4.1.3\",\n    \"@types/koa-compress\": \"^4.0.6\",\n    \"@types/koa-proxy\": \"^1.0.7\",\n    \"@types/koa-static\": \"^4.0.4\",\n    \"@types/node\": \"^24.0.0\",\n    \"@types/progress-stream\": \"^2\",\n    \"@types/react\": \"18.2.48\",\n    \"@types/react-autocomplete\": \"1.8.10\",\n    \"@types/react-dom\": \"18.2.18\",\n    \"@types/react-sidebar\": \"^3.0.4\",\n    \"@types/semver\": \"^7.7.1\",\n    \"@types/stream-json\": \"^1\",\n    \"autoprefixer\": \"^9.8.8\",\n    \"eslint\": \"^8.56.0\",\n    \"eslint-config-next\": \"^13.5.6\",\n    \"eslint-config-prettier\": \"^8.10.0\",\n    \"glob\": \"^7.2.3\",\n    \"husky\": \"^4.3.8\",\n    \"jest\": \"^24.9.0\",\n    \"nodemon\": \"^2.0.22\",\n    \"postcss\": \"^8.4.33\",\n    \"postcss-easy-import\": \"^3.0.0\",\n    \"postcss-loader\": \"^7.3.4\",\n    \"prettier\": \"2.8.8\",\n    \"pretty-quick\": \"^3.3.1\",\n    \"sass-loader\": \"^10.5.2\",\n    \"sitemap\": \"^7.1.1\",\n    \"ts-node\": \"^10.9.2\",\n    \"typescript\": \"^4.9.5\"\n  },\n  \"packageManager\": \"yarn@4.0.2\"\n}\n"
  },
  {
    "path": "pages/_app.page.tsx",
    "content": "import React from 'react'\nimport Head from 'next/head'\nimport { AppProps } from 'next/app'\nimport '../stylesheets/index.scss'\n\nfunction App({ Component, pageProps }: AppProps) {\n  return (\n    <>\n      <Head>\n        <meta\n          name=\"viewport\"\n          content=\"width=device-width, initial-scale=1, shrink-to-fit=no\"\n        />\n        <title key=\"title\">Bundlephobia ❘ cost of adding a npm package</title>\n      </Head>\n      <Component {...pageProps} />\n    </>\n  )\n}\n\nexport default App\n"
  },
  {
    "path": "pages/_document.page.tsx",
    "content": "import React from 'react'\nimport Document, {\n  DocumentContext,\n  Head as DocumentHead,\n  Html,\n  Main,\n  NextScript,\n} from 'next/document'\n\nconst amplitudeScript = `\n(function(e,t){var n=e.amplitude||{_q:[],_iq:{}};var r=t.createElement(\"script\")\n;r.type=\"text/javascript\"\n;r.integrity=\"sha384-girahbTbYZ9tT03PWWj0mEVgyxtZoyDF9KVZdL+R53PP5wCY0PiVUKq0jeRlMx9M\"\n;r.crossOrigin=\"anonymous\";r.async=true\n;r.src=\"https://cdn.amplitude.com/libs/amplitude-7.2.1-min.gz.js\"\n;r.onload=function(){if(!e.amplitude.runQueuedFunctions){\nconsole.log(\"[Amplitude] Error: could not load SDK\")}}\n;var i=t.getElementsByTagName(\"script\")[0];i.parentNode.insertBefore(r,i)\n;function s(e,t){e.prototype[t]=function(){\nthis._q.push([t].concat(Array.prototype.slice.call(arguments,0)));return this}}\nvar o=function(){this._q=[];return this}\n;var a=[\"add\",\"append\",\"clearAll\",\"prepend\",\"set\",\"setOnce\",\"unset\"]\n;for(var c=0;c<a.length;c++){s(o,a[c])}n.Identify=o;var u=function(){this._q=[]\n;return this}\n;var l=[\"setProductId\",\"setQuantity\",\"setPrice\",\"setRevenueType\",\"setEventProperties\"]\n;for(var p=0;p<l.length;p++){s(u,l[p])}n.Revenue=u\n;var d=[\"init\",\"logEvent\",\"logRevenue\",\"setUserId\",\"setUserProperties\",\"setOptOut\",\"setVersionName\",\"setDomain\",\"setDeviceId\",\"enableTracking\",\"setGlobalUserProperties\",\"identify\",\"clearUserProperties\",\"setGroup\",\"logRevenueV2\",\"regenerateDeviceId\",\"groupIdentify\",\"onInit\",\"logEventWithTimestamp\",\"logEventWithGroups\",\"setSessionId\",\"resetSessionId\"]\n;function v(e){function t(t){e[t]=function(){\ne._q.push([t].concat(Array.prototype.slice.call(arguments,0)))}}\nfor(var n=0;n<d.length;n++){t(d[n])}}v(n);n.getInstance=function(e){\ne=(!e||e.length===0?\"$default_instance\":e).toLowerCase()\n;if(!n._iq.hasOwnProperty(e)){n._iq[e]={_q:[]};v(n._iq[e])}return n._iq[e]}\n;e.amplitude=n})(window,document);\n\n  amplitude.getInstance()\n   .init(\n     \"a9919b09b92f868530fb24f628bd35c0\",\n      undefined, \n      {includeReferrer: true, includeUtm: true, includeGclid: true}\n    );\n`\n\nexport default class MyDocument extends Document {\n  static async getInitialProps(ctx: DocumentContext) {\n    const initialProps = await Document.getInitialProps(ctx)\n    return { ...initialProps }\n  }\n\n  render() {\n    return (\n      <Html lang=\"en\">\n        <DocumentHead>\n          <meta charSet=\"utf-8\" />\n          <meta httpEquiv=\"x-ua-compatible\" content=\"ie=edge\" />\n          <meta name=\"application-name\" content=\"Bundlephobia\" />\n          <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n          <link\n            rel=\"preconnect\"\n            href=\"https://fonts.gstatic.com\"\n            crossOrigin=\"anonymous\"\n          />\n          <link\n            href=\"https://fonts.googleapis.com/css?family=Source+Code+Pro:300,400,600\"\n            rel=\"stylesheet\"\n          />\n          <link\n            rel=\"search\"\n            type=\"application/opensearchdescription+xml\"\n            href=\"/open-search-description.xml\"\n            title=\"bundlephobia\"\n          />\n          <link\n            rel=\"apple-touch-icon\"\n            sizes=\"180x180\"\n            href=\"/apple-touch-icon.png\"\n          />\n          <link\n            rel=\"icon\"\n            type=\"image/png\"\n            sizes=\"32x32\"\n            href=\"/favicon-32x32.png?l=4\"\n          />\n          <link\n            rel=\"icon\"\n            type=\"image/png\"\n            sizes=\"16x16\"\n            href=\"/favicon-16x16.png?l=3\"\n          />\n          <link rel=\"manifest\" href=\"/manifest.json\" />\n          <link rel=\"mask-icon\" href=\"/safari-pinned-tab.svg\" color=\"#5bbad5\" />\n          <meta name=\"apple-mobile-web-app-title\" content=\"Bundlephobia\" />\n          <meta name=\"application-name\" content=\"Bundlephobia\" />\n          <meta name=\"theme-color\" content=\"#212121\" />\n\n          <meta\n            name=\"google-site-verification\"\n            content=\"XizU-iXvsrtQJG5G4DWEGhD57SRRA8x3Y9FnSwk53X0\"\n          />\n          <script\n            dangerouslySetInnerHTML={{\n              __html: amplitudeScript,\n            }}\n          />\n        </DocumentHead>\n        <body>\n          <Main />\n          <NextScript />\n        </body>\n\n        {/* See https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-javascript-proxy-api */}\n        <script\n          dangerouslySetInnerHTML={{\n            __html: `\n          if (\"Proxy\" in window) {\n            var handler = {\n              get: function(_, key) {\n                return new Proxy(function(cb) {\n                  if (key === \"flush\" || key === \"close\") return Promise.resolve();\n                  if (typeof cb === \"function\") return cb(window.Sentry);\n                  return window.Sentry;\n                }, handler);\n              },\n            };\n            window.Sentry = new Proxy({}, handler);\n          }\n        `,\n          }}\n        />\n        <script\n          src=\"https://browser.sentry-cdn.com/5.15.0/bundle.min.js\"\n          crossOrigin=\"anonymous\"\n        />\n        <script\n          src=\"https://browser.sentry-cdn.com/5.15.0/extraerrordata.min.js\"\n          crossOrigin=\"anonymous\"\n        />\n        <script\n          src=\"https://browser.sentry-cdn.com/5.15.0/captureconsole.min.js\"\n          crossOrigin=\"anonymous\"\n        />\n        <script\n          dangerouslySetInnerHTML={{\n            __html: `Sentry.init({ \n              dsn: 'https://c28864debd5f47b2a89d05c74cd60c1c@sentry.io/5174673',\n              release: \"${process.env.RELEASE_DATE}\",\n              environment: \"${process.env.NODE_ENV}\",\n              attachStacktrace: true\n            })`,\n          }}\n        />\n      </Html>\n    )\n  }\n}\n"
  },
  {
    "path": "pages/blog/components/Article.tsx",
    "content": "import { useRouter } from 'next/router'\nimport React from 'react'\nimport BlogLayout from '../../../client/components/BlogLayout'\nimport { useContentful } from 'react-contentful'\nimport BlogPost from './Post'\nimport ContentfulProvider from './ContentfulProvider'\n\nconst ArticleWithContent = () => {\n  return (\n    <ContentfulProvider>\n      <BlogLayout className=\"blog\">\n        <Article />\n      </BlogLayout>\n    </ContentfulProvider>\n  )\n}\n\nconst Article = () => {\n  const router = useRouter()\n\n  const { data, error, loading } = useContentful({\n    contentType: 'blogPost',\n  })\n\n  if (loading) {\n    return <>Loading...</>\n  } else if (error) {\n    return (\n      <pre>\n        <code>{error}</code>\n      </pre>\n    )\n  } else if (data) {\n    return (\n      <>\n        {data.items.map(item => (\n          <BlogPost\n            key={item.fields.title}\n            title={item.fields.title}\n            content={item.fields.content}\n            slug={item.fields.slug}\n            createdAt={item.fields.createdAt || item.sys.createdAt}\n          />\n        ))}{' '}\n      </>\n    )\n  }\n\n  return <>Loading...</>\n}\n\nexport default ArticleWithContent\n"
  },
  {
    "path": "pages/blog/components/ContentfulProvider.tsx",
    "content": "import React from 'react'\nimport {\n  ContentfulClient,\n  ContentfulClientInterface,\n  ContentfulProvider as ContentfulProviderOriginal,\n} from 'react-contentful'\n\nconst contentfulClient: ContentfulClientInterface =\n  new (ContentfulClient as any)({\n    accessToken:\n      process.env.NODE_ENV === 'production'\n        ? 'mTlrTPvam3pHJYiOQyFAjKp2rxIBRo1qelSJPrSQUPE'\n        : 'MvUlRvc3rMnq7CXCBPxiXSUstNscsq9XH8kMANcxoY8',\n    host:\n      process.env.NODE_ENV === 'production'\n        ? 'cdn.contentful.com'\n        : 'preview.contentful.com',\n    space: '9cnlte662r2w',\n  })\n\nconst ContentfulProvider = ({ children }: React.PropsWithChildren) => (\n  <ContentfulProviderOriginal client={contentfulClient}>\n    {children}\n  </ContentfulProviderOriginal>\n)\n\nexport default ContentfulProvider\n"
  },
  {
    "path": "pages/blog/components/Post.tsx",
    "content": "import React from 'react'\nimport {\n  documentToReactComponents,\n  NodeRenderer,\n} from '@contentful/rich-text-react-renderer'\nimport { BLOCKS, Document, TopLevelBlock } from '@contentful/rich-text-types'\nimport Link from 'next/link'\n\nconst getWordCount = (node: TopLevelBlock) => {\n  let count = 0\n  if (node.nodeType === 'paragraph') {\n    node.content.forEach(content => {\n      switch (content.nodeType) {\n        case 'text':\n          count += content.value.split(' ').length\n      }\n    })\n  } else if (node.nodeType === 'embedded-asset-block') {\n    count += 80\n  }\n\n  return count\n}\n\nconst makeContentPreview = (content: Document) => {\n  const wordLimit = 50\n  let wordCount = 0\n  const previewContent = []\n  let counter = 0\n\n  const { content: innerContent, ...others } = content\n\n  while (wordCount < wordLimit && counter < innerContent.length) {\n    previewContent.push(innerContent[counter])\n    wordCount += getWordCount(innerContent[counter])\n    counter++\n  }\n\n  return { ...others, content: previewContent }\n}\n\ntype PostProps = {\n  title: React.ReactNode\n  content: Document\n  slug: string\n  preview?: boolean\n  createdAt: string | number | Date\n}\n\nconst Post = ({ title, content, slug, preview, createdAt }: PostProps) => {\n  const options = {\n    renderNode: {\n      [BLOCKS.EMBEDDED_ASSET]: (node: Parameters<NodeRenderer>[0]) => {\n        // render the EMBEDDED_ASSET as you need\n        return (\n          <img\n            src={node.data.target.fields.file.url}\n            height={node.data.target.fields.file.details.image.height}\n            width={node.data.target.fields.file.details.image.width}\n            alt={node.data.target.fields.description}\n          />\n        )\n      },\n    },\n  }\n\n  return (\n    <article className=\"blog-post__preview\">\n      {preview ? (\n        <Link href={`/blog/${slug}`}>\n          <h2>{title}</h2>\n        </Link>\n      ) : (\n        <h1>{title}</h1>\n      )}\n      <h4 className=\"blog-post__preview-date\">\n        {new Intl.DateTimeFormat('en-GB', {\n          dateStyle: 'long',\n        }).format(new Date(createdAt))}\n      </h4>\n      <div className=\"blog-post__preview-content\">\n        {documentToReactComponents(\n          preview ? makeContentPreview(content) : content,\n          options\n        )}\n      </div>\n      {preview && (\n        <Link href={`/blog/${slug}`} className=\"blog-post__preview-read-more\">\n          Read more...\n        </Link>\n      )}\n    </article>\n  )\n}\n\nexport default Post\n"
  },
  {
    "path": "pages/blog/digital-ocean-partnership.page.tsx",
    "content": "import Article from './components/Article'\n\nconst ArticleIs = () => <Article />\n\nexport default ArticleIs\n"
  },
  {
    "path": "pages/blog/index.page.tsx",
    "content": "import React from 'react'\nimport BlogLayout from '../../client/components/BlogLayout'\nimport { useContentful } from 'react-contentful'\nimport Separator from '../../client/components/Separator'\nimport Post from './components/Post'\nimport ContentfulProvider from './components/ContentfulProvider'\n\nconst BlogWithContent = () => {\n  return (\n    <ContentfulProvider>\n      <BlogLayout className=\"blog\">\n        <BlogHome />\n      </BlogLayout>\n    </ContentfulProvider>\n  )\n}\n\nconst BlogHome = () => {\n  const { data, error, loading } = useContentful({\n    contentType: 'blogPost',\n  })\n\n  let content = null\n\n  if (loading) {\n    content = 'Loading...'\n  } else if (error) {\n    content = (\n      <pre>\n        <code>{error}</code>\n      </pre>\n    )\n  } else if (data) {\n    content = (\n      <>\n        {data.items.map(item => (\n          <Post\n            key={item.fields.title}\n            title={item.fields.title}\n            content={item.fields.content}\n            slug={item.fields.slug}\n            createdAt={item.fields.createdAt || item.sys.createdAt}\n            preview={true}\n          />\n        ))}{' '}\n      </>\n    )\n  }\n\n  return (\n    <>\n      <h1> Blogosphere </h1>\n      <Separator\n        text=\"\"\n        align=\"normal\"\n        showLeft={false}\n        containerStyles={{ marginBottom: '2rem' }}\n      />\n      {content}\n    </>\n  )\n}\n\nexport default BlogWithContent\n"
  },
  {
    "path": "pages/compare/ComparePage.js",
    "content": "import React, { PureComponent } from 'react'\nimport Head from 'next/head'\n\nimport Layout from '../../client/components/Layout'\nimport BarGraph from '../../client/components/BarGraph'\nimport AutocompleteInput from '../../client/components/AutocompleteInput'\nimport BuildProgressIndicator from '../../client/components/BuildProgressIndicator'\nimport Router from 'next/router'\nimport Link from 'next/link'\nimport isEmptyObject from 'is-empty-object'\nimport { parsePackageString } from '../../utils/common.utils'\n\nimport API from '../../client/api'\n\nimport GithubLogo from '../../client/assets/github-logo.svg'\nimport EmptyBox from '../../client/assets/empty-box.svg'\n\nexport default class ResultPage extends PureComponent {\n  fetchResults = packageString => {\n    const startTime = Date.now()\n\n    API.getInfo(packageString)\n      .then(results => {\n        const newPackageString = `${results.name}@${results.version}`\n        this.setState(\n          {\n            inputInitialValue: newPackageString,\n            results,\n          },\n          () => {\n            Router.replace(`/package/${newPackageString}`)\n          }\n        )\n      })\n      .catch(err => {\n        this.setState({\n          resultsError: err,\n          resultsPromiseState: 'rejected',\n        })\n        console.error(err)\n      })\n  }\n\n  fetchHistory = packageString => {\n    API.getHistory(packageString, 15)\n      .then(results => {\n        this.setState({\n          historicalResultsPromiseState: 'fulfilled',\n          historicalResults: results,\n        })\n      })\n      .catch(err => {\n        this.setState({ historicalResultsPromiseState: 'rejected' })\n        console.error(err)\n      })\n  }\n\n  handleSearchSubmit = packageString => {\n    this.setState({\n      results: {},\n      historicalResultsPromiseState: 'pending',\n      resultsPromiseState: 'pending',\n    })\n\n    const normalizedQuery = packageString.trim().toLowerCase()\n\n    Router.push(`/package/${normalizedQuery}`)\n\n    this.fetchResults(normalizedQuery)\n    this.fetchHistory(normalizedQuery)\n  }\n\n  handleProgressDone = () => {\n    this.setState({\n      resultsPromiseState: 'fulfilled',\n    })\n  }\n\n  formatHistoricalResults = () => {\n    const { results, historicalResults } = this.state\n    const totalVersions = {\n      ...historicalResults,\n      [results.version]: results,\n    }\n\n    const formattedResults = Object.keys(totalVersions).map(version => {\n      if (isEmptyObject(totalVersions[version])) {\n        return { version, disabled: true }\n      }\n      return {\n        version,\n        size: totalVersions[version].size,\n        gzip: totalVersions[version].gzip,\n      }\n    })\n    const sorted = formattedResults.sort((packageA, packageB) => {\n      const versionA = packageA.version.replace(/\\D/g, '')\n      const versionB = packageB.version.replace(/\\D/g, '')\n      return parseInt(versionA) > parseInt(versionB)\n    })\n    return typeof window !== 'undefined' && window.innerWidth < 640\n      ? sorted.slice(-10)\n      : sorted\n  }\n\n  handleBarClick = reading => {\n    const { results } = this.state\n\n    const packageString = `${results.name}@${reading.version}`\n    this.setState({ inputInitialValue: packageString })\n    this.handleSearchSubmit(packageString)\n  }\n\n  render() {\n    return (\n      <Layout className=\"compare-page\">\n        <div className=\"page-container\">\n          <header className=\"result-header\">\n            <section className=\"result-header--left-section\">\n              <Link href=\"/\">\n                <div className=\"logo-small\">\n                  <span>Bundle</span>\n                  <span className=\"logo-small__alt\">Phobia</span>\n                </div>\n              </Link>\n            </section>\n            <section className=\"result-header--right-section\">\n              <a\n                target=\"_blank\"\n                href=\"https://github.com/pastelsky/bundlephobia\"\n                rel=\"noreferrer\"\n              >\n                <GithubLogo />\n              </a>\n            </section>\n          </header>\n          <div className=\"compare__search-container\">\n            <div className=\"compare__search-inputs\">\n              <AutocompleteInput\n                key={''}\n                placeholder=\"package A\"\n                initialValue={''}\n                onSearchSubmit={this.handleSearchSubmit}\n                maxFullSizeCharsMultiplier={0.5}\n                hideSearchIcon\n              />\n              <div className=\"compare__vs\">vs</div>\n              <AutocompleteInput\n                key={'2'}\n                placeholder=\"package B\"\n                initialValue={''}\n                onSearchSubmit={this.handleSearchSubmit}\n                maxFullSizeCharsMultiplier={0.5}\n                hideSearchIcon\n              />\n            </div>\n          </div>\n        </div>\n      </Layout>\n    )\n  }\n}\n"
  },
  {
    "path": "pages/compare/ComparePage.scss",
    "content": "@import '../../stylesheets/base';\n@import '../../stylesheets/variables';\n\n.result-header {\n  padding: $global-spacing * 3;\n  padding-bottom: $global-spacing;\n  display: flex;\n  align-items: center;\n\n  @media screen and (max-width: 40em) {\n    padding: $global-spacing * 2;\n  }\n}\n\n.page-container {\n  min-height: 100vh;\n  display: flex;\n  flex-direction: column;\n  min-height: calc(100vh - 6px);\n  //border-top: 3px dashed #333;\n  //border-bottom: 3px dashed #333;\n  //border-width: 3px 0;\n}\n\n.result-header--right-section {\n  margin-left: auto;\n}\n\n.github-logo {\n  width: 30px;\n  height: 30px;\n\n  @media screen and (max-width: 40em) {\n    width: 20px;\n    height: 20px;\n  }\n\n  path {\n    fill: #666;\n    transition: fill 0.2s;\n  }\n\n  &:hover {\n    path {\n      fill: black;\n    }\n  }\n}\n\n.logo-small {\n  @include font-size-reg;\n  text-transform: uppercase;\n  font-weight: $font-weight-very-bold;\n  letter-spacing: 3px;\n  user-select: none;\n  cursor: pointer;\n  color: #212121;\n}\n\n.logo-small__alt {\n  color: #888;\n}\n\n.compare__search-container {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-grow: 1;\n\n  @media screen and (max-width: 40em) {\n    padding: 0 $global-spacing * 2;\n  }\n}\n\n.compare__search-inputs {\n  display: flex;\n  align-items: center;\n  margin-top: -15vh;\n  max-width: $global-spacing * 78;\n}\n\n.compare__vs {\n  @include font-size-xl;\n  //color: $raven;\n  font-weight: $font-weight-thin;\n  margin: 0 $global-spacing;\n}\n"
  },
  {
    "path": "pages/compare/index.js",
    "content": "import ComparePage from './ComparePage'\n\nexport default ComparePage\n"
  },
  {
    "path": "pages/index.page.tsx",
    "content": "import React from 'react'\nimport { useRouter } from 'next/router'\nimport Link from 'next/link'\n\nimport Analytics from '../client/analytics'\nimport { AutocompleteInput } from '../client/components/AutocompleteInput'\nimport AutocompleteInputBox from '../client/components/AutocompleteInputBox/AutocompleteInputBox'\nimport Layout from '../client/components/Layout'\nimport MetaTags from '../client/components/MetaTags'\nimport PageNav from '../client/components/PageNav'\nimport cx from 'classnames'\n\nimport { Press_Start_2P } from '@next/font/google'\n\nconst pressStart2P = Press_Start_2P({ weight: '400', subsets: ['latin'] })\n\nconst Logo = () => (\n  <svg\n    className=\"logo-graphic\"\n    width=\"137\"\n    height=\"157\"\n    viewBox=\"0 0 137 157\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g stroke=\"#000\" strokeWidth=\"1.5\" fill=\"none\" fillRule=\"evenodd\">\n      <g transform=\"translate(37.21 45.73)\">\n        <rect\n          fill=\"#C0C0C0\"\n          x=\"25.1\"\n          y=\"56.58\"\n          width=\"16.74\"\n          height=\"15.94\"\n          rx=\"7.97\"\n        />\n        <rect x=\"25.1\" y=\"40.64\" width=\"16.74\" height=\"31.88\" rx=\"8.37\" />\n        <ellipse cx=\"7.13\" cy=\"8.49\" rx=\"7.13\" ry=\"8.45\" />\n        <ellipse cx=\"56.54\" cy=\"8.49\" rx=\"7.13\" ry=\"8.45\" />\n      </g>\n      <g\n        className=\"logo__skeleton-group\"\n        opacity=\".15\"\n        transform=\"translate(104.153 25.807)\"\n      >\n        <circle className=\"logo__skeleton\" cx=\"23.51\" cy=\"4.78\" r=\"4.78\" />\n        <circle className=\"logo__skeleton\" cx=\"6.18\" cy=\"87.47\" r=\"5.92\" />\n        <path\n          className=\"logo__skeleton\"\n          d=\"M18.3 4.7l9.55.16m3.52 41.16L15 45.54m1.22-7.7L31.7 45.2\"\n        />\n      </g>\n      <path d=\"M114.1 117.84c1.2-1.02 1.74-1.96 2.48-3.56l19.3-42.92c-2.02-27.1-3.44-40.7-3.44-40.77 0-2.7-2.14-4.8-4.78-4.8-2.6 0-4.73 2.1-4.78 4.7l-3.05 37.7-14.76 42.1c-.44.8-.7 1.8-.7 2.8 0 .83.2 1.64.5 2.4l10.43 40.12 11.55-3.1-12.74-34.8z\" />\n      <path\n        className=\"logo__skeleton\"\n        d=\"M104.97 112.06l10.7 2.98\"\n        opacity=\".15\"\n      />\n      <g\n        className=\"logo__skeleton-group\"\n        opacity=\".15\"\n        transform=\"matrix(-1 0 0 1 33.225 25.807)\"\n      >\n        <circle className=\"logo__skeleton\" cx=\"23.51\" cy=\"4.78\" r=\"4.78\" />\n        <circle className=\"logo__skeleton\" cx=\"6.18\" cy=\"87.47\" r=\"5.92\" />\n        <path\n          className=\"logo__skeleton\"\n          d=\"M18.3 4.7l9.55.16m3.52 41.16L15 45.54m1.22-7.7L31.7 45.2\"\n        />\n      </g>\n      <path d=\"M23.27 117.84c-1.2-1.02-1.73-1.96-2.47-3.56L1.5 71.36c2.02-27.1 3.43-40.7 3.43-40.77 0-2.7 2.14-4.8 4.8-4.8 2.6 0 4.72 2.1 4.77 4.7l3.05 37.7 14.75 42.2c.45.8.7 1.8.7 2.8 0 .8-.18 1.6-.5 2.4l-10.4 40.1-11.55-3.1 12.74-34.8z\" />\n      <path\n        className=\"logo__skeleton\"\n        d=\"M32.4 112.06l-10.7 2.98\"\n        opacity=\".15\"\n      />\n      <path d=\"M94.26 91.23c12.2-7.54 20.25-20.38 20.25-34.94 0-3.9-.5-7.6-1.5-11.1C112.8 21 93.2 1.5 68.98 1.5S25 21.02 24.87 45.2c-1.05 3.52-1.6 7.23-1.6 11.05 0 16.54 10.43 30.9 25.6 37.72-.1 1.4-.1 2.82-.1 4.26 0 23.22 10.22 42.04 22.9 42.04 12.65 0 22.92-18.8 22.92-42.03 0-2.4-.2-4.8-.4-7.1z\" />\n      <g\n        className=\"logo__skeleton-group\"\n        opacity=\".15\"\n        transform=\"translate(23.263 1.5)\"\n      >\n        <circle className=\"logo__skeleton\" cx=\"45.63\" cy=\"44.03\" r=\"44.03\" />\n        <ellipse\n          className=\"logo__skeleton\"\n          cx=\"45.63\"\n          cy=\"54.79\"\n          rx=\"45.62\"\n          ry=\"42.04\"\n        />\n        <ellipse\n          className=\"logo__skeleton\"\n          cx=\"48.39\"\n          cy=\"96.83\"\n          rx=\"22.93\"\n          ry=\"42.04\"\n        />\n      </g>\n    </g>\n  </svg>\n)\n\nconst Home = () => {\n  const router = useRouter()\n\n  React.useEffect(() => {\n    Analytics.pageView('home')\n  }, [])\n\n  const handleSearchSubmit = (value: string) => {\n    Analytics.performedSearch(value.trim())\n    if (value ?? '') {\n      router.push(`/package/${value.trim()}`)\n    }\n  }\n\n  return (\n    <Layout className=\"homepage\">\n      <MetaTags\n        title=\"Bundlephobia | Size of npm dependencies\"\n        canonicalPath=\"\"\n      />\n      <div className=\"homepage__container\">\n        <PageNav minimal={true} />\n        <div className=\"homepage__content\">\n          <Logo />\n          <div className=\"logo\">\n            <span>Bundle</span>\n            <span className=\"logo__alt\">Phobia</span>\n          </div>\n          <h1 className=\"homepage__tagline\">\n            find the cost of adding a npm package to your bundle\n          </h1>\n          <AutocompleteInputBox className=\"homepage__search-input\">\n            <AutocompleteInput\n              containerClass=\"homepage__search-input-container\"\n              onSearchSubmit={handleSearchSubmit}\n              autoFocus={true}\n            />\n          </AutocompleteInputBox>\n          <div className=\"homepage__or-divider\">or</div>\n          <div className=\"homepage__scan-link\">\n            <Link href=\"/scan\">\n              <span>\n                Scan a <code>package.json</code> file\n              </span>\n              &nbsp;\n              <sup>beta</sup>\n            </Link>\n          </div>\n        </div>\n      </div>\n    </Layout>\n  )\n}\n\nexport default Home\n"
  },
  {
    "path": "pages/index.scss",
    "content": "@import '../stylesheets/base';\n@import '../stylesheets/variables';\n\n.homepage {\n  height: 100vh;\n  height: calc(100vh - 4px);\n  padding: 0 $global-spacing * 2;\n}\n\n.homepage__content {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  max-width: 100%;\n  height: calc(100vh - 90px);\n}\n\n.homepage__tagline {\n  @include font-size-md;\n  font-family: $font-family-body;\n  font-weight: $font-weight-hairline;\n  color: #777;\n  margin-top: $global-spacing;\n  text-align: center;\n  line-height: 1.4;\n  letter-spacing: 1px;\n}\n\n.logo {\n  text-transform: uppercase;\n  font-weight: $font-weight-bold;\n  letter-spacing: 4px;\n  //@include font-size-md-2; // Enable after removing chirstmas\n  @include font-size-md;\n  user-select: none;\n  margin-top: $global-spacing * 2;\n}\n\n.logo__alt {\n  color: #888;\n}\n\n.logo__skeleton {\n  animation: move 2s alternate infinite;\n\n  .logo-graphic:hover & {\n    stroke: desaturate($pastel-green, 30%);\n  }\n}\n\n.logo-graphic {\n  width: 137px * 0.9;\n  height: 157px * 0.9;\n\n  @media screen and (max-width: 40em) {\n    width: 137px * 0.8;\n    height: 157px * 0.8;\n  }\n\n  &:hover {\n    .logo__skeleton-group {\n      opacity: 0.4;\n    }\n  }\n}\n\n.homepage__search-input-container {\n  width: 100%;\n}\n\n.homepage__search-input {\n  margin-top: $global-spacing;\n  width: 100%;\n\n  @media screen and (max-width: 40em) {\n    margin-top: 5vh;\n    margin-bottom: 5vh;\n  }\n\n  .autocomplete-input {\n    text-align: center;\n  }\n  .autocomplete-input__dummy-input {\n    display: flex;\n    justify-content: center;\n  }\n}\n\n.homepage__or-divider {\n  font-weight: $font-weight-bold;\n  text-transform: uppercase;\n  color: $raven;\n  margin-top: 4vh;\n  letter-spacing: 3px;\n}\n\n.homepage__scan-link {\n  margin-top: 4vh;\n  margin-bottom: 14vh;\n  letter-spacing: 0.5px;\n\n  a {\n    color: inherit;\n\n    span {\n      border-bottom: 1px dashed $raven;\n    }\n\n    sup {\n      color: $cornflower-blue;\n    }\n  }\n\n  @media screen and (max-width: 40em) {\n    margin-bottom: 20vh;\n  }\n}\n\n@for $i from 1 through 3 {\n  .logo__skeleton:nth-of-type(#{$i}) {\n    animation-delay: 0.2 * $i * 1s;\n  }\n}\n\n@keyframes move {\n  0% {\n    transform: translate(1px, 0.5px);\n  }\n\n  20% {\n    transform: translate(0px, -1px);\n  }\n\n  75% {\n    transform: translate(-1px, 1px);\n  }\n\n  100% {\n    transform: translate(0.55px, -1px);\n  }\n}\n"
  },
  {
    "path": "pages/package/[...packageString]/ResultPage.js",
    "content": "import React, { PureComponent } from 'react'\nimport Analytics from '../../../client/analytics'\n\nimport ResultLayout from '../../../client/components/ResultLayout'\nimport BarGraph from '../../../client/components/BarGraph'\nimport { AutocompleteInput } from '../../../client/components/AutocompleteInput'\nimport AutocompleteInputBox from '../../../client/components/AutocompleteInputBox'\nimport BuildProgressIndicator from '../../../client/components/BuildProgressIndicator'\nimport Router, { withRouter } from 'next/router'\nimport semver from 'semver'\nimport isEmptyObject from 'is-empty-object'\nimport { parsePackageString } from '../../../utils/common.utils'\nimport {\n  getTimeFromSize,\n  DownloadSpeed,\n  resolveBuildError,\n  formatSize,\n} from '../../../utils'\nimport Stat from '../../../client/components/Stat'\n\nimport API from '../../../client/api'\nimport MetaTags, {\n  DEFAULT_DESCRIPTION_START,\n} from '../../../client/components/MetaTags'\nimport InterLinksSection from './components/InterLinksSection'\n\nimport TreemapSection from './components/TreemapSection'\nimport EmptyBox from '../../../client/assets/empty-box.svg'\nimport SimilarPackagesSection from './components/SimilarPackagesSection'\nimport ExportAnalysisSection from './components/ExportAnalysisSection'\nimport QuickStatsBar from '../../../client/components/QuickStatsBar/QuickStatsBar'\n\nimport Warning from '../../../client/components/Warning/Warning'\nimport arrayToSentence from 'array-to-sentence'\n\nclass ResultPage extends PureComponent {\n  state = {\n    results: {},\n    resultsPromiseState: null,\n    resultsError: null,\n    historicalResultsPromiseState: null,\n    inputInitialValue: this.getPackageString(this.props.router) || '',\n    historicalResults: [],\n    similarPackages: [],\n    similarPackagesCategory: '',\n  }\n\n  getPackageString(router) {\n    return router.query.packageString.join('/')\n  }\n\n  componentDidMount() {\n    Analytics.pageView('package result')\n\n    const packageString = this.getPackageString(this.props.router)\n    if (packageString) {\n      this.handleSearchSubmit(packageString)\n    }\n  }\n\n  componentDidUpdate(prevProps) {\n    const packageString = this.getPackageString(prevProps.router)\n    const nextPackageString = this.getPackageString(this.props.router)\n\n    if (!nextPackageString) {\n      return\n    }\n\n    const currentPackage = parsePackageString(packageString)\n    const nextPackage = parsePackageString(nextPackageString)\n\n    const isPackageDifferent =\n      currentPackage.name !== nextPackage.name ||\n      currentPackage.version !== nextPackage.version\n\n    if (isPackageDifferent) {\n      this.handleSearchSubmit(nextPackageString)\n    }\n  }\n\n  fetchResults = packageString => {\n    const startTime = Date.now()\n\n    API.getInfo(packageString)\n      .then(results => {\n        this.fetchSimilarPackages(packageString)\n\n        if (this.activeQuery !== packageString) return\n\n        const newPackageString = `${results.name}@${results.version}`\n        this.setState(\n          {\n            inputInitialValue: newPackageString,\n            results,\n          },\n          () => {\n            Router.replace(`/package/${newPackageString}`)\n          }\n        )\n\n        Analytics.searchSuccess({\n          packageName: packageString,\n          timeTaken: Date.now() - startTime,\n        })\n      })\n      .catch(err => {\n        this.setState({\n          resultsError: err,\n          resultsPromiseState: 'rejected',\n        })\n        console.error(err)\n\n        Analytics.searchFailure({\n          packageName: packageString,\n          timeTaken: Date.now() - startTime,\n        })\n      })\n  }\n\n  fetchHistory = packageString => {\n    API.getHistory(packageString, 15)\n      .then(results => {\n        if (this.activeQuery !== packageString) return\n\n        this.setState({\n          historicalResultsPromiseState: 'fulfilled',\n          historicalResults: results,\n        })\n      })\n      .catch(err => {\n        this.setState({ historicalResultsPromiseState: 'rejected' })\n        console.error('Fetching history failed:', err)\n      })\n  }\n\n  fetchSimilarPackages = packageString => {\n    const { name } = parsePackageString(packageString)\n    const promises = []\n\n    API.getSimilar(name)\n      .then(result => {\n        if (result.category.label) {\n          if (result.category.score < 12) return\n\n          result.category.similar.forEach(packageName => {\n            promises.push(API.getInfo(packageName))\n          })\n\n          Promise.allSettled(promises).then(results => {\n            if (this.activeQuery !== packageString) return\n\n            this.setState({\n              similarPackagesCategory: result.category.label,\n              similarPackages: results\n                .filter(result => result.status === 'fulfilled')\n                .map(result => result.value),\n            })\n          })\n        }\n      })\n      .catch(err => {\n        this.setState({ historicalResultsPromiseState: 'rejected' })\n        console.error(err)\n      })\n  }\n\n  handleSearchSubmit = packageString => {\n    Analytics.performedSearch(packageString)\n    const normalizedQuery = packageString.trim()\n\n    this.setState(\n      {\n        results: {},\n        historicalResultsPromiseState: 'pending',\n        resultsPromiseState: 'pending',\n        inputInitialValue: normalizedQuery,\n        similarPackages: [],\n        historicalResults: [],\n      },\n      () => {\n        Router.push(`/package/${normalizedQuery}`)\n        Analytics.pageView('package result')\n        this.activeQuery = normalizedQuery\n        this.fetchResults(normalizedQuery)\n        this.fetchHistory(normalizedQuery)\n      }\n    )\n  }\n\n  handleProgressDone = () => {\n    this.setState({\n      resultsPromiseState: 'fulfilled',\n    })\n  }\n\n  formatHistoricalResults = () => {\n    const { results, historicalResults } = this.state\n    const totalVersions = {\n      ...historicalResults,\n      [results.version]: results,\n    }\n\n    const formattedResults = Object.keys(totalVersions).map(version => {\n      if (isEmptyObject(totalVersions[version])) {\n        return { version, disabled: true }\n      }\n      return {\n        version,\n        size: totalVersions[version].size,\n        gzip: totalVersions[version].gzip,\n        hasSideEffects: totalVersions[version].hasSideEffects,\n        hasJSModule: totalVersions[version].hasJSModule,\n        hasJSNext: totalVersions[version].hasJSNext,\n        isModuleType: totalVersions[version].isModuleType,\n      }\n    })\n    const sorted = formattedResults.sort((packageA, packageB) =>\n      semver.compare(packageA.version, packageB.version)\n    )\n    return typeof window !== 'undefined' && window.innerWidth < 640\n      ? sorted.slice(-10)\n      : sorted\n  }\n\n  handleBarClick = reading => {\n    const { results } = this.state\n\n    const packageString = `${results.name}@${reading.version}`\n    this.setState({ inputInitialValue: packageString })\n    this.handleSearchSubmit(packageString)\n\n    Analytics.graphBarClicked({\n      packageName: packageString,\n      idDisabled: reading.disabled,\n    })\n  }\n\n  getMetaTags = () => {\n    const { router } = this.props\n    const { resultsPromiseState, results } = this.state\n    let name, version, formattedSizeText, formattedGZIPSizeText\n\n    if (resultsPromiseState === 'fulfilled') {\n      name = results.name\n      version = results.version\n      const formattedSize = formatSize(results.size)\n      const formattedGZIPSize = formatSize(results.gzip)\n      formattedSizeText = `${formattedSize.size.toFixed(1)} ${\n        formattedSize.unit\n      }`\n      formattedGZIPSizeText = `${formattedGZIPSize.size.toFixed(1)} ${\n        formattedGZIPSize.unit\n      }`\n    } else {\n      name = parsePackageString(this.getPackageString(router)).name\n      version = parsePackageString(this.getPackageString(router)).version\n      formattedSizeText = ''\n      formattedGZIPSizeText = ''\n    }\n\n    const origin =\n      typeof window === 'undefined'\n        ? 'https://bundlephobia.com'\n        : window.location.origin\n\n    const title = version ? `${name} v${version}` : name\n    const description =\n      resultsPromiseState === 'fulfilled'\n        ? `Size of ${title} is ${formattedSizeText} (minified), and ${formattedGZIPSizeText} when compressed using GZIP. ${DEFAULT_DESCRIPTION_START}`\n        : `Find the size of javascript package ${title}. ${DEFAULT_DESCRIPTION_START}`\n\n    return (\n      <MetaTags\n        title={`${title} ❘ Bundlephobia`}\n        image={\n          origin + `/api/stats-image?name=${name}&version=${version}&wide=true`\n        }\n        description={description}\n        twitterDescription=\"Insights into npm packages\"\n        canonicalPath={`/package/${name}`}\n        isLargeImage={true}\n      />\n    )\n  }\n\n  render() {\n    const {\n      inputInitialValue,\n      resultsPromiseState,\n      resultsError,\n      historicalResultsPromiseState,\n      results,\n      similarPackages,\n      similarPackagesCategory,\n    } = this.state\n\n    const { errorName, errorBody, errorDetails } =\n      resolveBuildError(resultsError)\n\n    const referenceSpeedInfoText = (speed, units) =>\n      `Download Speed: ⬇️ ${speed} ${units}.\\nExclusive of HTTP request latency.`\n\n    const getQuickStatsBar = () =>\n      resultsPromiseState === 'fulfilled' && (\n        <QuickStatsBar\n          description={results.description}\n          dependencyCount={results.dependencyCount}\n          hasSideEffects={results.hasSideEffects}\n          isTreeShakeable={\n            results.hasJSModule || results.hasJSNext || results.isModuleType\n          }\n          repository={results.repository}\n          name={results.name}\n        />\n      )\n\n    return (\n      <ResultLayout>\n        {this.getMetaTags()}\n        <section className=\"content-container-wrap\">\n          <div className=\"content-container\">\n            <AutocompleteInputBox footer={getQuickStatsBar()}>\n              <AutocompleteInput\n                key={inputInitialValue}\n                initialValue={inputInitialValue}\n                className=\"result-page__search-input\"\n                onSearchSubmit={this.handleSearchSubmit}\n                renderAsH1={true}\n              />\n            </AutocompleteInputBox>\n            {resultsPromiseState === 'pending' && (\n              <div className=\"result-pending\">\n                <BuildProgressIndicator\n                  isDone={!!results.version}\n                  onDone={this.handleProgressDone}\n                />\n              </div>\n            )}\n            {resultsPromiseState === 'fulfilled' &&\n              results.ignoredMissingDependencies &&\n              results.ignoredMissingDependencies.length && (\n                <Warning>\n                  Ignoring the size of missing{' '}\n                  {results.ignoredMissingDependencies.length > 1\n                    ? 'dependencies'\n                    : 'dependency'}{' '}\n                  &nbsp;\n                  <code>\n                    {arrayToSentence(results.ignoredMissingDependencies)}\n                  </code>\n                  .\n                  <a\n                    href=\"https://github.com/pastelsky/bundlephobia#1-why-does-search-for-package-x-throw-missingdependencyerror-\"\n                    target=\"_blank\"\n                    rel=\"noreferrer\"\n                  >\n                    Read more\n                  </a>\n                </Warning>\n              )}\n            {resultsPromiseState === 'fulfilled' && (\n              <div className=\"content-split-container\">\n                <div className=\"stats-container\">\n                  <div className=\"size-container\">\n                    <h3> Bundle Size </h3>\n                    <div className=\"size-stats\">\n                      <Stat\n                        value={results.size}\n                        type={Stat.type.SIZE}\n                        label=\"Minified\"\n                      />\n                      <Stat\n                        value={results.gzip}\n                        type={Stat.type.SIZE}\n                        label=\"Minified + Gzipped\"\n                      />\n                    </div>\n                  </div>\n                  <div className=\"time-container\">\n                    <h3> Download Time </h3>\n                    <div className=\"time-stats\">\n                      <Stat\n                        value={getTimeFromSize(results.gzip).threeG}\n                        type={Stat.type.TIME}\n                        label=\"Slow 3G\"\n                        infoText={referenceSpeedInfoText(\n                          DownloadSpeed.THREE_G,\n                          'kB/s'\n                        )}\n                      />\n                      <Stat\n                        value={getTimeFromSize(results.gzip).fourG}\n                        type={Stat.type.TIME}\n                        label=\"Emerging 4G\"\n                        infoText={referenceSpeedInfoText(\n                          DownloadSpeed.FOUR_G,\n                          'kB/s'\n                        )}\n                      />\n                    </div>\n                  </div>\n                </div>\n                <div className=\"chart-container\">\n                  {historicalResultsPromiseState === 'fulfilled' && (\n                    <BarGraph\n                      onBarClick={this.handleBarClick}\n                      readings={this.formatHistoricalResults()}\n                    />\n                  )}\n                </div>\n              </div>\n            )}\n          </div>\n\n          {resultsPromiseState === 'rejected' && (\n            <div className=\"result-error\">\n              <EmptyBox className=\"result-error__img\" />\n              <h2 className=\"result-error__code\">{errorName}</h2>\n              <p\n                className=\"result-error__message\"\n                dangerouslySetInnerHTML={{ __html: errorBody }}\n              />\n              {errorDetails && (\n                <details className=\"result-error__details\">\n                  <summary> Stacktrace</summary>\n                  <pre>{errorDetails}</pre>\n                </details>\n              )}\n            </div>\n          )}\n          {resultsPromiseState === 'fulfilled' &&\n            results.dependencySizes &&\n            results.dependencySizes.length && (\n              <div className=\"content-container\">\n                <TreemapSection\n                  packageName={results.name}\n                  packageSize={results.size}\n                  dependencySizes={results.dependencySizes}\n                />\n              </div>\n            )}\n\n          {resultsPromiseState === 'fulfilled' && (\n            <div className=\"content-container\">\n              <ExportAnalysisSection result={results} />\n            </div>\n          )}\n\n          {resultsPromiseState === 'fulfilled' && similarPackages.length > 0 && (\n            <div className=\"content-container\">\n              <SimilarPackagesSection\n                category={similarPackagesCategory}\n                packs={similarPackages}\n                comparisonGzip={results.gzip}\n              />\n            </div>\n          )}\n\n          {resultsPromiseState === 'fulfilled' &&\n            parsePackageString(results.name).scoped && (\n              <InterLinksSection packageName={results.name} />\n            )}\n        </section>\n      </ResultLayout>\n    )\n  }\n}\n\nexport const getServerSideProps = () => {\n  return { props: {} }\n}\n\nexport default withRouter(ResultPage)\n"
  },
  {
    "path": "pages/package/[...packageString]/ResultPage.scss",
    "content": "@use \"sass:math\";\n\n@import '../../../stylesheets/colors';\n@import '../../../stylesheets/variables';\n\n.result-page__search-input {\n  width: 100%;\n}\n\n.flash-message {\n  @include font-size-sm;\n  padding: math.div($global-spacing, 3) $global-spacing * 2;\n  text-align: center;\n  color: darken($pastel-green, 20%);\n  align-self: center;\n  margin-top: $global-spacing;\n  position: relative;\n  display: flex;\n  align-items: center;\n\n  code {\n    color: darken($pastel-green, 25%);\n    font-size: 95%;\n  }\n\n  a {\n    text-decoration: underline;\n    color: inherit;\n  }\n}\n\n.flash-message__icon {\n  margin-left: $global-spacing * 0.5;\n  margin-right: $global-spacing;\n  height: $global-spacing * 2.5;\n  width: auto;\n}\n\n.result-error {\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-direction: column;\n  flex-grow: 1;\n}\n\n.result-error__details {\n  font-family: $font-family-code;\n  text-transform: none;\n\n  summary {\n    text-align: center;\n    color: lighten($raven, 10%);\n\n    &:focus {\n      outline: none;\n    }\n  }\n\n  pre {\n    max-width: 600px;\n    @include font-size-sm;\n    font-weight: $font-weight-light;\n    text-align: left;\n    color: $raven;\n    line-height: 1.5;\n    background: transparentize($raven, 0.94);\n    padding: $global-spacing $global-spacing * 2;\n    white-space: normal;\n    border-radius: 10px;\n  }\n}\n\n.result-error__img {\n  width: 195px * 0.8;\n  height: 181px * 0.8;\n  opacity: 0.2;\n}\n\n.result-error__code {\n  font-family: $font-family-code;\n  margin-top: $global-spacing * 3;\n  margin-bottom: 0;\n}\n\n.result-error__message {\n  margin-top: $global-spacing;\n  font-weight: $font-weight-light;\n  color: $raven;\n  max-width: $global-spacing * 60;\n  text-align: center;\n  line-height: 1.5;\n}\n\n.page-container {\n  min-height: 100vh;\n  display: flex;\n  flex-direction: column;\n  min-height: calc(100vh - 6px);\n}\n\n.content-container {\n  display: flex;\n  width: 100%;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  margin: 4vh 0;\n\n  &:first-of-type {\n    margin-top: 0;\n  }\n}\n\n.content-split-container {\n  display: flex;\n  justify-content: space-around;\n  width: 100%;\n  margin-top: 10vh;\n\n  @media screen and (max-width: 48em) {\n    flex-direction: column;\n    margin-top: 5vh;\n  }\n\n  @media screen and (max-width: 40em) {\n    padding: 0 $global-spacing * 2;\n  }\n}\n\n.content-container-wrap {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  padding: 0 10%;\n  flex-grow: 1;\n  animation: fade-in-full 0.2s;\n}\n\n.result-pending,\n.result-error {\n  min-height: 75vh;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.chart-container {\n  width: 400px;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  flex: 1 1 0;\n\n  @media screen and (max-width: 48em) {\n    margin: 5vh 0;\n    align-items: center;\n    flex: 1;\n    width: 100%;\n  }\n}\n\n.stats-container {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-around;\n  margin: auto 0;\n  flex: 1 1 0;\n\n  @media screen and (max-width: 48em) {\n    flex: 1;\n  }\n}\n\n.time-container {\n  border-top: 1px solid lighten($raven, 50%);\n  padding-top: 5vh;\n\n  @media screen and (max-width: 48em) {\n    padding-top: 3vh;\n  }\n}\n\n.size-container {\n  margin-bottom: 5vh;\n\n  @media screen and (max-width: 48em) {\n    margin-bottom: 3vh;\n  }\n}\n\n.size-container,\n.time-container {\n  display: flex;\n  align-items: center;\n  flex-direction: column;\n}\n\n.size-container h3,\n.time-container h3 {\n  @include font-size-md;\n  text-transform: uppercase;\n  letter-spacing: 2px;\n  font-weight: $font-weight-thin;\n  margin: 0 0 $global-spacing * 2;\n  color: lighten($raven, 10%);\n\n  @media screen and (max-width: 48em) {\n    margin: 0 0 $global-spacing;\n  }\n}\n\n.size-stats,\n.time-stats {\n  display: flex;\n}\n\n.ct-series-a .ct-bar {\n  stroke: #00b4ae;\n  stroke-width: 40px;\n}\n\n@keyframes fade-in-full {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n.result__section-heading {\n  @include font-size-md-2;\n  text-align: center;\n  position: relative;\n  margin-top: 0;\n}\n\n.result__section-heading--new {\n  &::after {\n    @include font-size-xs;\n    content: 'NEW';\n    font-family: $font-family-code;\n    padding: $global-spacing * 0.2 math.div($global-spacing, 1.5);\n    position: absolute;\n    top: 0;\n    margin-left: $global-spacing;\n    background: #ffbc40;\n    border-radius: 2px;\n    line-height: 1.2;\n  }\n}\n\n// Treemap section\n\n.treemap__section {\n  width: 100%;\n}\n\n.treemap {\n  @include font-size-sm;\n  color: rgba(0, 0, 0, 0.6);\n}\n\n.treemap__square {\n  transition: all 0.3s;\n  position: relative;\n\n  &:hover {\n    //transform:scale(1.05);\n    z-index: 1;\n    color: rgba(0, 0, 0, 0.8);\n    //box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.12);\n    filter: brightness(105%);\n  }\n}\n\n.treemap__content {\n  max-height: 100%;\n  max-width: 100%;\n}\n.treemap__label {\n  font-family: $font-family-code;\n  max-height: 75%;\n  max-width: 100%;\n  overflow: hidden;\n  a {\n    color: inherit;\n\n    &:hover {\n      text-decoration: underline;\n\n      &::after {\n        position: absolute;\n        content: '↗';\n      }\n    }\n  }\n}\n\n.treemap__percent {\n  @include font-size-xl;\n  display: block;\n  font-weight: $font-weight-bold;\n  letter-spacing: -1px;\n  color: rgba(0, 0, 0, 0.5);\n\n  .treemap__square:hover & {\n    color: rgba(0, 0, 0, 0.8);\n  }\n\n  //color: black;\n  mix-blend-mode: soft-light;\n\n  // Disabling this for mobiles and tablets\n  // since text seems to glitch due to it\n  @media screen and (max-width: 48em) {\n    mix-blend-mode: normal;\n  }\n}\n\n.treemap__percent-sign {\n  font-size: 50%;\n  padding-left: 2px;\n}\n\n.treemap__ellipsis {\n  color: rgba(0, 0, 0, 0.35);\n}\n\n.treemap__note {\n  @include font-size-xs;\n  color: $raven;\n  margin: $global-spacing * 3 0 0 0;\n  line-height: 1.2;\n}\n"
  },
  {
    "path": "pages/package/[...packageString]/components/ExportAnalysisSection/ExportAnalysisSection.js",
    "content": "import React, { Component } from 'react'\nimport Analytics from '../../../../../client/analytics'\nimport cx from 'classnames'\nimport API from '../../../../../client/api'\nimport SearchIcon from '../../../../../client/components/Icons/SearchIcon'\nimport JumpingDots from '../../../../../client/components/JumpingDots'\nimport { formatSize, resolveBuildError } from '../../../../../utils'\n\nconst State = {\n  TBD: 'tbd',\n  IN_PROGRESS: 'in-progress',\n  EXPORTS_FULFILLED: 'exports-fulfilled',\n  SIZES_FULFILLED: 'sizes-fulfilled',\n  REJECTED: 'rejected',\n}\n\nfunction getBGClass(ratio) {\n  if (ratio < 0.05) {\n    return 'low-1'\n  } else if (ratio < 0.15) {\n    return 'low-2'\n  } else if (ratio < 0.25) {\n    return 'med-1'\n  } else if (ratio < 0.4) {\n    return 'med-2'\n  } else if (ratio < 0.5) {\n    return 'med-3'\n  } else if (ratio < 0.7) {\n    return 'high-1'\n  } else {\n    return 'high-2'\n  }\n}\n\nclass ExportPill extends React.Component {\n  render() {\n    const { name, size, totalSize, isLoading } = this.props\n    return (\n      <li className=\"export-analysis-section__pill export-analysis-section__dont-break\">\n        <div\n          className={cx(\n            'export-analysis-section__pill-fill',\n            `export-analysis-section__pill-fill--${getBGClass(\n              size / totalSize\n            )}`\n          )}\n          style={{\n            transform: `scaleX(${Math.min((size || 0) / totalSize, 1)})`,\n          }}\n        />\n        <div className=\"export-analysis-section__pill-name\"> {name} </div>\n        {isLoading && <div className=\"export-analysis-section__pill-spinner\" />}\n        {size && (\n          <div className=\"export-analysis-section__pill-size\">\n            {formatSize(size).size.toFixed(1)}\n            <span className=\"export-analysis-section__pill-size-unit\">\n              {formatSize(size).unit}\n            </span>\n          </div>\n        )}\n      </li>\n    )\n  }\n}\n\nfunction ExportList({ exports, totalSize, isLoading }) {\n  const shouldShowLabels = exports.length > 20\n  const exportDictionary = {}\n  let curIndex = 0\n\n  exports.forEach(exp => {\n    const firstLetter = exp.name[0].toLowerCase()\n    if (exportDictionary[firstLetter]) {\n      exportDictionary[firstLetter].push(exp)\n    } else {\n      exportDictionary[firstLetter] = [exp]\n    }\n  })\n\n  return (\n    <ul className=\"export-analysis-section__list\">\n      {Object.keys(exportDictionary)\n        .sort()\n        .map(letter => (\n          <div className=\"export-analysis-section__letter-group\" key={letter}>\n            {shouldShowLabels && (\n              <div className=\"export-analysis-section__dont-break\">\n                <h3 className=\"export-analysis-section__letter-heading\">\n                  {letter}\n                </h3>\n                <ExportPill\n                  size={exportDictionary[letter][0].gzip}\n                  totalSize={totalSize}\n                  name={exportDictionary[letter][0].name}\n                  path={exportDictionary[letter][0].path}\n                  key={exportDictionary[letter][0].name}\n                  isLoading={curIndex++ < 40 && isLoading}\n                />\n              </div>\n            )}\n            {exportDictionary[letter]\n              .slice(shouldShowLabels ? 1 : 0)\n              .map(exp => (\n                <ExportPill\n                  size={exp.gzip}\n                  totalSize={totalSize}\n                  name={exp.name}\n                  path={exp.path}\n                  key={exp.name}\n                  isLoading={curIndex++ < 40 && isLoading}\n                />\n              ))}\n          </div>\n        ))}\n      <div className=\"export-analysis-section__overflow-indicator\" />\n    </ul>\n  )\n}\n\nfunction InputExportFilter({ onChange }) {\n  return (\n    <div className=\"export-analysis-section__filter-input-container\">\n      <input\n        placeholder=\"Filter methods\"\n        className=\"export-analysis-section__filter-input\"\n        type=\"text\"\n        onChange={e => onChange(e.target.value.toLowerCase().trim())}\n      />\n      <SearchIcon className=\"export-analysis-section__filter-input-search-icon\" />\n    </div>\n  )\n}\n\nclass ExportAnalysisSection extends Component {\n  state = {\n    analysisState: State.TBD,\n    exports: {},\n    assets: [],\n    filterText: '',\n    resultError: {},\n  }\n\n  componentDidMount() {\n    const isCompatible = !this.getIncompatibleMessage()\n\n    if (isCompatible) {\n      this.startAnalysis()\n    }\n  }\n\n  startAnalysis = () => {\n    const { result } = this.props\n    const { name, version } = result\n    const packageString = `${name}@${version}`\n    const startTime = Date.now()\n    let sizeStartTime\n\n    this.setState({ analysisState: State.IN_PROGRESS })\n\n    Analytics.performedExportsAnalysis(packageString)\n\n    API.getExports(packageString)\n      .then(\n        results => {\n          this.setState({\n            exports: results.exports,\n            analysisState: State.EXPORTS_FULFILLED,\n          })\n\n          Analytics.exportsAnalysisSuccess({\n            packageName: packageString,\n            timeTaken: Date.now() - startTime,\n          })\n        },\n        err => {\n          Analytics.exportsAnalysisFailure({\n            packageName: packageString,\n            timeTaken: Date.now() - startTime,\n          })\n          return Promise.reject(err)\n        }\n      )\n      .then(() => {\n        sizeStartTime = Date.now()\n        return API.getExportsSizes(packageString)\n      })\n      .then(\n        results => {\n          this.setState({\n            analysisState: State.SIZES_FULFILLED,\n            assets: results.assets\n              .filter(asset => asset.type === 'js')\n              .map(asset => ({\n                ...asset,\n                path: this.state.exports[asset.name],\n              })),\n          })\n\n          Analytics.exportsSizesSuccess({\n            packageName: packageString,\n            timeTaken: Date.now() - sizeStartTime,\n          })\n        },\n        err => {\n          Analytics.exportsSizesFailure({\n            packageName: packageString,\n            timeTaken: Date.now() - sizeStartTime,\n          })\n          return Promise.reject(err)\n        }\n      )\n      .catch(err => {\n        this.setState({ analysisState: State.REJECTED, resultError: err })\n        console.error('Export analysis failed due to ', err)\n      })\n  }\n\n  handleFilterInputChange = value => {\n    this.setState({ filterText: value })\n  }\n\n  renderProgress() {\n    const { result } = this.props\n    return (\n      <div className=\"export-analysis-section__progress-container\">\n        Fetching all named exports in&nbsp;<code>{result.name}</code>{' '}\n        <JumpingDots />\n      </div>\n    )\n  }\n\n  getIncompatibleMessage() {\n    const { result } = this.props\n    let incompatibleMessage = ''\n\n    if (!(result.hasJSModule || result.hasJSNext || result.isModuleType)) {\n      incompatibleMessage = 'This package does not export ES6 modules.'\n    } else if (result.hasSideEffects === true) {\n      incompatibleMessage =\n        \"This package exports ES6 modules, but isn't marked side-effect free.\"\n    }\n    return incompatibleMessage\n  }\n\n  renderIncompatible() {\n    return (\n      <p className=\"export-analysis-section__subtext\">\n        Exports analysis is available only for packages that export ES Modules\n        and are side-effect free. <br />\n        {this.getIncompatibleMessage()}\n      </p>\n    )\n  }\n\n  renderSuccess() {\n    const { result } = this.props\n    const { gzip: totalSize } = result\n    const { exports, analysisState, assets, filterText } = this.state\n\n    const normalizedExports =\n      analysisState === State.SIZES_FULFILLED\n        ? assets\n        : Object.keys(exports)\n            .filter(exp => !exp.startsWith('_'))\n            .map(exp => ({ name: exp }))\n\n    const matchedExports = normalizedExports.filter(asset =>\n      !!filterText ? asset.name.toLowerCase().includes(filterText) : true\n    )\n\n    return (\n      <>\n        <div className=\"export-analysis-section__topbar\">\n          <p className=\"export-analysis-section__subtext export-analysis-section__infotext\">\n            GZIP sizes of individual exports\n          </p>\n          {normalizedExports.length > 15 && (\n            <InputExportFilter onChange={this.handleFilterInputChange} />\n          )}\n        </div>\n\n        <ExportList\n          isLoading={analysisState === State.EXPORTS_FULFILLED}\n          totalSize={totalSize}\n          exports={matchedExports}\n        />\n      </>\n    )\n  }\n\n  renderFailure() {\n    const { errorName, errorBody, errorDetails } = resolveBuildError(\n      this.state.resultError\n    )\n    return (\n      <div className=\"export-analysis-section__error\">\n        <h4> {errorName}</h4>\n        <p dangerouslySetInnerHTML={{ __html: errorBody }} />\n        {errorDetails && <pre>{errorDetails}</pre>}\n      </div>\n    )\n  }\n\n  render() {\n    const { analysisState } = this.state\n\n    return (\n      <div className=\"export-analysis-section\">\n        <h2 className=\"result__section-heading\"> Exports Analysis </h2>\n\n        {this.getIncompatibleMessage() && this.renderIncompatible()}\n        {analysisState === State.REJECTED && this.renderFailure()}\n        {(analysisState === State.EXPORTS_FULFILLED ||\n          analysisState === State.SIZES_FULFILLED) &&\n          this.renderSuccess()}\n        {analysisState === State.IN_PROGRESS}\n      </div>\n    )\n  }\n}\n\nexport default ExportAnalysisSection\n"
  },
  {
    "path": "pages/package/[...packageString]/components/ExportAnalysisSection/ExportAnalysisSection.scss",
    "content": "@import '../../../../../stylesheets/variables';\n@import '../../../../../stylesheets/colors';\n@import '../../../../../stylesheets/mixins';\n\n.export-analysis-section {\n  position: relative;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n}\n\n.export-analysis-section__topbar {\n  display: flex;\n  align-items: center;\n  position: relative;\n}\n\n.export-analysis-section__progress-container {\n  @include font-size-sm;\n\n  width: 100%;\n  flex-grow: 1;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  color: $raven;\n  margin-bottom: $global-spacing * 2;\n}\n\n.export-analysis-section__subtext {\n  @include font-size-xs;\n  color: $raven;\n  text-align: center;\n  max-width: $max-content-width;\n  line-height: 1.5;\n  margin: $global-spacing auto 0;\n}\n\n.export-analysis-section__infotext {\n  margin-top: -$global-spacing * 1.5;\n}\n\n.export-analysis-section__dont-break {\n  -webkit-column-break-inside: avoid;\n  page-break-inside: avoid;\n  break-inside: avoid;\n}\n\n.export-analysis-section__list {\n  @include horizontal-scroll-overflow-indicators;\n  position: relative;\n  column-width: $global-spacing * 25;\n  column-gap: $global-spacing;\n  padding: 0;\n  max-height: 80vh;\n  margin: 0;\n  list-style-type: none;\n  overflow-y: scroll;\n  height: 100%;\n  margin-top: $global-spacing * 3;\n}\n\n.export-analysis-section__filter-input-container {\n  position: absolute;\n  margin-right: auto;\n  right: 0;\n\n  @media screen and (max-width: 48em) {\n    display: none;\n  }\n}\n\n.export-analysis-section__filter-input-search-icon {\n  position: absolute;\n  right: $global-spacing;\n  z-index: 1;\n  top: 0;\n  bottom: 0;\n  margin: auto;\n  width: $global-spacing * 1.5;\n  height: $global-spacing * 1.5;\n\n  path {\n    stroke: #666;\n    stroke-width: 3px;\n  }\n}\n\n.export-analysis-section__filter-input {\n  @include font-size-sm;\n  font-family: $font-family-code;\n  padding-right: $global-spacing * 3;\n  width: 15vw;\n  transition: all 0.1s ease-in-out;\n  will-change: transform;\n  contain: strict;\n  border: 1px solid lighten($raven, 51%);\n  background: lighten($raven, 51%);\n\n  &:focus {\n    width: 23vw;\n    background: white;\n    border: 1px solid lighten($raven, 48%);\n  }\n}\n\n.export-analysis-section__pill {\n  @include font-size-sm;\n  display: flex;\n  align-items: center;\n  flex-wrap: wrap;\n  padding: $global-spacing;\n  margin-top: 0; //$global-spacing / 1.5;\n  margin-bottom: 0; //$global-spacing / 1.5;\n  position: relative;\n  z-index: 0;\n\n  &::after {\n    content: '';\n    background: lighten($raven, 52%);\n    position: absolute;\n    bottom: 0;\n    height: 1px;\n    width: 100%;\n    left: 0;\n    z-index: -2;\n  }\n}\n\n.export-analysis-section__pill-fill {\n  position: absolute;\n  right: 0;\n  top: 0;\n  height: 100%;\n  border-radius: inherit;\n  z-index: -1;\n  width: 100%;\n  transition: transform 0.4s cubic-bezier(0.635, 0.1, 0, 1.34);\n  transform-origin: 0 50%;\n}\n\n.export-analysis-section__pill-fill--low-1 {\n  background: lighten(#7ebf80, 15%);\n}\n\n.export-analysis-section__pill-fill--low-2 {\n  background: lighten(#9bc26b, 15%);\n}\n\n.export-analysis-section__pill-fill--med-1 {\n  background: lighten(#dee675, 10%);\n}\n\n.export-analysis-section__pill-fill--med-2 {\n  background: lighten(#fff080, 0%);\n}\n\n.export-analysis-section__pill-fill--med-3 {\n  background: lighten(#ffd966, 5%);\n}\n\n.export-analysis-section__pill-fill--high-1 {\n  background: lighten(#ffbf66, 10%);\n}\n\n.export-analysis-section__pill-fill--high-2 {\n  background: lighten(#ff8a66, 15%);\n}\n\n.export-analysis-section__pill-name {\n  font-family: $font-family-code;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  flex-grow: 1;\n}\n\n.export-analysis-section__pill-spinner {\n  border-radius: 50%;\n  height: $global-spacing;\n  width: $global-spacing;\n  background: rgba(0, 0, 0, 0.1);\n  animation: pule-pill-spinner 0.5s alternate infinite;\n}\n\n@keyframes pule-pill-spinner {\n  from {\n    transform: scale(0.1);\n  }\n  to {\n    transform: scale(1);\n  }\n}\n\n.export-analysis-section__pill-size {\n  @include font-size-xs;\n}\n\n.export-analysis-section__pill-size-unit {\n  color: $raven;\n  font-size: 90%;\n  margin-left: $global-spacing * 0.2;\n}\n\n.export-analysis-section__letter-heading {\n  @include font-size-md;\n  margin: $global-spacing * 1.5 0 $global-spacing;\n}\n\n.export-analysis-section__error {\n  margin: auto;\n  text-align: center;\n\n  h4 {\n    font-family: $font-family-code;\n    margin-bottom: 0;\n  }\n\n  p {\n    @include font-size-sm;\n    color: $raven;\n  }\n}\n"
  },
  {
    "path": "pages/package/[...packageString]/components/ExportAnalysisSection/index.js",
    "content": "import ExportAnalysisSection from './ExportAnalysisSection'\n\nexport default ExportAnalysisSection\n"
  },
  {
    "path": "pages/package/[...packageString]/components/InterLinksSection/InterLinksSection.js",
    "content": "import React, { useEffect, useState } from 'react'\nimport {\n  parsePackageString,\n  daysFromToday,\n} from '../../../../../utils/common.utils'\nimport API from '../../../../../client/api'\nimport InterLinksSectionCard from './InterLinksSectionCard'\n\nfunction usePackagesFromSameScope(packageName) {\n  const { scope } = parsePackageString(packageName)\n\n  const [morePackages, setMorePackages] = useState([])\n  const getAgeScore = result =>\n    Math.min(1 / Math.log(daysFromToday(result.package.date)), 1)\n\n  useEffect(() => {\n    API.getSuggestions(`@${scope}`).then(results => {\n      const sorted = results\n        .filter(result => result.package.scope === scope)\n        .filter(result => result.package.name !== packageName)\n        .sort(\n          (rA, rB) =>\n            rB.score.detail.popularity * getAgeScore(rB) -\n            rA.score.detail.popularity * getAgeScore(rA)\n        )\n      setMorePackages(sorted)\n    })\n  }, [packageName])\n  return morePackages\n}\n\nconst InterLinksSection = props => {\n  const { scope } = parsePackageString(props.packageName)\n  const morePackages = usePackagesFromSameScope(props.packageName)\n\n  if (!morePackages.length) {\n    return null\n  }\n\n  return (\n    <div className=\"content-container\">\n      <div className=\"interlinks-section\">\n        <h2 className=\"result__section-heading result__section-heading--new\">\n          More &nbsp;<code>{scope}</code> &nbsp;packages\n        </h2>\n        <div className=\"interlinks-section__list\">\n          {morePackages.map(pack => (\n            <InterLinksSectionCard\n              key={pack.package.name}\n              name={pack.package.name}\n              description={pack.package.description}\n              date={pack.package.date}\n            />\n          ))}\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport default InterLinksSection\n"
  },
  {
    "path": "pages/package/[...packageString]/components/InterLinksSection/InterLinksSection.scss",
    "content": "@import '../../../../../stylesheets/variables';\n@import '../../../../../stylesheets/mixins';\n\n.interlinks-section {\n  width: 100%;\n}\n\n.interlinks-section__list {\n  @include horizontal-scroll-overflow-indicators;\n  @include hide-scrollbars;\n\n  display: flex;\n  margin-left: -$global-spacing;\n  margin-right: -$global-spacing;\n  white-space: nowrap;\n  overflow-x: scroll;\n}\n"
  },
  {
    "path": "pages/package/[...packageString]/components/InterLinksSection/InterLinksSectionCard/InterLinksSectionCard.js",
    "content": "import { sanitizeHTML } from '../../../../../../utils/common.utils'\nimport Link from 'next/link'\nimport { formatDistanceToNow } from 'date-fns'\nimport React from 'react'\n\nexport default function InterLinksSectionCard(props) {\n  const { description, name, date } = props\n\n  return (\n    <Link href={`/package/${name}`} className=\"interlinks-card\">\n      <div className=\"interlinks-card__wrap\">\n        <div className=\"interlinks-card__header\">\n          <h4 className=\"interlinks-card__name\">{name}</h4>\n        </div>\n        <p\n          className=\"interlinks-card__description\"\n          dangerouslySetInnerHTML={{\n            __html: sanitizeHTML(description),\n          }}\n        />\n        <div className=\"interlinks-card__publish-date\">\n          published {formatDistanceToNow(new Date(date), { addSuffix: true })}\n        </div>\n      </div>\n    </Link>\n  )\n}\n"
  },
  {
    "path": "pages/package/[...packageString]/components/InterLinksSection/InterLinksSectionCard/InterLinksSectionCard.scss",
    "content": "@use \"sass:math\";\n\n@import '../../../../../../stylesheets/colors';\n@import '../../../../../../stylesheets/variables';\n\n.interlinks-card {\n  border: 1px solid $autocomplete-border-color;\n  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);\n  border-radius: 10px;\n  width: calc(22% - #{$global-spacing * 2});\n  margin: $global-spacing;\n  color: inherit;\n  transition: all 0.2s;\n  flex: 1 0 auto;\n  white-space: normal;\n\n  &:hover {\n    transform: scale(1.03);\n    box-shadow: 0 5px 10px rgba(0, 0, 0, 0.06);\n    border: 1px solid fade-in($autocomplete-border-color, 0.02);\n    background: rgba(200, 200, 200, 0.07);\n  }\n\n  @media screen and (max-width: 64em) {\n    margin: math.div($global-spacing, 1.5);\n    width: calc(30% - #{$global-spacing * 2});\n  }\n\n  @media screen and (max-width: 48em) {\n    margin: math.div($global-spacing, 1.5);\n    width: calc(45% - #{$global-spacing * 1.5});\n  }\n\n  @media screen and (max-width: 40em) {\n    width: 85%;\n  }\n}\n\n.interlinks-card__wrap {\n  padding: $global-spacing * 1.5;\n  padding-bottom: math.div($global-spacing, 1.5);\n  flex-grow: 1;\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.interlinks-card__header {\n  display: flex;\n}\n\n.interlinks-card__name {\n  margin: 0;\n  font-family: $font-family-code;\n  font-weight: $font-weight-light;\n  flex-grow: 1;\n  word-break: break-word;\n}\n\n.interlinks-card__description {\n  @include font-size-sm;\n  color: $dark-raven;\n  line-height: 1.5;\n  margin: $global-spacing 0 0 0;\n  word-break: break-word;\n  flex-grow: 1;\n\n  img {\n    height: auto;\n    max-width: 100%;\n  }\n}\n\n.interlinks-card__publish-date {\n  @include font-size-xs;\n  color: $raven;\n  border-top: 1px dashed lighten($raven, 50%);\n  padding-top: $global-spacing * 0.5;\n  margin-top: $global-spacing * 0.5;\n}\n"
  },
  {
    "path": "pages/package/[...packageString]/components/InterLinksSection/InterLinksSectionCard/index.js",
    "content": "export { default } from './InterLinksSectionCard'\n"
  },
  {
    "path": "pages/package/[...packageString]/components/InterLinksSection/index.js",
    "content": "export { default } from './InterLinksSection'\n"
  },
  {
    "path": "pages/package/[...packageString]/components/SimilarPackagesSection/SimilarPackagesSection.js",
    "content": "import React, { Component } from 'react'\nimport SimilarPackageCard from '../../../../../client/components/SimilarPackageCard/SimilarPackageCard'\n\nclass SimilarPackagesSection extends Component {\n  render() {\n    const { packs, category, comparisonGzip } = this.props\n    return (\n      <div className=\"similar-packages-section\">\n        <h2 className=\"result__section-heading similar-packages-section__heading\">\n          {' '}\n          Similar Packages{' '}\n        </h2>\n        <h5 className=\"similar-packages-section__subheading\"> {category} </h5>\n\n        <div className=\"similar-packages-section__list\">\n          {packs.map(pack => (\n            <SimilarPackageCard\n              key={pack.name}\n              pack={pack}\n              comparisonSizePercent={\n                ((pack.gzip - comparisonGzip) / comparisonGzip) * 100\n              }\n            />\n          ))}\n          <SimilarPackageCard category={category} isEmpty />\n        </div>\n      </div>\n    )\n  }\n}\n\nexport default SimilarPackagesSection\n"
  },
  {
    "path": "pages/package/[...packageString]/components/SimilarPackagesSection/SimilarPackagesSection.scss",
    "content": "@import '../../../../../stylesheets/variables';\n@import '../../../../../stylesheets/colors';\n\n.similar-packages-section__list {\n  display: flex;\n  flex-wrap: wrap;\n  margin-left: -$global-spacing;\n  margin-right: -$global-spacing;\n}\n\n.similar-packages-section__heading {\n  margin-bottom: 0;\n}\n\n.similar-packages-section__subheading {\n  color: lighten($raven, 15%);\n  text-align: center;\n  margin-top: $global-spacing;\n}\n"
  },
  {
    "path": "pages/package/[...packageString]/components/SimilarPackagesSection/index.js",
    "content": "import SimilarPackagesSection from './SimilarPackagesSection'\n\nexport default SimilarPackagesSection\n"
  },
  {
    "path": "pages/package/[...packageString]/components/TreemapSection.js",
    "content": "import React, { Component } from 'react'\nimport { formatSize } from 'utils'\nimport colors from 'client/config/colors'\nimport { Treemap, TreemapSquare } from 'client/components/Treemap'\n\nclass TreemapSection extends Component {\n  state = {\n    width: 0,\n    height: 0,\n  }\n\n  componentDidMount() {\n    const { dependencySizes } = this.props\n    const width = this.treemapSection.getBoundingClientRect().width\n    let heightMultiplier = 1\n\n    if (dependencySizes.length < 5) {\n      heightMultiplier = 0.5\n    } else if (dependencySizes.length <= 10) {\n      heightMultiplier = 0.7\n    } else if (dependencySizes.length <= 15) {\n      heightMultiplier = 1.1\n    }\n\n    let height = 250 * heightMultiplier\n\n    if (window.innerWidth <= 640) {\n      height = window.innerHeight * 0.65 * heightMultiplier\n    } else if (window.innerWidth <= 768) {\n      height = window.innerHeight * 0.45 * heightMultiplier\n    }\n    this.setState({\n      width,\n      height,\n    })\n  }\n\n  render() {\n    const { packageName, packageSize, dependencySizes } = this.props\n    const { width, height } = this.state\n\n    const getFormattedSize = value => {\n      const { size, unit } = formatSize(value)\n      return `${size.toFixed(2)} ${unit}`\n    }\n\n    let depdendenciesCopy = [...dependencySizes]\n    depdendenciesCopy.forEach(dep => {\n      if (dep.name === packageName) {\n        dep.name = '(self)'\n        dep.isSelf = true\n      }\n    })\n\n    const sizeSum = depdendenciesCopy.reduce(\n      (acc, dep) => acc + dep.approximateSize,\n      0\n    )\n    depdendenciesCopy = depdendenciesCopy\n      .map(dep => ({\n        ...dep,\n        percentShare: (dep.approximateSize / sizeSum) * 100,\n        // The size given by the API is after performing\n        // minimal minification on the dependency source –\n        // whitespace removal, dead code removal etc.\n        // whereas the displayed size of the package searched by the user\n        // is after full minification. We use the ratio from approximate\n        // sizes to predict what these dependencies possibly weighed if\n        // they were also minified completely instead of partially\n        sizeShare: (dep.approximateSize / sizeSum) * packageSize,\n      }))\n      .map(dep => ({\n        ...dep,\n        tooltip: `${dep.name} ｜ ${dep.percentShare.toFixed(\n          1\n        )}% ｜ ~ ${getFormattedSize(dep.sizeShare)}`,\n      }))\n\n    depdendenciesCopy.sort((depA, depB) => {\n      return depB.percentShare - depA.percentShare\n    })\n\n    let compactedDependencies = []\n\n    const compactLimit = window.innerWidth <= 768 ? 8 : 16\n    const ellipsizeLimit = window.innerWidth <= 768 ? 3.5 : 1.5\n    if (depdendenciesCopy.length > compactLimit) {\n      const otherDependencies = depdendenciesCopy.slice(compactLimit)\n      compactedDependencies = depdendenciesCopy.slice(0, compactLimit)\n\n      const approximateSize = otherDependencies.reduce(\n        (acc, dep) => acc + dep.approximateSize,\n        0\n      )\n      const percentShare = otherDependencies.reduce(\n        (acc, dep) => acc + dep.percentShare,\n        0\n      )\n      const sizeShare = otherDependencies.reduce(\n        (acc, dep) => acc + dep.sizeShare,\n        0\n      )\n\n      compactedDependencies.push({\n        name: '(others)',\n        approximateSize,\n        percentShare,\n        sizeShare,\n        isOthers: true,\n        tooltip: otherDependencies\n          .map(\n            dep =>\n              `${dep.name} ｜ ${dep.percentShare.toFixed(\n                1\n              )}% ｜ ~ ${getFormattedSize(dep.sizeShare)} min`\n          )\n          .join(' \\u000D\\u000A  \\u000D\\u000A '),\n      })\n    } else {\n      compactedDependencies = depdendenciesCopy\n    }\n\n    return (\n      <section\n        className=\"treemap__section\"\n        ref={ts => (this.treemapSection = ts)}\n      >\n        <h2 className=\"result__section-heading\"> Composition </h2>\n        <Treemap width={width} height={height} className=\"treemap\">\n          {compactedDependencies.map((dep, index) => (\n            <TreemapSquare\n              key={dep.name}\n              value={dep.percentShare}\n              style={{ background: colors[index % colors.length] }}\n              data-balloon={dep.tooltip}\n              data-balloon-pos=\"top\"\n              className=\"treemap__square\"\n            >\n              {dep.percentShare > ellipsizeLimit &&\n              dep.name.length < dep.percentShare * (12 / ellipsizeLimit) ? (\n                <div className=\"treemap__content\">\n                  <div className=\"treemap__label\">\n                    {dep.isSelf || dep.isOthers ? (\n                      <span> {dep.name} </span>\n                    ) : (\n                      <a\n                        href={`/package/${dep.name}`}\n                        target=\"_blank\"\n                        rel=\"noreferrer\"\n                      >\n                        {dep.name}\n                      </a>\n                    )}\n                  </div>\n                  <div\n                    className=\"treemap__percent\"\n                    style={{\n                      fontSize: `${\n                        14 + Math.min(dep.percentShare * 1.2, 25)\n                      }px`,\n                    }}\n                  >\n                    {dep.percentShare.toFixed(1)}\n                    <span className=\"treemap__percent-sign\">%</span>\n                  </div>\n                </div>\n              ) : (\n                <span className=\"treemap__ellipsis\">&hellip;</span>\n              )}\n            </TreemapSquare>\n          ))}\n        </Treemap>\n        <p className=\"treemap__note\">\n          <b>Note: </b> These sizes represent the contribution made by\n          dependencies (direct or transitive) to <code>{packageName}</code>\n          &apos;s size. These may be different from the dependencies&apos;\n          standalone sizes.\n        </p>\n      </section>\n    )\n  }\n}\n\nexport default TreemapSection\n"
  },
  {
    "path": "pages/package/[...packageString]/index.page.js",
    "content": "import ResultPage from './ResultPage'\nexport { getServerSideProps } from './ResultPage'\nexport default ResultPage\n"
  },
  {
    "path": "pages/scan/Scan.js",
    "content": "import React, { Component } from 'react'\nimport Analytics from '../../client/analytics'\nimport ResultLayout from '../../client/components/ResultLayout'\nimport Separator from '../../client/components/Separator'\nimport MetaTags from '../../client/components/MetaTags'\nimport scanBlacklist from '../../client/config/scanBlacklist'\nimport Dropzone from 'react-dropzone'\nimport Router from 'next/router'\nimport * as semver from 'semver'\n\nexport default class Scan extends Component {\n  state = {\n    packages: null,\n    selectedPackages: [],\n  }\n\n  componentDidMount() {\n    Analytics.pageView('scan')\n  }\n\n  resolveVersionFromRange = range => {\n    const rangeSet = new semver.Range(range).set\n    return rangeSet[0][0].semver.version\n  }\n\n  setSelectedPackages = () => {\n    const checkedInputs =\n      this.packageSelectionContainer.querySelectorAll('input:checked')\n\n    const selectedPackages = Array.from(checkedInputs).map(({ value }) => {\n      const [name, resolvedVersion] = value.split('#')\n      return { name, resolvedVersion }\n    })\n\n    this.setState({ selectedPackages })\n  }\n\n  handleSelectionChange = () => {\n    this.setSelectedPackages()\n  }\n\n  handleDropAccepted = ([file]) => {\n    const reader = new FileReader()\n    reader.onload = () => {\n      try {\n        const json = JSON.parse(reader.result)\n        const packages = Object.keys(json.dependencies)\n          .filter(packageName => {\n            const versionRange = json.dependencies[packageName]\n            return semver.valid(versionRange) || semver.validRange(versionRange)\n          })\n          .map(packageName => {\n            const versionRange = json.dependencies[packageName]\n\n            return {\n              name: packageName,\n              versionRange,\n              resolvedVersion: this.resolveVersionFromRange(versionRange),\n            }\n          })\n\n        this.setState({ packages }, this.setSelectedPackages)\n\n        Analytics.scanPackageJsonDropped(packages.length)\n      } catch (err) {\n        this.showInvalidFileError()\n      }\n    }\n\n    try {\n      reader.readAsBinaryString(file)\n    } catch (err) {\n      console.error(err)\n      this.showInvalidFileError()\n    }\n  }\n\n  handleDropRejected = () => {\n    this.showInvalidFileError()\n  }\n\n  handleScanClick = () => {\n    const { selectedPackages } = this.state\n    const query = selectedPackages\n      .map(pack => `${pack.name}@${pack.resolvedVersion}`)\n      .join(',')\n    Router.push(`/scan-results?packages=${query}`)\n\n    Analytics.performedScan()\n  }\n\n  handleResetClick = () => {\n    this.setState({ packages: null, selectedPackages: [] })\n  }\n\n  showInvalidFileError() {\n    alert('Could not parse the `package.json` file.')\n\n    Analytics.scanParseError()\n  }\n\n  render() {\n    let content\n    const { packages, selectedPackages } = this.state\n\n    if (!packages) {\n      content = (\n        <div>\n          <Dropzone\n            className=\"scan__dropzone\"\n            onDropAccepted={this.handleDropAccepted}\n            onDropRejected={this.handleDropRejected}\n            multiple={false}\n            accept=\"application/json\"\n          >\n            <p>\n              Drop a <code> package.json </code> file here\n            </p>\n            <Separator />\n            <button className=\"scan__btn\">\n              Upload <code> package.json </code>\n            </button>\n          </Dropzone>\n        </div>\n      )\n    } else {\n      content = (\n        <div>\n          <header className=\"scan__selection-header\">\n            <h1 className=\"scan__page-title\"> Select packages to scan </h1>\n            <button className=\"scan__btn\" onClick={this.handleScanClick}>\n              Scan {selectedPackages.length} packages\n            </button>\n            <button className=\"scan__btn\" onClick={this.handleResetClick}>\n              Reset\n            </button>\n          </header>\n          <ul\n            className=\"scan__package-container\"\n            ref={pc => (this.packageSelectionContainer = pc)}\n          >\n            {packages.map(({ name, versionRange, resolvedVersion }) => (\n              <li className=\"scan__package-item\" key={name}>\n                <label>\n                  <input\n                    type=\"checkbox\"\n                    defaultChecked={\n                      !scanBlacklist.some(regex => regex.test(name))\n                    }\n                    value={`${name}#${resolvedVersion}`}\n                    onChange={this.handleSelectionChange}\n                  />\n                  <span className=\"scan__package-item-title\">\n                    <span>{name}</span>\n                    <span className=\"scan__package-item-version\">\n                      {versionRange} &rarr; {resolvedVersion}\n                    </span>\n                  </span>\n                </label>\n              </li>\n            ))}\n          </ul>\n        </div>\n      )\n    }\n    return (\n      <ResultLayout className=\"scan-page\">\n        <MetaTags\n          title=\"Scan package.json ❘ Bundlephobia\"\n          canonicalPath=\"/scan\"\n          description=\"Scan dependencies in your package.json to find the largest and heaviest npm packages in your frontend javascript bundle.\"\n        />\n        {content}\n      </ResultLayout>\n    )\n  }\n}\n\nexport const getServerSideProps = () => {\n  return { props: {} }\n}\n"
  },
  {
    "path": "pages/scan/Scan.scss",
    "content": "@use \"sass:math\";\n\n@import '../../stylesheets/base';\n@import '../../stylesheets/variables';\n\n.scan__dropzone {\n  border: 2px dashed lighten($raven, 20%);\n  width: 50vw;\n  height: 50vh;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-direction: column;\n\n  p {\n    margin-top: 0;\n  }\n}\n\n.scan__btn {\n  @include font-size-xs;\n  cursor: pointer;\n  margin-top: $global-spacing;\n  background: #212121;\n  border-radius: 6px;\n  border: none;\n  padding: $global-spacing $global-spacing * 2;\n  display: block;\n  transition: background 0.2s;\n  color: white;\n  letter-spacing: 1px;\n  font-weight: $font-weight-bold;\n  font-family: $font-family-body;\n\n  &:hover {\n    background: $raven;\n  }\n\n  & ~ & {\n    margin-left: $global-spacing * 1.5;\n  }\n}\n\n.scan__package-container {\n  list-style: none;\n  columns: 3;\n\n  @media screen and (max-width: 48em) {\n    columns: 2;\n  }\n\n  @media screen and (max-width: 40em) {\n    columns: 1;\n  }\n}\n\n.scan__package-item-title {\n  opacity: 0.5;\n}\n\n.scan__package-item {\n  padding: math.div($global-spacing, 2.5);\n\n  input {\n    margin-right: $global-spacing;\n\n    &:checked ~ .scan__package-item-title {\n      opacity: 1;\n    }\n  }\n\n  label {\n    cursor: pointer;\n  }\n}\n\n.scan__package-item-version {\n  @include font-size-xs;\n  font-family: $font-family-code;\n  margin-left: $global-spacing;\n  color: lighten($raven, 10%);\n}\n\n.scan__selection-header {\n  display: flex;\n  padding: 0 $global-spacing * 2;\n  align-items: center;\n  margin-bottom: $global-spacing * 2;\n\n  .scan__page-title {\n    margin: 0 $global-spacing * 4 0 0;\n  }\n\n  .scan__btn {\n    @include font-size-xxs;\n    text-transform: uppercase;\n    padding: $global-spacing $global-spacing * 2;\n    border: 2px solid #212121;\n    background: white;\n    color: #212121;\n\n    &:hover {\n      background: #212121;\n      color: white;\n    }\n  }\n}\n"
  },
  {
    "path": "pages/scan/index.page.js",
    "content": "import Scan from './Scan'\nexport { getServerSideProps } from './Scan'\nexport default Scan\n"
  },
  {
    "path": "pages/scan-results/ScanResults.js",
    "content": "import Router, { withRouter } from 'next/router'\nimport React, { Component } from 'react'\nimport Analytics from '../../client/analytics'\nimport FlipMove from 'react-flip-move'\nimport cx from 'classnames'\n\nconst PromiseQueue = require('p-queue')\nconst queryString = require('query-string')\nimport Stat from '../../client/components/Stat'\nimport Link from 'next/link'\nimport ResultLayout from '../../client/components/ResultLayout'\nimport { parsePackageString } from '../../utils/common.utils'\n\nimport API from '../../client/api'\nimport { getTimeFromSize } from '../../utils'\n\nclass ResultCard extends Component {\n  render() {\n    const { pack, index } = this.props\n\n    let content\n\n    switch (pack.promiseState) {\n      case 'pending':\n        content = (\n          <div className=\"scan-results__loading-text\">Calculating &hellip;</div>\n        )\n        break\n\n      case 'fulfilled':\n        content = (\n          <div className=\"scan-results__stat-container\">\n            <Stat\n              className=\"scan-results__stat-item\"\n              value={pack.result.size}\n              type={Stat.type.SIZE}\n              label=\"Min\"\n              compact\n            />\n            <Stat\n              className=\"scan-results__stat-item\"\n              value={pack.result.gzip}\n              type={Stat.type.SIZE}\n              label=\"Min + GZIP\"\n              compact\n            />\n            <Stat\n              className=\"scan-results__stat-item\"\n              value={getTimeFromSize(pack.result.gzip).threeG}\n              type={Stat.type.TIME}\n              label=\"Slow 3G\"\n              compact\n            />\n            <Stat\n              className=\"scan-results__stat-item\"\n              value={getTimeFromSize(pack.result.gzip).fourG}\n              type={Stat.type.TIME}\n              label=\"Emerging 4G\"\n              compact\n            />\n          </div>\n        )\n        break\n\n      case 'rejected':\n        content = (\n          <details className=\"scan-results__error-text\">\n            <summary> {pack.error.code}</summary>\n            <p dangerouslySetInnerHTML={{ __html: pack.error.message }} />\n          </details>\n        )\n        break\n    }\n\n    return (\n      <li\n        className={cx('scan-results__item', {\n          'scan-results__item--loading': pack.promiseState === 'pending',\n          'scan-results__item--error': pack.promiseState === 'rejected',\n        })}\n      >\n        <div className=\"scan-results__index\">\n          <span> {index + 1}. </span>\n        </div>\n        <div className=\"scan-results__name\" data-name={pack.name}>\n          <Link href={`/package/${pack.packageString}`}>\n            <span className=\"scan-results__package-name\"> {pack.name}</span>\n            <div>\n              {pack.version && (\n                <span className=\"scan-results__package-version\">\n                  v{pack.version}\n                </span>\n              )}\n            </div>\n          </Link>\n        </div>\n        {content}\n      </li>\n    )\n  }\n}\n\nclass ScanResults extends Component {\n  constructor(props) {\n    super(props)\n\n    const { router } = this.props\n    const sortMode = router.query.sortMode\n    const packageStrings = router.query.packages\n    const packages = packageStrings\n      .split(',')\n      .map(str => str.trim())\n      .map(str => ({\n        promiseState: 'pending',\n        packageString: str,\n        ...parsePackageString(str),\n      }))\n\n    this.state = { packages, sortMode: sortMode }\n  }\n\n  // Disables Next.js's Automatic Static Optimization\n  // which causes query params to be empty\n  // see https://nextjs.org/docs/routing/dynamic-routes#caveats\n  static async getInitialProps() {\n    return {}\n  }\n\n  componentDidMount() {\n    const { packages } = this.state\n    const queue = new PromiseQueue({ concurrency: 3 })\n    const startTime = Date.now()\n\n    Analytics.pageView('scan results')\n\n    packages.forEach(pack => {\n      queue.add(() => {\n        const start = Date.now()\n\n        API.getInfo(pack.packageString)\n          .then(result => {\n            this.updatePackageState(pack, {\n              promiseState: 'fulfilled',\n              version: result.version,\n              result,\n            })\n\n            Analytics.searchSuccess({\n              packageName: pack.packageString,\n              timeTaken: Date.now() - start,\n            })\n          })\n          .catch(({ error }) => {\n            console.error(error)\n            this.updatePackageState(pack, {\n              promiseState: 'rejected',\n              error,\n            })\n\n            Analytics.searchFailure({\n              packageName: pack.packageString,\n              timeTaken: Date.now() - start,\n            })\n          })\n      })\n    })\n\n    queue.onIdle().then(() => {\n      const successfulBuildCount = packages.reduce(\n        (curSum, nextPack) =>\n          nextPack.promiseState === 'fulfilled' ? curSum + 1 : curSum,\n        0\n      )\n\n      Analytics.scanCompleted({\n        successRatio: successfulBuildCount / packages.length,\n        timeTaken: Date.now() - startTime,\n      })\n    })\n  }\n\n  updatePackageState(pack, state) {\n    const { packages } = this.state\n    const packIndex = packages.findIndex(\n      ({ packageString }) => packageString === pack.packageString\n    )\n\n    packages[packIndex] = {\n      ...packages[packIndex],\n      ...state,\n    }\n\n    this.setState({ packages })\n  }\n\n  setParamsAndState = sortMode => {\n    debugger\n    const updatedQuery = { ...this.props.router.query, sortMode }\n    Router.replace(\n      `/scan-results?${queryString.stringify(updatedQuery, { encode: false })}`\n    )\n\n    this.setState({ sortMode: sortMode })\n  }\n\n  handleSortAlphabetic = () => {\n    this.setParamsAndState('alphabetic')\n  }\n\n  handleSortSize = () => {\n    this.setParamsAndState('size')\n  }\n\n  sortPackages = () => {\n    const { packages, sortMode } = this.state\n    let sortedList\n\n    if (sortMode === 'size') {\n      sortedList = packages.sort((packA, packB) => {\n        const packASize = packA.result ? packA.result.gzip : 0\n        const packBSize = packB.result ? packB.result.gzip : 0\n\n        return packBSize - packASize\n      })\n    } else {\n      sortedList = packages.sort((packA, packB) =>\n        packA.name.localeCompare(packB.name)\n      )\n    }\n    return sortedList\n  }\n\n  render() {\n    const { sortMode } = this.state\n    const packages = this.sortPackages()\n\n    const totalMinSize = packages.reduce(\n      (curTotal, pack) => curTotal + (pack.result ? pack.result.size : 0),\n      0\n    )\n\n    const totalGZIPSize = packages.reduce(\n      (curTotal, pack) => curTotal + (pack.result ? pack.result.gzip : 0),\n      0\n    )\n\n    return (\n      <ResultLayout className=\"scan-results\">\n        <h1> Results</h1>\n        <div className=\"scan-results__sort-panel\">\n          <label> Sort By: </label>\n          <button\n            className={cx({\n              'scan-results__sort--selected': sortMode === 'alphabetic',\n            })}\n            onClick={this.handleSortAlphabetic}\n          >\n            Name: A &rarr; Z\n          </button>\n          <button\n            className={cx({\n              'scan-results__sort--selected': sortMode === 'size',\n            })}\n            onClick={this.handleSortSize}\n          >\n            Size: High &rarr; Low\n          </button>\n        </div>\n        <ul className=\"scan-results__container\">\n          <FlipMove\n            duration={350}\n            easing=\"cubic-bezier(0.175, 0.885, 0.325, 1.040)\"\n          >\n            {packages.map((pack, index) => (\n              <ResultCard pack={pack} index={index} key={pack.name} />\n            ))}\n          </FlipMove>\n          <li className=\"scan-results__item scan-results__item--total\">\n            <div className=\"scan-results__name\">Total</div>\n            <div className=\"scan-results__stat-container\">\n              <Stat\n                className=\"scan-results__stat-item\"\n                value={totalMinSize}\n                type={Stat.type.SIZE}\n                label=\"Min\"\n                compact\n              />\n              <Stat\n                className=\"scan-results__stat-item\"\n                value={totalGZIPSize}\n                type={Stat.type.SIZE}\n                label=\"Min + GZIP\"\n                compact\n              />\n              <Stat\n                className=\"scan-results__stat-item\"\n                value={totalGZIPSize / 1024 / 30}\n                type={Stat.type.TIME}\n                label=\"2G Edge\"\n                compact\n              />\n              <Stat\n                className=\"scan-results__stat-item\"\n                value={totalGZIPSize / 1024 / 50}\n                type={Stat.type.TIME}\n                label=\"Slow 3G\"\n                compact\n              />\n            </div>\n          </li>\n        </ul>\n        <div className=\"scan-results__note\">\n          <b>NOTE:</b> Sizes shown are when importing the complete package.\n          Actual sizes might be smaller if only parts of the package are used or\n          if packages share common dependencies. This is not a substitute for\n          bundle size.\n        </div>\n      </ResultLayout>\n    )\n  }\n}\n\nexport default withRouter(ScanResults)\n"
  },
  {
    "path": "pages/scan-results/ScanResults.scss",
    "content": "@import '../../stylesheets/base';\n@import '../../stylesheets/variables';\n\n.scan-results {\n  .page-content {\n    width: $max-content-width;\n    margin: auto;\n  }\n\n  h1 {\n    margin-bottom: 0.5rem;\n  }\n}\n\n.scan-results__container {\n  width: 100%;\n  padding: 0;\n  margin: 0;\n  border: 1px solid lighten($raven, 50%);\n  border-radius: 2px;\n  box-shadow: 0 0 4px lighten($raven, 50%);\n}\n\n.scan-results__note {\n  @include font-size-xs;\n  color: lighten($raven, 20%);\n  margin-top: $global-spacing * 2;\n  margin-bottom: $global-spacing * 4;\n}\n\n.scan-results__sort-panel {\n  @include font-size-sm;\n  margin-bottom: 1.5rem;\n  color: $raven;\n\n  label {\n    text-transform: uppercase;\n    font-weight: $font-weight-bold;\n  }\n\n  button {\n    cursor: pointer;\n    border: none;\n    border-bottom: 1px dashed lighten($raven, 40%);\n    box-shadow: none;\n    background: none;\n    margin-left: $global-spacing;\n    padding: $global-spacing * 0.5 0;\n    transition: border-color 0.2s;\n    color: inherit;\n\n    &:hover {\n      border-color: lighten($raven, 30%);\n    }\n\n    &:focus {\n      outline: none;\n    }\n  }\n\n  .scan-results__sort--selected {\n    color: $pastel-green;\n    border-color: $pastel-green;\n  }\n}\n\n.scan-results__item {\n  background: linear-gradient(\n    rgba(255, 255, 255, 0) 5%,\n    rgba(255, 255, 255, 1) 10%,\n    rgba(255, 255, 255, 1) 90%,\n    rgba(255, 255, 255, 0) 95%\n  );\n  display: flex;\n  list-style: none;\n  padding: $global-spacing * 2 $global-spacing * 2;\n  align-items: center;\n\n  & + & {\n    border-top: 1px solid lighten($raven, 50%);\n  }\n}\n\n.scan-results__item--total {\n  background: transparentize($raven, 0.96);\n}\n\n.scan-results__item--loading {\n  position: relative;\n  padding: $global-spacing * 2.5 $global-spacing * 2;\n\n  &::after {\n    content: '';\n    left: 0;\n    top: 0;\n    position: absolute;\n    width: 50%;\n    height: 100%;\n    background: rgba(60, 60, 70, 0.04);\n    animation: progress-bar 1s cubic-bezier(0.645, 0.045, 0.355, 1) alternate\n      infinite;\n  }\n}\n\n@keyframes progress-bar {\n  from {\n    transform: translateX(0%);\n  }\n\n  to {\n    transform: translateX(100%);\n  }\n}\n\n.scan-results__stat-container {\n  flex-grow: 1;\n  display: flex;\n  align-items: center;\n  animation: fade-in-result 0.4s;\n  max-width: $global-spacing * 68;\n}\n\n.scan-results__stat-item {\n  flex: 1;\n}\n\n.scan-results__item--total {\n  .scan-results__stat-item {\n    .stat-container__value,\n    .stat-container__unit,\n    .stat-container__label {\n      font-weight: $font-weight-bold;\n    }\n\n    .stat-container__label {\n      letter-spacing: 0.6px;\n    }\n  }\n}\n\n.scan-results__loading-text,\n.scan-results__error-text {\n  @include font-size-sm;\n  flex-grow: 1;\n  text-align: center;\n  color: lighten($raven, 20%);\n  text-transform: uppercase;\n  font-weight: $font-weight-bold;\n  letter-spacing: 0.5px;\n}\n\n.scan-results__error-text {\n  font-family: $font-family-code;\n  text-transform: none;\n\n  summary {\n    text-align: left;\n\n    &:focus {\n      outline: none;\n    }\n  }\n\n  p {\n    @include font-size-sm;\n    font-weight: $font-weight-light;\n    text-align: left;\n    color: lighten($raven, 10%);\n    font-family: $font-family-body;\n    line-height: 1.5;\n  }\n}\n\n@keyframes fade-in-result {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n$index-width: 4rem;\n\n.scan-results__index {\n  @include font-size-xl;\n  font-weight: $font-weight-very-bold;\n  color: lighten($raven, 50%);\n  position: relative;\n  width: $index-width;\n}\n\n.scan-results__name {\n  position: relative;\n  width: 15%;\n  min-width: $global-spacing * 18;\n  padding-left: $global-spacing;\n\n  a {\n    color: inherit;\n  }\n\n  .scan-results__package-version {\n    color: $raven;\n    font-family: $font-family-code;\n  }\n\n  .scan-results__item--total & {\n    @include font-size-md;\n    width: 15%;\n    margin-left: $index-width;\n    font-weight: $font-weight-bold;\n    letter-spacing: 1px;\n    text-transform: uppercase;\n  }\n}\n\n.scan-results__package-name {\n  .scan-results__item--error & {\n    text-decoration: line-through;\n  }\n}\n"
  },
  {
    "path": "pages/scan-results/index.page.js",
    "content": "import ScanResults from './ScanResults'\n\nexport default ScanResults\n"
  },
  {
    "path": "process.yml",
    "content": "apps:\n  - script: './index.js'\n    args: '--project tsconfig.server.json ./index.ts'\n    name: main\n    instances: 1\n    max_memory_restart: 500M\n    error_file: logs/index.log\n    out_file: logs/index.log\n    kill_timeout: 20000\n    exec_mode: cluster\n    env:\n      NODE_ENV: production\n      DEBUG: 'bp:*'\n      PORT: 5400\n      CACHE_SERVICE_ENDPOINT: http://localhost:7001\n      BUILD_SERVICE_ENDPOINT: http://localhost:7002\n\n  - script: ./build-service/index.js\n    name: build-service\n    instances: 3\n    max_memory_restart: 1000M\n    exec_mode: cluster\n    error_file: logs/build-service.log\n    out_file: logs/build-service.log\n    kill_timeout: 20000\n    env:\n      NODE_ENV: production\n      DEBUG: 'bp:*'\n\n  - script: ./cache-service/index.js\n    name: cache-service\n    instances: 1\n    max_memory_restart: 200M\n    exec_mode: cluster\n    kill_timeout: 5000\n    error_file: logs/cache-service.log\n    out_file: logs/cache-service.log\n    env:\n      NODE_ENV: production\n      DEBUG: 'bp:*'\n"
  },
  {
    "path": "scripts/README.md",
    "content": "# scripts\n"
  },
  {
    "path": "scripts/cleanup-old-keys.ts",
    "content": "import * as fs from 'fs'\nimport * as path from 'path'\nimport * as admin from 'firebase-admin'\nimport * as semver from 'semver'\nimport { chain } from 'stream-chain'\nimport { parser } from 'stream-json'\nimport { streamArray } from 'stream-json/streamers/StreamArray'\nimport { streamObject } from 'stream-json/streamers/StreamObject'\n// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst JSONStream = require('JSONStream')\nimport progress from 'progress-stream'\n\n// Initialize Firebase (you'll need to set up your service account key)\nadmin.initializeApp({\n  credential: admin.credential.cert(\n    path.join(\n      __dirname,\n      './keys/module-cost-firebase-adminsdk-xcnum-ca64ae80ff.json'\n    )\n  ),\n  databaseURL: 'https://module-cost.firebaseio.com',\n})\nconst db = admin.database()\n\ninterface SearchesV2 {\n  [packageName: string]: {\n    lastSearched: number\n    count: number\n  }\n}\n\nconst searchesV2: SearchesV2 = {}\nconst sixMonthsAgo = Date.now() - 6 * 30 * 24 * 60 * 60 * 1000 // Approximate 6 months in milliseconds\n\nfunction formatETA(seconds: number): string {\n  const sec = Math.floor(seconds % 60)\n  const min = Math.floor((seconds / 60) % 60)\n  const hr = Math.floor(seconds / 3600)\n\n  return [hr, min, sec].map(v => v.toString().padStart(2, '0')).join(':')\n}\n\nasync function processBackupFile(\n  backupFilePath: string,\n  dryRun: boolean\n): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const fileSize = fs.statSync(backupFilePath).size\n    const progressStream = progress({\n      length: fileSize,\n      time: 1000, // Update every second\n    })\n\n    progressStream.on('progress', progressData => {\n      const percentage = progressData.percentage.toFixed(2)\n      const eta = formatETA(progressData.eta)\n      process.stdout.write(\n        `Processing backup file: ${percentage}% | ETA: ${eta}   \\r`\n      )\n    })\n\n    const outputStream = fs.createWriteStream('pruned-module-cost-v2.json')\n    const stringifyStream = JSONStream.stringifyObject()\n    stringifyStream.pipe(outputStream)\n\n    let packageCount = 0\n    let prunedPackageCount = 0\n    let packagesRemoved = 0\n    let versionsRemoved = 0\n    let originalSize = 0\n    let prunedSize = 0\n\n    const pipeline = chain([\n      fs.createReadStream(backupFilePath).pipe(progressStream),\n      parser(),\n      streamObject(),\n    ] as any)\n\n    let isProcessingSearchesV2 = false\n    let isProcessingModuleCostV2 = false\n\n    pipeline.on('data', ({ key, value }) => {\n      if (key === 'searches-v2') {\n        isProcessingSearchesV2 = true\n        processSearchesV2(value).then(() => {\n          isProcessingSearchesV2 = false\n        })\n      } else if (key === 'modules-v2') {\n        if (isProcessingSearchesV2) {\n          // Wait until searches-v2 processing is complete\n          const interval = setInterval(() => {\n            if (!isProcessingSearchesV2) {\n              clearInterval(interval)\n              processModuleCostV2(value)\n            }\n          }, 100)\n        } else {\n          processModuleCostV2(value)\n        }\n      }\n    })\n\n    pipeline.on('end', () => {\n      stringifyStream.end()\n      console.log('\\nProcessing completed.')\n\n      // Print summary\n      console.log('\\nPruning Summary:')\n      console.log(`Original package count: ${packageCount}`)\n      console.log(`Pruned package count: ${prunedPackageCount}`)\n      console.log(`Packages removed: ${packagesRemoved}`)\n      console.log(`Versions removed: ${versionsRemoved}`)\n      console.log(\n        `Original size: ${(originalSize / (1024 * 1024)).toFixed(2)} MB`\n      )\n      console.log(`Pruned size: ${(prunedSize / (1024 * 1024)).toFixed(2)} MB`)\n      console.log(\n        `Size reduction: ${(\n          ((originalSize - prunedSize) / originalSize) *\n          100\n        ).toFixed(2)}%`\n      )\n\n      if (!dryRun) {\n        console.log('Pushing pruned data to Firebase...')\n        uploadPrunedDataToFirebase('pruned-module-cost-v2.json')\n          .then(() => {\n            console.log(\n              'Pruned data has been pushed to module-cost-pruned table'\n            )\n            resolve()\n          })\n          .catch(reject)\n      } else {\n        console.log('Dry run complete. No data has been modified.')\n        resolve()\n      }\n    })\n\n    pipeline.on('error', err => {\n      console.error('Error processing backup file:', err)\n      reject(err)\n    })\n\n    async function processSearchesV2(searchesData: any): Promise<void> {\n      return new Promise(resolve => {\n        const searchesPipeline = chain([streamObject()])\n\n        searchesPipeline.on('data', ({ key, value }) => {\n          searchesV2[key] = value as SearchesV2[string]\n        })\n\n        searchesPipeline.on('end', () => {\n          console.log(\n            `\\nFinished processing searches-v2, total ${\n              Object.keys(searchesV2).length\n            } searches`\n          )\n          resolve()\n        })\n\n        searchesPipeline.write({ key: null, value: searchesData })\n        searchesPipeline.end()\n      })\n    }\n\n    function processModuleCostV2(moduleData: any): void {\n      const modulePipeline = chain([streamObject()])\n\n      modulePipeline.on('data', ({ key: packageName, value: versionsObj }) => {\n        packageCount++\n        originalSize += JSON.stringify(versionsObj).length\n\n        const searchInfo = searchesV2[packageName]\n\n        let action = ''\n        let reason = ''\n\n        // Check if package should be removed based on search criteria\n        if (\n          !searchInfo ||\n          searchInfo.count <= 1 ||\n          searchInfo.lastSearched < sixMonthsAgo\n        ) {\n          packagesRemoved++\n          action = 'Pruned'\n          if (!searchInfo) {\n            reason = 'Not found in searches-v2'\n          } else if (searchInfo.count <= 1) {\n            reason = `Search count (${searchInfo.count}) <= 1`\n          } else if (searchInfo.lastSearched < sixMonthsAgo) {\n            reason = 'Last searched more than 6 months ago'\n          }\n\n          console.log(\n            `Package: ${packageName} | Action: ${action} | Reason: ${reason}`\n          )\n          return\n        }\n\n        // Sort versions and keep only the last 20\n        const sortedVersions = Object.keys(versionsObj).sort((a, b) =>\n          semver.compare(b, a)\n        )\n        const versionsToKeep = sortedVersions.slice(0, 20)\n\n        if (sortedVersions.length > 20) {\n          versionsRemoved += sortedVersions.length - 20\n          action = 'Pruned versions'\n          reason = `Keeping only the latest 20 versions`\n        } else {\n          action = 'Kept'\n          reason = `All versions are within limit`\n        }\n\n        const prunedVersions: { [version: string]: any } = {}\n        for (const version of versionsToKeep) {\n          prunedVersions[version] = versionsObj[version]\n        }\n\n        prunedPackageCount++\n        prunedSize += JSON.stringify(prunedVersions).length\n\n        // Write to output stream\n        ;(stringifyStream as any).write([packageName, prunedVersions])\n\n        console.log(\n          `Package: ${packageName} | Action: ${action} | Reason: ${reason}`\n        )\n      })\n\n      modulePipeline.on('end', () => {\n        // Module-cost-v2 processing done\n      })\n\n      modulePipeline.write({ key: null, value: moduleData })\n      modulePipeline.end()\n    }\n  })\n}\n\nasync function uploadPrunedDataToFirebase(filePath: string) {\n  const prunedRef = db.ref('module-cost-pruned')\n  const readStream = fs.createReadStream(filePath)\n  const parseStream = JSONStream.parse('*')\n  const pipeline = chain([readStream, parseStream] as any)\n\n  let buffer: { [key: string]: any } = {}\n  let count = 0\n\n  return new Promise<void>((resolve, reject) => {\n    pipeline.on('data', ({ key, value }) => {\n      buffer[key] = value\n      count++\n\n      if (count % 1000 === 0) {\n        prunedRef.update(buffer)\n        buffer = {}\n        console.log(`Uploaded ${count} packages to Firebase`)\n      }\n    })\n\n    pipeline.on('end', () => {\n      if (Object.keys(buffer).length > 0) {\n        prunedRef.update(buffer)\n        console.log(`Uploaded remaining packages to Firebase`)\n      }\n      resolve()\n    })\n\n    pipeline.on('error', err => {\n      console.error('Error uploading pruned data to Firebase:', err)\n      reject(err)\n    })\n  })\n}\n\n// Run the script\nconst backupFilePath = process.argv[2]\nconst dryRun = process.argv.includes('--dry-run')\n\nif (!backupFilePath) {\n  console.error('Please provide the path to the backup file as an argument.')\n  process.exit(1)\n}\n\n;(async () => {\n  try {\n    console.log('Starting processing of backup file...')\n    await processBackupFile(backupFilePath, dryRun)\n  } catch (error) {\n    console.error('An error occurred:', error)\n  }\n})()\n"
  },
  {
    "path": "scripts/generate-top-packages.js",
    "content": "/**\n * Script to generate a list of top packages to pre-populate modules-v3\n *\n * This reads from Firebase:\n * - searches-v2: to find popular packages (high count, searched in last 6 months)\n * - modules-v2: to get the latest 20 versions for each package\n *\n * Output: top-packages.json with package name, versions, and priority\n *\n * Features:\n * - Concurrent fetching for speed\n * - Incremental saves to resume from crashes\n * - Proper semver validation (no pre-release/beta versions)\n *\n * Usage: node scripts/generate-top-packages.js\n */\n\nconst path = require('path')\nconst fs = require('fs')\nconst admin = require('firebase-admin')\nconst semver = require('semver')\n\n// Use the same encoding/decoding as the main app\nconst { encodeFirebaseKey, decodeFirebaseKey } = require('../utils/index')\n\n// Initialize Firebase Admin\nconst serviceAccountPath = path.join(\n  __dirname,\n  './keys/module-cost-firebase-adminsdk-xcnum-ca64ae80ff.json'\n)\n\nif (!fs.existsSync(serviceAccountPath)) {\n  console.error(\n    'Firebase service account key not found at:',\n    serviceAccountPath\n  )\n  process.exit(1)\n}\n\nadmin.initializeApp({\n  credential: admin.credential.cert(serviceAccountPath),\n  databaseURL: 'https://module-cost.firebaseio.com',\n})\n\nconst db = admin.database()\n\n// Configuration\nconst SIX_MONTHS_MS = 6 * 30 * 24 * 60 * 60 * 1000\nconst MIN_SEARCH_COUNT = 2 // At least searched twice\nconst MAX_VERSIONS_PER_PACKAGE = 20\nconst TOP_PACKAGES_LIMIT = 1000 // Top N packages by search count\nconst CONCURRENCY = 20 // Number of concurrent Firebase requests\nconst OUTPUT_PATH = path.join(__dirname, '../top-packages.json')\nconst PROGRESS_PATH = path.join(__dirname, '../top-packages-progress.json')\n\n/**\n * Check if a version is a valid, stable semver version (no pre-release tags)\n */\nfunction isValidStableVersion(version) {\n  // Check for prerelease tags in the original version string\n  // This catches things like \"1.0.0-alpha\", \"2.0.0-beta.1\", \"3.0.0-rc.0\"\n  if (version.includes('-')) {\n    return false\n  }\n\n  // Clean and validate the version\n  const cleaned = semver.valid(semver.coerce(version))\n  if (!cleaned) {\n    return false\n  }\n\n  return true\n}\n\n// Load existing progress if available\nfunction loadProgress() {\n  if (fs.existsSync(PROGRESS_PATH)) {\n    try {\n      const data = JSON.parse(fs.readFileSync(PROGRESS_PATH, 'utf8'))\n      console.log(\n        `Resuming from progress file: ${data.results.length} packages already processed`\n      )\n      return data\n    } catch (e) {\n      console.log('Could not load progress file, starting fresh')\n    }\n  }\n  return { processedNames: new Set(), results: [] }\n}\n\n// Save progress incrementally\nfunction saveProgress(processedNames, results) {\n  const data = {\n    processedNames: Array.from(processedNames),\n    results,\n    lastSaved: new Date().toISOString(),\n  }\n  fs.writeFileSync(PROGRESS_PATH, JSON.stringify(data, null, 2))\n}\n\n// Process a batch of packages concurrently\nasync function processBatch(packages, processedNames, results, topPackages) {\n  const promises = packages.map(async pkg => {\n    if (processedNames.has(pkg.name)) {\n      return null // Already processed\n    }\n\n    try {\n      const versionsSnapshot = await db\n        .ref('modules-v2')\n        .child(pkg.encodedName)\n        .once('value')\n\n      const versionsData = versionsSnapshot.val()\n\n      if (versionsData) {\n        // Get all versions, decode them, and filter for valid stable versions\n        const allVersions = Object.keys(versionsData)\n          .map(v => decodeFirebaseKey(v))\n          .filter(v => isValidStableVersion(v))\n\n        if (allVersions.length === 0) {\n          return null // No valid stable versions\n        }\n\n        // Sort by semver (descending - newest first)\n        allVersions.sort((a, b) => {\n          const cleanA = semver.valid(semver.coerce(a))\n          const cleanB = semver.valid(semver.coerce(b))\n          if (cleanA && cleanB) {\n            return semver.compare(cleanB, cleanA) // descending\n          }\n          return 0\n        })\n\n        // Take latest N versions\n        const latestVersions = allVersions.slice(0, MAX_VERSIONS_PER_PACKAGE)\n\n        return {\n          name: pkg.name,\n          versions: latestVersions,\n          searchCount: pkg.count,\n          lastSearched: new Date(pkg.lastSearched).toISOString(),\n          priority: topPackages.findIndex(p => p.name === pkg.name) + 1, // 1-based priority\n        }\n      }\n      return null\n    } catch (err) {\n      console.error(`Error processing package ${pkg.name}:`, err.message)\n      return null\n    }\n  })\n\n  const batchResults = await Promise.all(promises)\n  return batchResults.filter(r => r !== null)\n}\n\nasync function main() {\n  console.log('Fetching searches-v2 data...')\n\n  const sixMonthsAgo = Date.now() - SIX_MONTHS_MS\n\n  // Fetch all searches\n  const searchesSnapshot = await db.ref('searches-v2').once('value')\n  const searchesData = searchesSnapshot.val()\n\n  if (!searchesData) {\n    console.error('No searches data found')\n    process.exit(1)\n  }\n\n  console.log(\n    `Found ${Object.keys(searchesData).length} total packages in searches-v2`\n  )\n\n  // Filter packages: searched at least MIN_SEARCH_COUNT times AND within last 6 months\n  const eligiblePackages = []\n\n  for (const [encodedName, data] of Object.entries(searchesData)) {\n    const { count, lastSearched } = data\n\n    if (count >= MIN_SEARCH_COUNT && lastSearched >= sixMonthsAgo) {\n      eligiblePackages.push({\n        encodedName,\n        name: decodeFirebaseKey(encodedName),\n        count,\n        lastSearched,\n      })\n    }\n  }\n\n  console.log(\n    `Found ${eligiblePackages.length} eligible packages (count >= ${MIN_SEARCH_COUNT}, searched in last 6 months)`\n  )\n\n  // Sort by count (descending) and take top N\n  eligiblePackages.sort((a, b) => b.count - a.count)\n  const topPackages = eligiblePackages.slice(0, TOP_PACKAGES_LIMIT)\n\n  console.log(\n    `Processing top ${topPackages.length} packages with concurrency ${CONCURRENCY}...`\n  )\n\n  // Load existing progress\n  const progress = loadProgress()\n  const processedNames = new Set(progress.processedNames || [])\n  const results = progress.results || []\n\n  // Process in batches\n  let processed = processedNames.size\n\n  for (let i = 0; i < topPackages.length; i += CONCURRENCY) {\n    const batch = topPackages.slice(i, i + CONCURRENCY)\n    const unprocessedBatch = batch.filter(p => !processedNames.has(p.name))\n\n    if (unprocessedBatch.length === 0) {\n      continue // All already processed\n    }\n\n    const batchResults = await processBatch(\n      unprocessedBatch,\n      processedNames,\n      results,\n      topPackages\n    )\n\n    // Add results and mark as processed\n    for (const result of batchResults) {\n      results.push(result)\n      processedNames.add(result.name)\n    }\n\n    // Also mark failed ones as processed to avoid retrying\n    for (const pkg of unprocessedBatch) {\n      processedNames.add(pkg.name)\n    }\n\n    processed = processedNames.size\n    console.log(\n      `Processed ${processed}/${topPackages.length} packages (${results.length} with valid versions)`\n    )\n\n    // Save progress after each batch\n    saveProgress(processedNames, results)\n  }\n\n  console.log(\n    `\\nCompleted! Found ${results.length} packages with valid stable versions`\n  )\n\n  // Sort results by priority\n  results.sort((a, b) => a.priority - b.priority)\n\n  // Write final output\n  fs.writeFileSync(OUTPUT_PATH, JSON.stringify(results, null, 2))\n  console.log(`Written to ${OUTPUT_PATH}`)\n\n  // Cleanup progress file\n  if (fs.existsSync(PROGRESS_PATH)) {\n    fs.unlinkSync(PROGRESS_PATH)\n    console.log('Cleaned up progress file')\n  }\n\n  // Print summary\n  console.log('\\n=== Summary ===')\n  console.log(`Total packages: ${results.length}`)\n  console.log(\n    `Total versions: ${results.reduce((sum, p) => sum + p.versions.length, 0)}`\n  )\n  console.log('\\nTop 10 packages by search count:')\n  results.slice(0, 10).forEach((pkg, i) => {\n    console.log(\n      `  ${i + 1}. ${pkg.name} (${pkg.searchCount} searches, ${\n        pkg.versions.length\n      } versions)`\n    )\n    console.log(`     Latest versions: ${pkg.versions.slice(0, 5).join(', ')}`)\n  })\n\n  // Cleanup\n  await admin.app().delete()\n  process.exit(0)\n}\n\nmain().catch(err => {\n  console.error('Fatal error:', err)\n  process.exit(1)\n})\n"
  },
  {
    "path": "scripts/package.json",
    "content": "{\n  \"name\": \"scripts\",\n  \"packageManager\": \"yarn@4.0.2\"\n}\n"
  },
  {
    "path": "scripts/populate-v3.js",
    "content": "/**\n * Script to pre-populate modules-v3 by building top packages\n *\n * Prerequisites:\n * - Ensure services are running (yarn dev or pm2)\n * - Ensure FIREBASE_WRITE_KEY=modules-v3 in environment\n *\n * This script:\n * - Reads top-packages.json\n * - Builds each package@version by calling the API\n * - Tracks progress for resumability\n * - Runs with configurable concurrency\n *\n * Usage: node scripts/populate-v3.js [--concurrency=N] [--reset]\n */\n\nconst path = require('path')\nconst fs = require('fs')\nconst axios = require('axios')\n\n// Configuration\nconst API_BASE = process.env.API_BASE || 'http://localhost:5000'\nconst CONCURRENCY = parseInt(process.env.CONCURRENCY || '3', 10)\nconst TIMEOUT_MS = 120000 // 120 seconds per request\nconst TOP_PACKAGES_PATH = path.join(__dirname, '../top-packages.json')\nconst PROGRESS_PATH = path.join(__dirname, '../populate-v3-progress.json')\nconst STATS_PATH = path.join(__dirname, '../populate-v3-stats.json')\nconst COMPARISON_PATH = path.join(__dirname, '../populate-v3-comparison.json')\n\nconst EXPORTS_STATS_PATH = path.join(\n  __dirname,\n  '../populate-v3-exports-stats.json'\n)\nconst EXPORTS_COMPARISON_PATH = path.join(\n  __dirname,\n  '../populate-v3-exports-comparison.json'\n)\n\nconst CACHE_SERVICE_BASE =\n  process.env.CACHE_SERVICE_BASE || 'http://localhost:7001'\n\n// Parse command line args\nconst args = process.argv.slice(2)\nconst shouldReset = args.includes('--reset')\nconst concurrencyArg = args.find(a => a.startsWith('--concurrency='))\nconst concurrency = concurrencyArg\n  ? parseInt(concurrencyArg.split('=')[1], 10)\n  : CONCURRENCY\n\nconst limitArg = args.find(a => a.startsWith('--limit-packages='))\nconst packageLimit = limitArg ? parseInt(limitArg.split('=')[1], 10) : Infinity\n\nconst packageFilterArg = args.find(a => a.startsWith('--package='))\nconst packageFilter = packageFilterArg ? packageFilterArg.split('=')[1] : null\n\nconst sizesOnly = args.includes('--sizes-only')\nconst exportsOnly = args.includes('--exports-only')\n\n// Validate flags\nif (sizesOnly && exportsOnly) {\n  console.error(\n    'Error: Cannot use both --sizes-only and --exports-only flags together'\n  )\n  process.exit(1)\n}\n\nconsole.log(`\n=== Populate modules-v3 ===\nAPI: ${API_BASE}\nConcurrency: ${concurrency}\nPackage Limit: ${packageLimit}\nPackage Filter: ${packageFilter || 'None'}\nReset: ${shouldReset}\nMode: ${\n  sizesOnly\n    ? 'Sizes Only'\n    : exportsOnly\n    ? 'Exports Only'\n    : 'Both (Sizes + Exports)'\n}\n`)\n\n// Load progress\nfunction loadProgress() {\n  // Only full reset if requested AND no package filter\n  if (shouldReset && !packageFilter) {\n    console.log('Resetting ALL progress...')\n    return {\n      completed: new Set(),\n      completed_exports: new Set(),\n      failed: new Set(),\n      stats: {\n        success: 0,\n        failed: 0,\n        skipped: 0,\n        exports_success: 0,\n        exports_failed: 0,\n      },\n    }\n  }\n\n  if (fs.existsSync(PROGRESS_PATH)) {\n    try {\n      const data = JSON.parse(fs.readFileSync(PROGRESS_PATH, 'utf8'))\n      console.log(\n        `Resuming: ${data.completed.length} sizes completed, ${\n          data.completed_exports?.length || 0\n        } exports completed, ${data.failed.length} failed`\n      )\n\n      const stats = data.stats || {}\n      return {\n        completed: new Set(data.completed),\n        completed_exports: new Set(data.completed_exports || []),\n        failed: new Set(data.failed),\n        stats: {\n          success: stats.success || 0,\n          failed: stats.failed || 0,\n          skipped: stats.skipped || 0,\n          exports_success: stats.exports_success || 0,\n          exports_failed: stats.exports_failed || 0,\n        },\n      }\n    } catch (e) {\n      console.log('Could not load progress, starting fresh')\n    }\n  }\n  return {\n    completed: new Set(),\n    completed_exports: new Set(),\n    failed: new Set(),\n    stats: {\n      success: 0,\n      failed: 0,\n      skipped: 0,\n      exports_success: 0,\n      exports_failed: 0,\n    },\n  }\n}\n\n// Save progress\nfunction saveProgress(progress) {\n  const data = {\n    completed: Array.from(progress.completed),\n    completed_exports: Array.from(progress.completed_exports),\n    failed: Array.from(progress.failed),\n    stats: progress.stats,\n    lastSaved: new Date().toISOString(),\n  }\n  fs.writeFileSync(PROGRESS_PATH, JSON.stringify(data, null, 2))\n}\n\n// Save detailed stats\nfunction saveStats(stats) {\n  fs.writeFileSync(STATS_PATH, JSON.stringify(stats, null, 2))\n}\n\n// Save comparisons\nfunction saveComparisons(comparisons) {\n  fs.writeFileSync(COMPARISON_PATH, JSON.stringify(comparisons, null, 2))\n}\n\n// Save exports stats\nfunction saveExportsStats(stats) {\n  fs.writeFileSync(EXPORTS_STATS_PATH, JSON.stringify(stats, null, 2))\n}\n\n// Save exports comparisons\nfunction saveExportsComparisons(comparisons) {\n  fs.writeFileSync(\n    EXPORTS_COMPARISON_PATH,\n    JSON.stringify(comparisons, null, 2)\n  )\n}\n\n// Timeout wrapper that ABORTS the underlying request\nfunction withAbortableTimeout(promiseFactory, timeoutMs, timeoutValue) {\n  const controller = new AbortController()\n  let timeoutId\n\n  const timeoutPromise = new Promise(resolve => {\n    timeoutId = setTimeout(() => {\n      console.log(`[WATCHDOG] Aborting request after ${timeoutMs}ms`)\n      controller.abort()\n      resolve(timeoutValue)\n    }, timeoutMs)\n  })\n\n  const workPromise = promiseFactory(controller.signal)\n    .then(result => {\n      clearTimeout(timeoutId)\n      return result\n    })\n    .catch(err => {\n      clearTimeout(timeoutId)\n      if (err.name === 'CanceledError' || err.name === 'AbortError') {\n        return timeoutValue\n      }\n      throw err\n    })\n\n  return Promise.race([workPromise, timeoutPromise])\n}\n\n// Build a single package version\nasync function buildPackage(packageName, version, signal = null) {\n  const key = `${packageName}@${version}`\n\n  // 1. Fetch v2 data from cache-service (short timeout, no external signal)\n  let v2Result = null\n  try {\n    const v2Response = await axios.get(`${CACHE_SERVICE_BASE}/package-cache`, {\n      params: { name: packageName, version: version, readKey: 'modules-v2' },\n      timeout: 10000,\n    })\n    if (v2Response.data && v2Response.data.size) {\n      v2Result = { size: v2Response.data.size, gzip: v2Response.data.gzip }\n    }\n  } catch (err) {\n    // v2 doesn't exist or request failed\n  }\n\n  // 2. Fetch v3 data (trigger build) - use external signal for abort\n  const url = `${API_BASE}/api/size?package=${encodeURIComponent(\n    key\n  )}&record=true&force=true`\n\n  try {\n    const response = await axios.get(url, {\n      timeout: TIMEOUT_MS,\n      signal: signal, // Use the passed signal for abort\n    })\n    if (response.data && response.data.size) {\n      return {\n        success: true,\n        size: response.data.size,\n        gzip: response.data.gzip,\n        v2: v2Result,\n      }\n    }\n    return { success: false, error: 'No size in response', v2: v2Result }\n  } catch (err) {\n    if (err.name === 'CanceledError' || err.name === 'AbortError') {\n      return { success: false, error: 'Request aborted', v2: v2Result }\n    }\n    const errorMsg =\n      err.response?.data?.error?.code ||\n      err.response?.data?.error?.message ||\n      err.message ||\n      'Unknown Error'\n    return { success: false, error: errorMsg, v2: v2Result }\n  }\n}\n\nasync function buildExports(packageName, version, signal = null) {\n  const key = `${packageName}@${version}`\n\n  // 1. Fetch v2 data from cache-service (short timeout, no external signal)\n  let v2Result = null\n  try {\n    const v2Response = await axios.get(`${CACHE_SERVICE_BASE}/exports-cache`, {\n      params: { name: packageName, version: version, readKey: 'exports' },\n      timeout: 10000,\n    })\n    if (v2Response.data) {\n      v2Result = v2Response.data\n    }\n  } catch (err) {\n    // v2 doesn't exist or request failed\n  }\n\n  // 2. Fetch v3 data (trigger build) - use external signal for abort\n  const url = `${API_BASE}/api/exports-sizes?package=${encodeURIComponent(\n    key\n  )}&force=true`\n\n  try {\n    const response = await axios.get(url, {\n      timeout: TIMEOUT_MS,\n      signal: signal, // Use the passed signal for abort\n    })\n    if (response.data) {\n      return {\n        success: true,\n        data: response.data,\n        v2: v2Result,\n      }\n    }\n    return { success: false, error: 'No data in response', v2: v2Result }\n  } catch (err) {\n    if (err.name === 'CanceledError' || err.name === 'AbortError') {\n      return { success: false, error: 'Request aborted', v2: v2Result }\n    }\n    const errorMsg =\n      err.response?.data?.error?.code ||\n      err.response?.data?.error?.message ||\n      err.message ||\n      'Unknown Error'\n    return { success: false, error: errorMsg, v2: v2Result }\n  }\n}\n\n// Process a batch of package versions in parallel (with abort handling to prevent hangs)\nasync function processBatch(\n  batch,\n  progress,\n  detailedStats,\n  comparisons,\n  exportsStats,\n  exportsComparisons\n) {\n  const promises = batch.map(async ({ packageName, version }) => {\n    const key = `${packageName}@${version}`\n\n    // Determine what to build based on flags and completion status\n    const shouldBuildSize = !exportsOnly && !progress.completed.has(key)\n    const shouldBuildExports =\n      !sizesOnly && !progress.completed_exports.has(key)\n\n    if (!shouldBuildSize && !shouldBuildExports) {\n      progress.stats.skipped++\n      return { key, skipped: true }\n    }\n\n    const startTime = Date.now()\n    let sizeResult = { success: true, skipped: true }\n    let exportsResult = { success: true, skipped: true }\n\n    // Run size and exports in parallel for THIS package (different endpoints)\n    const tasks = []\n    if (shouldBuildSize) {\n      console.log(`[DEBUG] Building size for ${key}`)\n      tasks.push(\n        withAbortableTimeout(\n          signal => buildPackage(packageName, version, signal),\n          TIMEOUT_MS + 5000,\n          { success: false, error: 'Operation timeout (aborted)' }\n        ).then(res => {\n          sizeResult = res\n        })\n      )\n    }\n    if (shouldBuildExports) {\n      console.log(`[DEBUG] Building exports for ${key}`)\n      tasks.push(\n        withAbortableTimeout(\n          signal => buildExports(packageName, version, signal),\n          TIMEOUT_MS + 5000,\n          { success: false, error: 'Operation timeout (aborted)' }\n        ).then(res => {\n          exportsResult = res\n        })\n      )\n    }\n\n    if (tasks.length > 0) {\n      await Promise.all(tasks)\n    }\n\n    console.log(\n      `[DEBUG] Completed ${key} in ${((Date.now() - startTime) / 1000).toFixed(\n        1\n      )}s`\n    )\n    const duration = ((Date.now() - startTime) / 1000).toFixed(1)\n    const finalResult = { key, duration }\n\n    if (shouldBuildSize) {\n      if (sizeResult.success) {\n        progress.completed.add(key)\n        progress.stats.success++\n        detailedStats.push({\n          package: key,\n          status: 'success',\n          size: sizeResult.size,\n          gzip: sizeResult.gzip,\n          duration: parseFloat(duration),\n          timestamp: new Date().toISOString(),\n        })\n\n        if (sizeResult.v2) {\n          comparisons.push({\n            package: key,\n            v2_size: sizeResult.v2.size,\n            v3_size: sizeResult.size,\n            v2_gzip: sizeResult.v2.gzip,\n            v3_gzip: sizeResult.gzip,\n            size_diff: sizeResult.size - sizeResult.v2.size,\n            gzip_diff: sizeResult.gzip - sizeResult.v2.gzip,\n            timestamp: new Date().toISOString(),\n          })\n        }\n        finalResult.success = true\n      } else {\n        progress.failed.add(key)\n        progress.stats.failed++\n        detailedStats.push({\n          package: key,\n          status: 'failed',\n          error: sizeResult.error,\n          duration: parseFloat(duration),\n          timestamp: new Date().toISOString(),\n        })\n        finalResult.error = sizeResult.error\n      }\n    }\n\n    if (shouldBuildExports) {\n      if (exportsResult.success) {\n        progress.completed_exports.add(key)\n        progress.stats.exports_success++\n        exportsStats.push({\n          package: key,\n          status: 'success',\n          data: exportsResult.data,\n          duration: parseFloat(duration),\n          timestamp: new Date().toISOString(),\n        })\n\n        if (exportsResult.v2) {\n          exportsComparisons.push({\n            package: key,\n            v2: exportsResult.v2,\n            v3: exportsResult.data,\n            timestamp: new Date().toISOString(),\n          })\n        }\n      } else {\n        progress.stats.exports_failed++\n        exportsStats.push({\n          package: key,\n          status: 'failed',\n          error: exportsResult.error,\n          duration: parseFloat(duration),\n          timestamp: new Date().toISOString(),\n        })\n        finalResult.exports_error = exportsResult.error\n      }\n    }\n\n    return {\n      key,\n      duration,\n      size: shouldBuildSize ? sizeResult : null,\n      exports: shouldBuildExports ? exportsResult : null,\n    }\n  })\n\n  return Promise.all(promises)\n}\n\n// Format time\nfunction formatTime(seconds) {\n  const h = Math.floor(seconds / 3600)\n  const m = Math.floor((seconds % 3600) / 60)\n  const s = Math.floor(seconds % 60)\n  return `${h}h ${m}m ${s}s`\n}\n\nasync function main() {\n  // Load packages\n  if (!fs.existsSync(TOP_PACKAGES_PATH)) {\n    console.error(\n      'top-packages.json not found. Run generate-top-packages.js first.'\n    )\n    process.exit(1)\n  }\n\n  const packages = JSON.parse(fs.readFileSync(TOP_PACKAGES_PATH, 'utf8'))\n  console.log(`Loaded ${packages.length} packages from top-packages.json`)\n\n  // Limit packages if specified\n  let targetPackages = packages\n  if (packageFilter) {\n    targetPackages = packages.filter(p => p.name === packageFilter)\n    console.log(\n      `Filtering to package: ${packageFilter} (found ${targetPackages.length} matches)`\n    )\n  }\n\n  targetPackages = targetPackages.slice(0, packageLimit)\n  if (packageLimit !== Infinity) {\n    console.log(`Limiting to top ${packageLimit} packages`)\n  }\n\n  // Flatten to package@version pairs, ordered by priority\n  const allVersions = []\n  for (const pkg of targetPackages) {\n    for (const version of pkg.versions) {\n      allVersions.push({\n        packageName: pkg.name,\n        version,\n        priority: pkg.priority,\n      })\n    }\n  }\n\n  console.log(`Total versions to process: ${allVersions.length}`)\n\n  console.log('Loading progress and stats files...')\n  // Load progress\n  const progress = loadProgress()\n  let detailedStats = []\n  let comparisons = []\n  let exportsStats = []\n  let exportsComparisons = []\n\n  // Load existing stats unless doing a full global reset\n  if (!(shouldReset && !packageFilter)) {\n    if (fs.existsSync(STATS_PATH)) {\n      try {\n        detailedStats = JSON.parse(fs.readFileSync(STATS_PATH, 'utf8'))\n      } catch (e) {}\n    }\n    if (fs.existsSync(COMPARISON_PATH)) {\n      try {\n        comparisons = JSON.parse(fs.readFileSync(COMPARISON_PATH, 'utf8'))\n      } catch (e) {}\n    }\n    if (fs.existsSync(EXPORTS_STATS_PATH)) {\n      try {\n        exportsStats = JSON.parse(fs.readFileSync(EXPORTS_STATS_PATH, 'utf8'))\n      } catch (e) {}\n    }\n    if (fs.existsSync(EXPORTS_COMPARISON_PATH)) {\n      try {\n        exportsComparisons = JSON.parse(\n          fs.readFileSync(EXPORTS_COMPARISON_PATH, 'utf8')\n        )\n      } catch (e) {}\n    }\n  }\n\n  // If we are filtering by package and requested a reset, clear pertinent data only\n  if (shouldReset && packageFilter) {\n    console.log(`Clearing existing data for package: ${packageFilter}`)\n    // Clean Sets\n    const keysToRemove = []\n    progress.completed.forEach(key => {\n      if (key.startsWith(packageFilter + '@')) keysToRemove.push(key)\n    })\n    progress.completed_exports.forEach(key => {\n      if (key.startsWith(packageFilter + '@')) keysToRemove.push(key)\n    })\n    progress.failed.forEach(key => {\n      if (key.startsWith(packageFilter + '@')) keysToRemove.push(key)\n    })\n\n    keysToRemove.forEach(key => {\n      progress.completed.delete(key)\n      progress.completed_exports.delete(key)\n      progress.failed.delete(key)\n    })\n\n    // Clean Arrays\n    detailedStats = detailedStats.filter(\n      item => !item.package.startsWith(packageFilter + '@')\n    )\n    comparisons = comparisons.filter(\n      item => !item.package.startsWith(packageFilter + '@')\n    )\n    exportsStats = exportsStats.filter(\n      item => !item.package.startsWith(packageFilter + '@')\n    )\n    exportsComparisons = exportsComparisons.filter(\n      item => !item.package.startsWith(packageFilter + '@')\n    )\n  }\n\n  console.log('Checking API reachability...')\n  // Check API is reachable\n  try {\n    await axios.get(`${API_BASE}/api/recent?limit=1`, { timeout: 10000 })\n    console.log('API is reachable')\n  } catch (err) {\n    console.error(`Cannot reach API at ${API_BASE}. Error: ${err.message}`)\n    console.error('Make sure the server is running.')\n    process.exit(1)\n  }\n\n  const startTime = Date.now()\n  let processed = 0\n\n  console.log('\\nStarting builds...\\n')\n\n  // Process in batches\n  for (let i = 0; i < allVersions.length; i += concurrency) {\n    const batch = allVersions.slice(i, i + concurrency)\n    console.log(\n      `\\n[DEBUG] Processing batch ${\n        Math.floor(i / concurrency) + 1\n      }, packages: ${batch\n        .map(b => b.packageName + '@' + b.version)\n        .join(', ')}`\n    )\n    const results = await processBatch(\n      batch,\n      progress,\n      detailedStats,\n      comparisons,\n      exportsStats,\n      exportsComparisons\n    )\n    console.log(`[DEBUG] Batch completed, got ${results.length} results`)\n\n    // Log results\n    for (const result of results) {\n      const parts = []\n      let symbol = '✓'\n\n      if (!result.size && !result.exports) {\n        // Skipped completely\n        continue\n      }\n\n      if (result.size) {\n        if (result.size.success) {\n          parts.push(`Size: ${(result.size.size / 1024).toFixed(1)}kB`)\n        } else {\n          symbol = '✗'\n          parts.push(`Size: Failed (${result.size.error})`)\n        }\n      }\n\n      if (result.exports) {\n        if (result.exports.success) {\n          parts.push(`Exports: OK`)\n        } else {\n          // Only mark as failure (cross) if exports failed and it wasn't just a missing size failure (which usually cascades)\n          if (symbol === '✓') symbol = '⚠'\n          parts.push(`Exports: Failed (${result.exports.error})`)\n        }\n      }\n\n      console.log(\n        `${symbol} ${result.key} (${result.duration}s) - ${parts.join(', ')}`\n      )\n    }\n\n    processed = Math.min(i + concurrency, allVersions.length)\n    // ... (rest of stats calculation) ...\n    const elapsed = (Date.now() - startTime) / 1000\n    const rate = (progress.stats.success + progress.stats.failed) / elapsed\n    const remaining = (allVersions.length - processed) / (rate || 1)\n\n    // Progress update\n    console.log(\n      `\\n[${processed}/${allVersions.length}] Sizes (S: ${progress.stats.success}, F: ${progress.stats.failed}), Exports (S: ${progress.stats.exports_success}, F: ${progress.stats.exports_failed}), Skipped: ${progress.stats.skipped}`\n    )\n    console.log(\n      `Elapsed: ${formatTime(elapsed)}, ETA: ${formatTime(remaining)}\\n`\n    )\n\n    // Save progress after each batch\n    saveProgress(progress)\n    saveStats(detailedStats)\n    saveComparisons(comparisons)\n    saveExportsStats(exportsStats)\n    saveExportsComparisons(exportsComparisons)\n  }\n\n  // Final summary\n  const totalTime = (Date.now() - startTime) / 1000\n  console.log('\\n=== Completed ===')\n  console.log(`Total time: ${formatTime(totalTime)}`)\n  console.log(`Success: ${progress.stats.success}`)\n  console.log(`Failed: ${progress.stats.failed}`)\n  console.log(`Skipped: ${progress.stats.skipped}`)\n  console.log(`\\nProgress saved to: ${PROGRESS_PATH}`)\n  console.log(`Stats saved to: ${STATS_PATH}`)\n  console.log(`Comparison saved to: ${COMPARISON_PATH}`)\n  console.log(`Exports Stats saved to: ${EXPORTS_STATS_PATH}`)\n  console.log(`Exports Comparison saved to: ${EXPORTS_COMPARISON_PATH}`)\n\n  // Cleanup progress if all done successfully\n  if (progress.stats.failed === 0 && progress.stats.success > 0) {\n    console.log('\\nAll packages built successfully!')\n  } else if (progress.stats.failed > 0) {\n    console.log(`\\n${progress.stats.failed} packages failed. Re-run to retry.`)\n  }\n\n  process.exit(0)\n}\n\n// Handle interrupts gracefully\nprocess.on('SIGINT', () => {\n  console.log('\\n\\nInterrupted! Progress has been saved.')\n  process.exit(0)\n})\n\nmain().catch(err => {\n  console.error('Fatal error:', err)\n  process.exit(1)\n})\n"
  },
  {
    "path": "server/CustomError.js",
    "content": "/**\n * Wraps the original error with a identifiable\n * name.\n */\n// Use ES6 supported by Node v6.10 only!\n\nmodule.exports = function CustomError(name, originalError, extra) {\n  Error.captureStackTrace(this, this.constructor)\n  this.name = name\n  this.originalError = originalError\n  this.extra = extra\n}\n"
  },
  {
    "path": "server/Logger.js",
    "content": "const winston = require('winston')\n// const StatsD = require('hot-shots')\n// const WinstonGraylog2 = require('winston-graylog2')\n//\n// const statsClient = new StatsD({\n//   port: 8086,\n//   globalTags: { env: process.env.NODE_ENV },\n//   errorHandler: (err, a) => console.error('error', err, a),\n//   telegraf: true,\n// })\n//\n// const graylogOptions = {\n//   name: 'graylog',\n//   level: 'debug',\n//   silent: false,\n//   handleExceptions: false,\n//   graylog: {\n//     servers: [\n//       { host: process.env.GRAYLOG_HOST, port: process.env.GRAYLOG_PORT },\n//     ],\n//     hostname: 'bundlephobia',\n//     bufferSize: 1400,\n//   },\n// }\n\nconst logFormat = winston.format.printf(function (info) {\n  let date = new Date().toISOString()\n  return `${date} ${info.level}: ${info.message}`\n})\n\nclass Logger {\n  constructor() {\n    let transports = [\n      new winston.transports.Console({\n        format: winston.format.combine(winston.format.colorize(), logFormat),\n      }),\n    ]\n\n    this.logger = winston.createLogger({\n      transports,\n    })\n  }\n\n  info(tag, json, message) {\n    this.logger.info(message, {\n      metadata: {\n        message,\n        tag,\n        ...json,\n      },\n    })\n  }\n\n  error(tag, json, message) {\n    this.logger.error(message, {\n      metadata: {\n        tag,\n        ...json,\n      },\n    })\n  }\n\n  // statsd methods\n\n  increment(label) {\n    // statsClient.increment(label)\n  }\n\n  decrement(label) {\n    // statsClient.increment(label)\n  }\n\n  histogram(label, value) {\n    // statsClient.histogram(label, value)\n  }\n\n  set(label, value) {\n    // statsClient.set(label, value)\n  }\n\n  timing(label, value) {\n    // statsClient.timing(label, value)\n  }\n}\n\nmodule.exports = new Logger()\n"
  },
  {
    "path": "server/Queue.js",
    "content": "const log = require('debug')('bp:queue')\n\nconst Job = {\n  status: {\n    READY: Symbol('ready'),\n    PROCESSING: Symbol('processing'),\n  },\n\n  priority: {\n    LOW: 5,\n    MEDIUM: 10,\n    HIGH: 20,\n  },\n}\n\n/**\n * A simple promise based throttle queue with support\n * for priorities, concurrency control, job de-duplication,\n * and more.\n */\nclass Queue {\n  constructor(options) {\n    this.jobs = []\n    this.options = {\n      concurrency: 1,\n      aging: true,\n      maxAge: Number.POSITIVE_INFINITY,\n      ...options,\n    }\n    this.executorMap = {}\n  }\n\n  /**\n   * Set a function (async or sync) that\n   * executes the jobs sent to the queue.\n   * The handler will be passed a params object.\n   * @param jobType\n   * @param handler\n   */\n  addExecutor(jobType, handler) {\n    this.executorMap[jobType] = handler\n  }\n\n  hasJob(id, type) {\n    return this.jobs.some(job => job.id === id && type === job.type)\n  }\n\n  getRunningJobs() {\n    return this.jobs.filter(job => job.status === Job.status.PROCESSING)\n  }\n\n  getReadyJobs() {\n    return this.jobs.filter(job => job.status === Job.status.READY)\n  }\n\n  /**\n   * Remove \"ready\" jobs from the queue that have\n   * lived past their max age. Removed jobs have\n   * their failure listeners notified of the same.\n   */\n  pruneQueue() {\n    this.getReadyJobs().forEach(job => {\n      const { addedTime, maxAge, failureListeners } = job\n      const isJobExpired = (addedTime.getTime() + maxAge) * 1000 < Date.now()\n\n      if (isJobExpired) {\n        failureListeners.forEach(listener => {\n          listener({\n            code: 'JOB_EXPIRED',\n            message:\n              \"This job's age exceeded it's specified maxAge, and was dropped\",\n          })\n        })\n      }\n    })\n  }\n\n  /**\n   * Get the next \"ready\" job from the queue\n   * to be executed. The next ready job depends on -\n   * 1) Priority: higher priority always executes first\n   * 2) Time Added: if priorities are equal, the older\n   * job executes first.\n   */\n  getNextJobToRun() {\n    return this.jobs\n      .filter(job => job.status === Job.status.READY)\n      .sort((jobA, jobB) => {\n        const priorityDiff = jobB.priority - jobA.priority\n        if (priorityDiff) {\n          return jobB.priority - jobA.priority\n        }\n        return jobA.addedTime.getTime() - jobB.addedTime.getTime()\n      })\n      .shift()\n  }\n\n  /**\n   * Increments the priorites of \"ready\" jobs\n   * in the queue to prevent starvation of lower\n   * priority jobs.\n   */\n  ageJobs() {\n    this.jobs\n      .filter(job => job.status === Job.status.READY)\n      .forEach(job => {\n        job.priority += 1\n      })\n    log(\n      'after aging, job queue is... %o',\n      this.jobs.map(({ id, type, priority }) => ({ id, type, priority }))\n    )\n  }\n\n  removeJob(id, type) {\n    this.jobs = this.jobs.filter(job => job.id !== id && job.type !== type)\n  }\n\n  /**\n   * Clear the queue of all \"ready\" jobs. Jobs\n   * in execution are not terminate prematurely.\n   * Jobs being terminated have their failure listeners notified.\n   */\n  clear() {\n    this.jobs\n      .filter(job => job.status === Job.status.READY)\n      .forEach(job => {\n        job.failureListeners.forEach(failureListener => {\n          failureListener({\n            code: 'QUEUE_CLEARED',\n            message: 'This job was terminated since the queue was cleared',\n            job,\n          })\n        })\n      })\n    this.jobs = []\n  }\n\n  setJobToProcessing(id, type) {\n    this.jobs.forEach(job => {\n      if (job.id === id && job.type === type) {\n        job.status = Job.status.PROCESSING\n      }\n    })\n  }\n\n  /**\n   * Executes the next job in queue iff\n   * they haven't expired and concurrency\n   * limit is not already achieved.\n   */\n  executeNextJobIfPossible() {\n    if (!this.getReadyJobs().length) {\n      log('all done. job queue is empty')\n      return\n    }\n\n    if (this.getRunningJobs().length < this.options.concurrency) {\n      if (this.options.aging) {\n        this.ageJobs()\n      }\n      this.pruneQueue()\n      this.executeNextJob()\n    } else {\n      log('waiting... all workers ain quere are occupied')\n    }\n  }\n\n  executeNextJob() {\n    const nextJob = this.getNextJobToRun()\n    log('executing job ... %o', {\n      id: nextJob.id,\n      type: nextJob.type,\n      priority: nextJob.priority,\n    })\n\n    this.setJobToProcessing(nextJob.id, nextJob.type)\n    const callResult = this.executorMap[nextJob.type].call(this, nextJob.params)\n\n    Promise.resolve(callResult)\n      .then(result => {\n        log('job %s was a success, removing it', nextJob.id, nextJob.type)\n        nextJob.successListeners.forEach(listener => {\n          listener.call(this, result)\n        })\n\n        this.removeJob(nextJob.id, nextJob.type)\n        this.executeNextJobIfPossible()\n      })\n      .catch(err => {\n        log('job %s was a failure, removing it', nextJob.id, nextJob.type)\n        nextJob.failureListeners.forEach(listener => {\n          listener.call(this, err)\n        })\n\n        this.removeJob(nextJob.id, nextJob.type)\n        this.executeNextJobIfPossible()\n      })\n  }\n\n  addListenersToJob(id, type, { resolve, reject }) {\n    this.jobs.forEach(job => {\n      if (job.id === id && job.type === type) {\n        job.successListeners.push(resolve)\n        job.failureListeners.push(reject)\n      }\n    })\n  }\n\n  /**\n   *\n   * @param id\n   * @param type\n   * @param jobParams\n   * @param options\n   * @return {Promise<any>}\n   */\n  process(id, type, jobParams, options = {}) {\n    log('added new job %s %o %o', type, jobParams, options)\n    const {\n      priority = Job.priority.LOW,\n      maxAge = this.options.maxAge,\n      onSuccess = () => {},\n      onFailure = () => {},\n    } = options\n    this.pruneQueue()\n\n    return new Promise((resolve, reject) => {\n      // If a job with the given id already exists,\n      // just add the success / failure callbacks to\n      // that existing job (dedup)\n      if (this.hasJob(id, type)) {\n        log('job id %s already present, adding callbacks', id)\n        this.addListenersToJob(id, type, { resolve, reject })\n        return\n      }\n\n      this.jobs.push({\n        id,\n        type,\n        maxAge,\n        priority,\n        addedTime: new Date(),\n        status: Job.status.READY,\n        params: jobParams,\n        successListeners: [resolve, onSuccess],\n        failureListeners: [reject, onFailure],\n      })\n\n      this.executeNextJobIfPossible()\n    })\n  }\n}\n\nQueue.priority = Job.priority\n\nmodule.exports = Queue\n"
  },
  {
    "path": "server/api/BuildService.js",
    "content": "const axios = require('axios')\nconst { requestQueue } = require('../init')\nconst CONFIG = require('../config')\nconst { pool } = require('../init')\nconst CustomError = require('../CustomError')\nconst debug = require('debug')('bp:build')\n\nconst OpertationType = {\n  PACKAGE_BUILD_STATS: 'PACKAGE_BUILD_STATS',\n  PACKAGE_EXPORTS: 'PACKAGE_EXPORTS',\n  PACKAGE_EXPORTS_SIZES: 'PACKAGE_EXPORTS_SIZES',\n}\n\nclass BuildService {\n  constructor() {\n    const operations = [\n      {\n        type: OpertationType.PACKAGE_BUILD_STATS,\n        endpoint: '/size',\n        methodName: 'getPackageStats',\n      },\n      {\n        type: OpertationType.PACKAGE_EXPORTS,\n        endpoint: '/exports',\n        methodName: 'getAllPackageExports',\n      },\n      {\n        type: OpertationType.PACKAGE_EXPORTS_SIZES,\n        endpoint: '/exports-sizes',\n        methodName: 'getPackageExportSizes',\n      },\n    ]\n\n    operations.forEach(operation => {\n      requestQueue.addExecutor(operation.type, async ({ packageString }) => {\n        if (process.env.BUILD_SERVICE_ENDPOINT) {\n          try {\n            const response = await axios.get(\n              `${process.env.BUILD_SERVICE_ENDPOINT}${\n                operation.endpoint\n              }?p=${encodeURIComponent(packageString)}`\n            )\n            return response.data\n          } catch (error) {\n            this._handleError(error, operation.type)\n          }\n        } else {\n          return await pool\n            .exec(operation.methodName, [packageString])\n            .timeout(CONFIG.WORKER_TIMEOUT)\n        }\n      })\n    })\n  }\n\n  _handleError(error, operationType) {\n    if (error.response) {\n      // The request was made and the server responded with a status code\n      // that falls out of the range of 2xx\n      const contents = error.response.data\n      throw new CustomError(\n        contents.name || 'BuildError',\n        contents.originalError,\n        contents.extra\n      )\n    } else if (error.request) {\n      // The request was made but no response was received\n      // `error.request` is an instance of XMLHttpRequest in the browser and an instance of\n      // http.ClientRequest in node.js\n      debug('No response received from build server. Is the server down?')\n      throw new CustomError('BuildError', {\n        operation: operationType,\n        reason: 'BUILD_SERVICE_UNREACHABLE',\n        url: error.request._currentUrl,\n      })\n    } else {\n      // Something happened in setting up the request that triggered an Error\n      throw new CustomError('BuildError', error.message, {\n        operation: operationType,\n      })\n    }\n  }\n\n  async getPackageBuildStats(packageString, priority) {\n    return await requestQueue.process(\n      packageString,\n      OpertationType.PACKAGE_BUILD_STATS,\n      { packageString },\n      { priority }\n    )\n  }\n\n  async getPackageExports(packageString, priority) {\n    return await requestQueue.process(\n      packageString,\n      OpertationType.PACKAGE_EXPORTS,\n      { packageString },\n      { priority }\n    )\n  }\n\n  async getPackageExportSizes(packageString, priority) {\n    return await requestQueue.process(\n      packageString,\n      OpertationType.PACKAGE_EXPORTS_SIZES,\n      { packageString },\n      { priority }\n    )\n  }\n}\n\nmodule.exports = BuildService\n"
  },
  {
    "path": "server/config.js",
    "content": "// Use ES6 supported by Node v6.10 only!\n\nconst path = require('path')\nconst dev = false //process.env.NODE_ENV === 'development'\n\nmodule.exports = {\n  tmp: path.join(__dirname, '..', 'tmp-build'),\n\n  MAX_WORKERS: require('os').cpus().length,\n\n  MAX_FAILURE_CACHE_ENTRIES: 600,\n\n  WORKER_TIMEOUT: 600 * 1000, //ms,\n\n  DEFAULT_DEV_PORT: 5000,\n\n  blackList: [\n    /hack-cheats/,\n    /hacks?-cheats?/,\n    /hack-unlimited/,\n    /generator-unlimited/,\n    /hack-\\d+/,\n    /cheat-\\d+/,\n    /-hacks?-/,\n    /^nuxt$/,\n    /^next$/,\n    /^react-scripts/,\n    /^polymer-cli/,\n    /^parcel$/,\n    /^devextreme$/,\n    /^yarn$/,\n\n    // big packages, that fail often\n    /^styled-icons$/,\n    /^razzle$/,\n  ],\n\n  unsupported: [\n    {\n      test: /^@types\\//,\n      reason: \"Type packages don't usually contain any runtime code.\",\n    },\n  ],\n\n  CACHE: {\n    PUBLIC_ASSETS: dev ? 0 : 24 * 60 * 60,\n    RECENTS_API: dev ? 0 : 20 * 60,\n    PACKAGE_HISTORY_API: dev ? 0 : 60 * 60,\n    SIMILAR_API: dev ? 0 : 60 * 60 * 2,\n    SIZE_API_DEFAULT: dev ? 0 : 30,\n    SIZE_API_ERROR: dev ? 0 : 60,\n    SIZE_API_ERROR_FATAL: dev ? 0 : 60 * 60,\n    SIZE_API_ERROR_UNSUPPORTED: dev ? 0 : 24 * 60 * 60,\n    SIZE_API_HAS_VERSION: dev ? 0 : 24 * 60 * 60,\n  },\n}\n"
  },
  {
    "path": "server/data/similar-packages/date-time.js",
    "content": "// Ununsed file\n\nexport default [[{ name: 'moment' }, { name: 'luxon' }, { name: 'date-fns' }]]\n"
  },
  {
    "path": "server/data/similar-packages/index.js",
    "content": "// Ununsed file\n\nimport dateTimeList from './date-time'\nimport markdownList from './markdown'\nimport storageList from './storage'\n\nexport default [...dateTimeList, ...markdownList, ...storageList]\n"
  },
  {
    "path": "server/data/similar-packages/markdown.js",
    "content": "// Ununsed file\n"
  },
  {
    "path": "server/data/similar-packages/storage.js",
    "content": "// Ununsed file\n"
  },
  {
    "path": "server/init.js",
    "content": "const config = require('./config')\nconst LRU = require('lru-cache')\nconst workerpool = require('workerpool')\nconst Queue = require('./Queue')\nconst logger = require('./Logger')\n\nconst failureCache = new LRU({\n  max: config.MAX_FAILURE_CACHE_ENTRIES,\n  maxAge: 6 * 1000 * 60 * 60,\n})\n\nconst debug = require('debug')('bp:request')\n\nconst requestQueue = new Queue({\n  concurrency: 4,\n  maxAge: 60 * 2,\n})\n\nconst pool = workerpool.pool(`./server/worker.js`, {\n  maxWorkers: config.MAX_WORKERS,\n})\n\nif (process.env.BUILD_SERVICE_ENDPOINT) {\n  pool.terminate()\n}\n\nmodule.exports = {\n  failureCache,\n  requestQueue,\n  pool,\n  debug,\n  logger,\n}\n"
  },
  {
    "path": "server/middlewares/exports.middleware.js",
    "content": "const semver = require('semver')\nconst CONFIG = require('../config')\nconst now = require('performance-now')\nconst logger = require('../Logger')\nconst { getRequestPriority } = require('../../utils/server.utils')\nconst { parsePackageString } = require('../../utils/common.utils')\nconst BuildService = require('../api/BuildService')\n\nconst buildService = new BuildService()\n\nasync function exportsMiddleware(ctx) {\n  let result,\n    priority = getRequestPriority(ctx)\n  const { name, version, packageString } = ctx.state.resolved\n  const { force, package: packageQuery } = ctx.query\n\n  const buildStart = now()\n  result = await buildService.getPackageExports(packageString, priority)\n\n  const buildEnd = now()\n\n  ctx.cacheControl = {\n    maxAge: force\n      ? 0\n      : semver.valid(parsePackageString(packageQuery).version)\n      ? CONFIG.CACHE.SIZE_API_HAS_VERSION\n      : CONFIG.CACHE.SIZE_API_DEFAULT,\n  }\n  ctx.body = { name, version, exports: result }\n  const time = buildEnd - buildStart\n\n  logger.info(\n    'BUILD_EXPORTS',\n    {\n      result,\n      requestId: ctx.state.id,\n      packageString,\n      time,\n    },\n    `BUILD EXPORTS: ${packageString} built in ${time.toFixed()}s`\n  )\n}\n\nmodule.exports = exportsMiddleware\n"
  },
  {
    "path": "server/middlewares/exportsSizes.middleware.js",
    "content": "const semver = require('semver')\nconst CONFIG = require('../config')\nconst now = require('performance-now')\nconst logger = require('../Logger')\nconst Cache = require('../../utils/cache.utils')\nconst { getRequestPriority } = require('../../utils/server.utils')\nconst { parsePackageString } = require('../../utils/common.utils')\nconst BuildService = require('../api/BuildService')\n\nconst cache = new Cache()\nconst buildService = new BuildService()\n\nasync function exportSizesMiddleware(ctx) {\n  let result,\n    priority = getRequestPriority(ctx)\n  const { name, version, packageString } = ctx.state.resolved\n  const { force, peek, package: packageQuery } = ctx.query\n\n  if (peek) {\n    return { name, version, peekSuccess: false }\n  }\n\n  const buildStart = now()\n  result = await buildService.getPackageExportSizes(packageString, priority)\n\n  const buildEnd = now()\n\n  ctx.cacheControl = {\n    maxAge: force\n      ? 0\n      : semver.valid(parsePackageString(packageQuery).version)\n      ? CONFIG.CACHE.SIZE_API_HAS_VERSION\n      : CONFIG.CACHE.SIZE_API_DEFAULT,\n  }\n\n  const body = { name, version, ...result }\n  ctx.body = body\n  const time = buildEnd - buildStart\n\n  logger.info(\n    'BUILD_EXPORTS_SIZES',\n    {\n      result,\n      requestId: ctx.state.id,\n      packageString,\n      time,\n    },\n    `BUILD EXPORTS SIZES: ${packageString} built in ${time.toFixed()}s`\n  )\n\n  if (force === 'true') {\n    cache.setExportsSize({ name, version }, body)\n  }\n}\n\nmodule.exports = exportSizesMiddleware\n"
  },
  {
    "path": "server/middlewares/generateImg.middleware.js",
    "content": "const { drawStatsImg } = require('../../utils/draw.utils')\nconst Cache = require('../../utils/cache.utils')\nconst send = require('koa-send')\nconst path = require('path')\nconst queryString = require('query-string')\nconst { resolvePackage } = require('../../utils/server.utils')\nconst semver = require('semver')\n\nconst cache = new Cache()\n\nasync function generateImgMiddleware(ctx, next) {\n  // See https://github.com/facebook/react/issues/13838\n  const url = ctx.url.replace(/&amp;/g, '&')\n\n  let { name, version, theme, wide } = queryString.parseUrl(url).query\n\n  try {\n    if (!semver.valid(version)) {\n      const resolved = await resolvePackage(name)\n      version = resolved.version\n    }\n\n    const result = await cache.getPackageSize({ name, version })\n\n    ctx.type = 'png'\n    ctx.cacheControl = {\n      maxAge: 60 * 60 * 60,\n    }\n    ctx.body = drawStatsImg({\n      name: result.name,\n      version: result.version,\n      min: result.size,\n      gzip: result.gzip,\n      theme,\n      wide,\n    })\n  } catch (err) {\n    console.error(err)\n    ctx.cacheControl = {\n      noCache: true,\n    }\n    await send(ctx, 'client/assets/public/android-chrome-192x192.png')\n  }\n}\n\nmodule.exports = generateImgMiddleware\n"
  },
  {
    "path": "server/middlewares/jsonCache.middleware.js",
    "content": "const koaCache = require('koa-cash')\n\nfunction jsonCacheMiddleware({ get, set, hash: hashFn }) {\n  return koaCache({\n    async get(key) {\n      // Emulate koa-cash cache value\n      const value = await get(key)\n      return {\n        body: value,\n        type: 'application/json',\n      }\n    },\n    set(key, value) {\n      // We only need the body part from what\n      // koa-cash gives us\n      set(key, JSON.parse(value.body))\n    },\n    hash(ctx) {\n      return hashFn(ctx)\n    },\n  })\n}\n\nmodule.exports = jsonCacheMiddleware\n"
  },
  {
    "path": "server/middlewares/rateLimit.middleware.js",
    "content": "// Modified package `koa-better-ratelimit`\n// to accept original client ips from cloudflare\n\nconst ipchecker = require('ipchecker')\nconst defaults = {\n  duration: 1000 * 60 * 60,\n  whiteList: [],\n  blackList: [],\n  accessLimited: '429: Too Many Requests.',\n  accessForbidden: '403: This is forbidden area for you.',\n  max: 100,\n  env: null,\n}\n\n/**\n * With options through init you can control\n * black/white lists, limit per ip and reset interval.\n *\n * @param {Object} options\n * @api public\n */\nmodule.exports = function betterlimit(options = {}) {\n  const db = {}\n\n  for (const key in defaults) {\n    if (!options[key]) {\n      options[key] = defaults[key]\n    }\n  }\n\n  if (options.message_429) {\n    options.accessLimited = options.message_429\n  }\n\n  if (options.message_403) {\n    options.accessForbidden = options.message_403\n  }\n\n  const whiteListMap = ipchecker.map(options.whiteList)\n  const blackListMap = ipchecker.map(options.blackList)\n\n  return function* ratelimit(next) {\n    const ip =\n      this.request.header['x-koaip'] ||\n      this.request.header['cf-connecting-ip'] ||\n      this.ip\n\n    if (!ip) {\n      return yield* next\n    }\n    if (ipchecker.check(ip, blackListMap)) {\n      this.response.status = 403\n      this.response.body = options.accessForbidden\n      return\n    }\n    if (ipchecker.check(ip, whiteListMap)) {\n      return yield* next\n    }\n\n    const now = Date.now()\n    const reset = now + options.duration\n\n    if (!db.hasOwnProperty(ip)) {\n      db[ip] = { ip, reset, limit: options.max }\n    }\n\n    const delta = db[ip].reset - now\n    const retryAfter = (delta / 1000) | 0\n\n    db[ip].limit = db[ip].limit - 1\n    this.response.set('X-RateLimit-Limit', options.max)\n\n    if (db[ip].reset > now) {\n      const rateLimiting = db[ip].limit < 0 ? 0 : db[ip].limit\n      this.response.set('X-RateLimit-Remaining', rateLimiting)\n    }\n\n    if (db[ip].limit < 0 && db[ip].reset < now) {\n      db[ip] = { ip, reset, limit: options.max }\n      db[ip].limit = db[ip].limit - 1\n      this.response.set('X-RateLimit-Remaining', db[ip].limit)\n    }\n\n    this.response.set('X-RateLimit-Reset', db[ip].reset)\n\n    if (db[ip].limit < 0) {\n      this.response.set('Retry-After', retryAfter)\n      this.response.status = 429\n      this.response.body = options.accessLimited\n      return\n    }\n\n    return yield* next\n  }\n}\n"
  },
  {
    "path": "server/middlewares/requestLogger.middleware.js",
    "content": "const logger = require('../Logger')\nconst now = require('performance-now')\n\nasync function requestLoggerMiddleware(ctx, next) {\n  if (!ctx.request.url.includes('/api/')) {\n    await next()\n    return\n  }\n\n  const requestStart = now()\n  await next()\n  const requestEnd = now()\n  const time = requestEnd - requestStart\n\n  logger.info(\n    'REQUEST',\n    {\n      url: ctx.request.url,\n      type: ctx.request.type,\n      query: ctx.request.query,\n      headers: ctx.request.headers,\n      ip:\n        ctx.request.header['x-koaip'] ||\n        ctx.request.header['cf-connecting-ip'] ||\n        ctx.ip,\n      requestId: ctx.state.id,\n      method: ctx.request.method,\n      origin: ctx.request.origin,\n      hostname: ctx.request.hostname,\n      status: ctx.response.status,\n      time,\n    },\n    `REQUEST: ${ctx.response.status} ${(time / 1000).toFixed(2)}s ${\n      ctx.req.method\n    } ${ctx.request.url}`\n  )\n}\n\nmodule.exports = requestLoggerMiddleware\n"
  },
  {
    "path": "server/middlewares/results/blockBlacklist.middleware.js",
    "content": "const { parsePackageString } = require('../../../utils/common.utils')\nconst CustomError = require('./../../CustomError')\nconst CONFIG = require('../../config')\n\nasync function blockBlacklistMiddleware(ctx, next) {\n  const { package: packageString, force } = ctx.query\n  if (force) {\n    await next()\n    return\n  }\n\n  const parsedPackage = parsePackageString(packageString)\n\n  // If package is blacklisted, fail fast\n  if (CONFIG.blackList.some(entry => entry.test(parsedPackage.name))) {\n    throw new CustomError('BlocklistedPackageError', { ...parsedPackage })\n  }\n\n  // If package is unsupported, fail fast\n  const matchedUnsupportedRule = CONFIG.unsupported.find(rule =>\n    new RegExp(rule.test).test(parsedPackage.name)\n  )\n  if (matchedUnsupportedRule) {\n    throw new CustomError(\n      'UnsupportedPackageError',\n      { ...parsedPackage },\n      { reason: matchedUnsupportedRule.reason }\n    )\n  }\n\n  await next()\n}\n\nmodule.exports = blockBlacklistMiddleware\n"
  },
  {
    "path": "server/middlewares/results/build.middleware.js",
    "content": "const semver = require('semver')\nconst CONFIG = require('../../config')\nconst firebaseUtils = require('../../../utils/firebase.utils')\nconst now = require('performance-now')\nconst logger = require('../../Logger')\nconst Cache = require('../../../utils/cache.utils')\nconst BuildService = require('../../api/BuildService')\nconst { getRequestPriority } = require('../../../utils/server.utils')\nconst { parsePackageString } = require('../../../utils/common.utils')\n\nconst cache = new Cache()\nconst buildService = new BuildService()\n\nasync function buildMiddleware(ctx, next) {\n  let result,\n    priority = getRequestPriority(ctx)\n  const { scoped, name, version, description, repository, packageString } =\n    ctx.state.resolved\n  const { force, record, package: packageQuery } = ctx.query\n\n  const buildStart = now()\n  result = await buildService.getPackageBuildStats(packageString, priority)\n  const buildEnd = now()\n\n  ctx.cacheControl = {\n    maxAge: force\n      ? 0\n      : semver.valid(parsePackageString(packageQuery).version)\n      ? CONFIG.CACHE.SIZE_API_HAS_VERSION\n      : CONFIG.CACHE.SIZE_API_DEFAULT,\n  }\n\n  const body = { scoped, name, version, description, repository, ...result }\n  ctx.body = body\n  ctx.state.buildResult = body\n  const time = buildEnd - buildStart\n\n  logger.info(\n    'BUILD',\n    {\n      result,\n      requestId: ctx.state.id,\n      packageString,\n      time,\n    },\n    `BUILD: ${packageString} built in ${time.toFixed()}s and is ${\n      result.size\n    } bytes`\n  )\n\n  if (record === 'true') {\n    firebaseUtils.setRecentSearch(name, { name, version })\n  }\n\n  if (force === 'true') {\n    cache.setPackageSize({ name, version }, body)\n  }\n}\n\nmodule.exports = buildMiddleware\n"
  },
  {
    "path": "server/middlewares/results/cachedResponse.middleware.js",
    "content": "const semver = require('semver')\nconst CONFIG = require('../../config')\nconst { failureCache, debug } = require('../../init')\nconst logger = require('../../Logger')\n\nasync function cachedResponse(ctx, next) {\n  const { force, peep } = ctx.query\n  if (force) {\n    await next()\n    return\n  }\n  const { name, version, packageString } = ctx.state.resolved\n\n  const logCache = ({ hit, type = '', message }) =>\n    logger.info(\n      'CACHE',\n      {\n        name,\n        version,\n        packageString,\n        hit,\n        type,\n        requestId: ctx.state.id,\n      },\n      message\n    )\n\n  const cached = await ctx.cashed()\n  if (cached) {\n    ctx.cacheControl = {\n      maxAge: force\n        ? 0\n        : semver.valid(version)\n        ? CONFIG.CACHE.SIZE_API_HAS_VERSION\n        : CONFIG.CACHE.SIZE_API_DEFAULT,\n    }\n\n    logCache({ hit: true, message: `CACHE HIT: ${packageString}` })\n    return\n  }\n\n  const failureCacheEntry = failureCache.get(packageString)\n  if (failureCacheEntry) {\n    debug('fetched %s from failure cache', packageString)\n\n    logCache({\n      hit: true,\n      type: 'failure',\n      message: `FAILURE CACHE HIT: ${packageString}`,\n    })\n\n    ctx.status = failureCacheEntry.status\n    ctx.body = failureCacheEntry.body\n    return\n  }\n\n  logCache({ hit: false, message: `CACHE MISS: ${packageString}` })\n\n  // When peeping into the built results,\n  // we return a 404 if a build is required.\n  if (peep) {\n    ctx.status = 404\n    return\n  }\n  await next()\n}\n\nmodule.exports = cachedResponse\n"
  },
  {
    "path": "server/middlewares/results/error.middleware.js",
    "content": "const arrayToSentence = require('array-to-sentence')\nconst now = require('performance-now')\nconst { failureCache } = require('../../init')\nconst CONFIG = require('../../config')\nconst debug = require('debug')('bp:error')\nconst logger = require('../../Logger')\n\nasync function errorHandler(ctx, next) {\n  const { force } = ctx.query\n  const start = now()\n\n  const respondWithError = (status, { code, message = '', details = {} }) => {\n    ctx.status = status\n    ctx.body = {\n      error: { code, message, details },\n    }\n\n    logger.error(\n      'BUILD_ERROR',\n      {\n        type: code,\n        requestId: ctx.state.id,\n        time: now() - start,\n        ...ctx.state.resolved,\n        details,\n      },\n      `${code} ${ctx.state.resolved.packageString}`\n    )\n  }\n\n  try {\n    await next()\n  } catch (err) {\n    console.error(err)\n    ctx.cacheControl = {\n      maxAge: force ? 0 : CONFIG.CACHE.SIZE_API_ERROR,\n    }\n\n    if (!('name' in err)) {\n      respondWithError(500, { code: 'UnknownError', details: err })\n      return\n    }\n\n    switch (err.name) {\n      case 'BlocklistedPackageError':\n        respondWithError(403, {\n          code: 'BlocklistedPackageError',\n          message:\n            'The package you were looking for is blocklisted ' +\n            \"because it failed to build multiple times in the past and further tries aren't likely to succeed. This can \" +\n            \"happen if this package wasn't meant to be bundled in a client side application.\",\n        })\n        break\n\n      case 'UnsupportedPackageError':\n        ctx.cacheControl = {\n          maxAge: force ? 0 : CONFIG.CACHE.SIZE_API_ERROR_UNSUPPORTED,\n        }\n        respondWithError(403, {\n          code: 'UnsupportedPackageError',\n          message: `The package you were looking for is unsupported and cannot be built by bundlephobia — ${err.extra.reason}`,\n        })\n        break\n\n      case 'PackageNotFoundError':\n        respondWithError(404, {\n          code: 'PackageNotFoundError',\n          message: \"The package you were looking for doesn't exist.\",\n        })\n        break\n\n      case 'PackageVersionMismatchError': {\n        const validVersions = arrayToSentence(\n          err.extra.validVersions.map(version => `\\`<code>${version}</code>\\``)\n        )\n\n        respondWithError(404, {\n          code: 'PackageVersionMismatchError',\n          message: `This package has not been published with this particular version. \n                Valid versions - ${validVersions}`,\n        })\n        break\n      }\n\n      case 'InstallError':\n        respondWithError(500, {\n          code: 'InstallError',\n          message: 'Installing the package failed.',\n        })\n        // Installing can fail for various reasons,\n        // let's not cache this since it will\n        // likely be resolved on a retry\n        ctx.cacheControl = {\n          maxAge: 0,\n        }\n        break\n\n      case 'EntryPointError': {\n        const status = 500\n        const body = {\n          error: {\n            code: 'EntryPointError',\n            message:\n              'We could not guess a valid entry point for this package. ' +\n              \"Perhaps the author hasn't specified one in its package.json ?\",\n          },\n        }\n\n        ctx.cacheControl = {\n          maxAge: force ? 0 : CONFIG.CACHE.SIZE_API_ERROR_FATAL,\n        }\n\n        respondWithError(status, body.error)\n\n        debug(\n          'saved %s to failure cache',\n          `${ctx.state.resolved.packageString}`\n        )\n        failureCache.set(`${ctx.state.packageString}`, { status, body })\n\n        break\n      }\n\n      case 'MissingDependencyError': {\n        const status = 500\n        const missingModules = arrayToSentence(\n          err.extra.missingModules.map(module => `\\`<code>${module}</code>\\``)\n        )\n        const body = {\n          error: {\n            code: 'MissingDependencyError',\n            message:\n              `This package (or this version) uses ${missingModules}, ` +\n              `but does not specify ${\n                missingModules.length > 1 ? 'them' : 'it'\n              } either as a dependency or a peer dependency`,\n            details: err,\n          },\n        }\n\n        ctx.cacheControl = {\n          maxAge: force ? 0 : CONFIG.CACHE.SIZE_API_ERROR_FATAL,\n        }\n\n        respondWithError(status, body.error)\n\n        debug(\n          'saved %s to failure cache',\n          `${ctx.state.resolved.packageString}`\n        )\n        failureCache.set(`${ctx.state.packageString}`, { status, body })\n        break\n      }\n\n      case 'MinifyError': {\n        const status = 500\n        console.log('err.originalError', err.originalError)\n        const body = {\n          error: {\n            code: 'MinifyError',\n            message:\n              'We could not minify one of the source files in this package or its dependencies. ' +\n              `Please verify if the contents of <code>${err.extra.filePath}</code> can be minified using <a href=\"https://try.terser.org/\" target=\"_blank\">terser</a>.`,\n            details: {\n              originalError: JSON.stringify(err.originalError, null, 2),\n            },\n          },\n        }\n\n        ctx.cacheControl = {\n          maxAge: force ? 0 : CONFIG.CACHE.SIZE_API_ERROR_FATAL,\n        }\n\n        respondWithError(status, body.error)\n\n        debug(\n          'saved %s to failure cache',\n          `${ctx.state.resolved.packageString}`\n        )\n        failureCache.set(`${ctx.state.packageString}`, { status, body })\n\n        break\n      }\n\n      case 'BuildError':\n      default: {\n        const status = 500\n        const errorJSON = {\n          code: 'BuildError',\n          message: 'Failed to build this package.',\n          details: err,\n        }\n        respondWithError(500, errorJSON)\n        debug(\n          'saved %s to failure cache',\n          `${ctx.state.resolved.packageString}`\n        )\n        failureCache.set(`${ctx.state.packageString}`, {\n          status,\n          body: { error: errorJSON },\n        })\n        break\n      }\n    }\n  }\n}\n\nmodule.exports = errorHandler\n"
  },
  {
    "path": "server/middlewares/results/index.js",
    "content": ""
  },
  {
    "path": "server/middlewares/results/resolvePackage.middleware.js",
    "content": "const { resolvePackage } = require('../../../utils/server.utils')\nconst { parsePackageString } = require('../../../utils/common.utils')\nconst gitURLParse = require('git-url-parse')\nconst { debug, logger } = require('../../init')\nconst now = require('performance-now')\n\nasync function resolvePackageMiddleware(ctx, next) {\n  const { package: packageString } = ctx.query\n  const parsedPackage = parsePackageString(packageString)\n  let resolvedPackage\n\n  // prefill values in case resolution fails\n  ctx.state.resolved = {\n    ...parsedPackage,\n    packageString: `${parsedPackage.name}@${parsedPackage.version}`,\n  }\n\n  const resolveStart = now()\n  resolvedPackage = await resolvePackage(packageString)\n  const resolveEnd = now()\n\n  const { name, version, repository, description } = resolvedPackage\n  let truncatedDescription = ''\n  let repositoryURL = ''\n\n  try {\n    repositoryURL = gitURLParse(repository.url || repository).toString('https')\n  } catch (e) {\n    console.error('failed to parse repository url', repository)\n  }\n\n  if (description) {\n    truncatedDescription =\n      description.length > 300\n        ? description.substring(0, 300) + '…'\n        : description\n  }\n\n  const result = {\n    name,\n    version,\n    scoped: parsedPackage.scoped,\n    packageString: `${name}@${version}`,\n    description: truncatedDescription,\n    repository: repositoryURL,\n  }\n\n  ctx.state.resolved = result\n\n  debug('resolved to %s@%s', name, version)\n  const time = resolveEnd - resolveStart\n  logger.info(\n    'RESOLVE_PACKAGE',\n    { ...result, time, requestId: ctx.state.id },\n    `RESOLVED: ${result.packageString} in ${time.toFixed(0)}ms`\n  )\n\n  await next()\n}\n\nmodule.exports = resolvePackageMiddleware\n"
  },
  {
    "path": "server/middlewares/similar-packages/fixtures.js",
    "content": "const Weight = {\n  SMALL: 1,\n  MID: 3,\n  NORMAL: 5,\n  HIGH: 7,\n  MAX: 15,\n}\n\nconst categories = {\n  'classname-strings': {\n    name: 'Classname string construction',\n    tags: [\n      { tag: 'class', weight: Weight.HIGH },\n      { tag: 'classnames', weight: Weight.HIGH },\n      { tag: 'string', weight: Weight.MID },\n      { tag: 'construct', weight: Weight.MID },\n      { tag: 'conditional', weight: Weight.MID },\n    ],\n    similar: ['clsx', 'classnames', 'classcat', 'merge-class-names'],\n  },\n  clipboard: {\n    name: 'Clipboard Utilities',\n    tags: [\n      { tag: 'clipboard', weight: Weight.MAX },\n      { tag: 'copy', weight: Weight.MID },\n      { tag: 'cut', weight: Weight.MID },\n    ],\n    similar: [\n      'clipboard',\n      'clipboardy',\n      'clipboard-copy',\n      'copy-text-to-clipboard',\n      'clipboard-polyfill',\n    ],\n  },\n  'css-in-js': {\n    name: 'CSS in JS libraries',\n    tags: [\n      { tag: 'css js', weight: Weight.NORMAL },\n      { tag: 'styles', weight: Weight.NORMAL },\n      { tag: 'inline', weight: Weight.NORMAL },\n      { tag: 'in', weight: Weight.MID },\n    ],\n    similar: ['styled-components', 'jss', 'emotion', 'linaria'],\n  },\n  'color-manipulation': {\n    name: 'Color parsing and manipulation',\n    tags: [{ tag: 'color convert parse manipulate', weight: Weight.NORMAL }],\n    similar: ['chroma-js', 'color', 'tinycolor2', 'colors.js', 'color2k'],\n  },\n  'cookie-browser': {\n    name: 'Cookie Manipulation for Node.js',\n    tags: [\n      { tag: 'cookie', weight: Weight.MAX },\n      { tag: 'manipulate', weight: Weight.NORMAL },\n      { tag: 'parse', weight: Weight.MID },\n      { tag: 'parser', weight: Weight.MID },\n      { tag: 'jar', weight: Weight.NORMAL },\n      { tag: 'node', weight: Weight.NORMAL },\n    ],\n    similar: [\n      'cookie',\n      'tough-cookie',\n      'js-cookie',\n      'tiny-cookie',\n      'universal-cookie',\n    ],\n  },\n  'cookie-node': {\n    name: 'Cookie Manipulation for Browsers',\n    tags: [\n      { tag: 'cookie', weight: Weight.MAX },\n      { tag: 'manipulate', weight: Weight.NORMAL },\n      { tag: 'parse', weight: Weight.MID },\n      { tag: 'parser', weight: Weight.MID },\n      { tag: 'browser', weight: Weight.SMALL },\n    ],\n    similar: ['js-cookie', 'browser-cookies', 'universal-cookie'],\n  },\n  'date-nlp': {\n    name: 'Natural language date-time utilities',\n    tags: [\n      { tag: 'date', weight: Weight.HIGH },\n      { tag: 'time', weight: Weight.HIGH },\n      { tag: 'parse', weight: Weight.MID },\n      { tag: 'parser', weight: Weight.MID },\n      { tag: 'nlp', weight: Weight.HIGH },\n      { tag: 'natural language', weight: Weight.HIGH },\n      { tag: 'format', weight: Weight.MID },\n      { tag: 'human', weight: Weight.MID },\n    ],\n    similar: ['chrono-node', 'its-a-date', 'parse-messy-time'],\n  },\n  'deep-equality': {\n    name: 'Deep Equality Check',\n    tags: [\n      { tag: 'deep equal', weight: Weight.NORMAL },\n      { tag: 'object', weight: Weight.MID },\n      { tag: 'compare', weight: Weight.NORMAL },\n      { tag: 'isequal', weight: Weight.HIGH },\n    ],\n    similar: ['fast-deep-equal', 'deep-eql', 'deep-equal', 'lodash.isequal'],\n  },\n  'drag-and-drop': {\n    name: 'Drag & Drop Libraries',\n    tags: [\n      { tag: 'drag drop', weight: Weight.NORMAL },\n      { tag: 'sort', weight: Weight.MID },\n      { tag: 'order', weight: Weight.MID },\n    ],\n    similar: [\n      '@shopify/draggable',\n      'dragula',\n      'muuri',\n      'sortablejs',\n      'draggabilly',\n      'interactjs',\n    ],\n  },\n  'excel-parsers': {\n    name: 'Excel File Readers, Manipulators & Writers',\n    tags: [\n      { tag: 'excel', weight: Weight.MAX },\n      { tag: 'read', weight: Weight.SMALL },\n      { tag: 'write', weight: Weight.SMALL },\n      { tag: 'manipulate', weight: Weight.SMALL },\n      { tag: 'parse', weight: Weight.SMALL },\n    ],\n    similar: ['xlsx', 'exceljs', 'node-xlsx', 'excel4node'],\n  },\n  'full-text-search': {\n    name: 'Text search',\n    tags: [\n      { tag: 'search', weight: Weight.NORMAL },\n      { tag: 'solr', weight: Weight.HIGH },\n      { tag: 'fuzzy', weight: Weight.NORMAL },\n      { tag: 'text', weight: Weight.NORMAL },\n    ],\n    similar: ['flexsearch', 'lunr', 'wade', 'js-search', 'fuse.js'],\n  },\n  'fetch-polyfill': {\n    name: 'Fetch polyfills',\n    tags: [\n      { tag: 'fetch', weight: Weight.HIGH },\n      { tag: 'polyfill', weight: Weight.NORMAL },\n      { tag: 'xhr', weight: Weight.NORMAL },\n      { tag: 'http', weight: Weight.MID },\n      { tag: 'request', weight: Weight.MID },\n    ],\n    similar: ['whatwg-fetch', 'node-fetch', 'unfetch', 'make-fetch-happen'],\n  },\n  'general-purpose-date-time': {\n    name: 'General purpose date-time utilities',\n    tags: [\n      { tag: 'date', weight: Weight.HIGH },\n      { tag: 'time', weight: Weight.HIGH },\n      { tag: 'parse', weight: Weight.MID },\n      { tag: 'parser', weight: Weight.MID },\n      { tag: 'format', weight: Weight.MID },\n    ],\n    similar: ['moment', 'luxon', 'dayjs', 'date-fns'],\n  },\n  'general-purpose-3d': {\n    name: 'General purpose 3D libraries',\n    tags: [\n      { tag: '3d', weight: Weight.MAX },\n      { tag: 'webgl', weight: Weight.MAX },\n      { tag: 'gl', weight: Weight.HIGH },\n    ],\n    similar: ['three', 'babylonjs'],\n  },\n  'general-purpose-animation': {\n    name: 'General purpose animation libraries',\n    tags: [\n      { tag: 'animation', weight: Weight.HIGH },\n      { tag: 'tween', weight: Weight.NORMAL },\n      { tag: 'easing', weight: Weight.MID },\n      { tag: 'morph', weight: Weight.MID },\n      { tag: 'transform', weight: Weight.NORMAL },\n      { tag: 'motion', weight: Weight.NORMAL },\n      { tag: 'svg', weight: Weight.MID },\n      { tag: 'physics', weight: Weight.MID },\n      { tag: 'dom', weight: Weight.MID },\n    ],\n    similar: ['gsap', 'animejs', 'velocity-animate', 'popmotion'],\n  },\n  'general-purpose-charting': {\n    name: 'General purpose Charting libraries',\n    tags: [\n      { tag: 'dom', weight: Weight.MID },\n      { tag: 'visualization', weight: Weight.HIGH },\n      { tag: 'dataviz', weight: Weight.HIGH },\n      { tag: 'svg', weight: Weight.SMALL },\n      { tag: 'canvas', weight: Weight.MID },\n      { tag: 'charts', weight: Weight.MAX },\n      { tag: 'data', weight: Weight.MID },\n    ],\n    similar: [\n      'd3',\n      'chart.js',\n      'echarts',\n      'chartist',\n      'frappe-charts',\n      'highcharts',\n      'uplot',\n    ],\n  },\n  'graphql-client': {\n    name: 'GraphQL Clients',\n    tags: [\n      { tag: 'graphql', weight: Weight.HIGH },\n      { tag: 'client', weight: Weight.MID },\n      { tag: 'js', weight: Weight.MID },\n      { tag: 'javascript', weight: Weight.MID },\n    ],\n    similar: [\n      '@apollo/client',\n      'graphql.js',\n      'lokka',\n      'graphql',\n      'relay-runtime',\n      'urql',\n    ],\n  },\n  'html-sanitization': {\n    name: 'HTML Sanitization',\n    tags: [\n      { tag: 'html', weight: Weight.NORMAL },\n      { tag: 'dom', weight: Weight.NORMAL },\n      { tag: 'sanitize', weight: Weight.HIGH },\n      { tag: 'untrusted', weight: Weight.SMALL },\n      { tag: 'escape', weight: Weight.MID },\n      { tag: 'filter', weight: Weight.NORMAL },\n      { tag: 'xss', weight: Weight.HIGH },\n      { tag: 'whitelist', weight: Weight.SMALL },\n    ],\n    similar: ['sanitize-html', 'xss', 'dompurify', 'sanitizer'],\n  },\n  i18n: {\n    name: 'Internationalization',\n    tags: [\n      { tag: 'i18n', weight: Weight.HIGH },\n      { tag: 'intl', weight: Weight.HIGH },\n      { tag: 'internation', weight: Weight.HIGH },\n      { tag: 'language', weight: Weight.MID },\n    ],\n    similar: ['fbt', 'globalize', 'i18next', 'node-polyglot', '@lingui/core'],\n  },\n  'node-http-request': {\n    name: 'HTTP client libraries for Node.js',\n    tags: [\n      { tag: 'http', weight: Weight.NORMAL },\n      { tag: 'get', weight: Weight.NORMAL },\n      { tag: 'post', weight: Weight.NORMAL },\n      { tag: 'ajax', weight: Weight.HIGH },\n      { tag: 'url', weight: Weight.SMALL },\n      { tag: 'request', weight: Weight.HIGH },\n      { tag: 'agent', weight: Weight.MID },\n      { tag: 'xhr', weight: Weight.NORMAL },\n      { tag: 'node node.js', weight: Weight.NORMAL },\n    ],\n    similar: ['got', 'phin', 'axios', 'node-fetch', 'superagent'],\n  },\n  'browser-http-request': {\n    name: 'HTTP client libraries for Browser',\n    tags: [\n      { tag: 'http', weight: Weight.NORMAL },\n      { tag: 'get', weight: Weight.NORMAL },\n      { tag: 'post', weight: Weight.NORMAL },\n      { tag: 'ajax', weight: Weight.HIGH },\n      { tag: 'url', weight: Weight.SMALL },\n      { tag: 'request', weight: Weight.HIGH },\n      { tag: 'agent', weight: Weight.MID },\n      { tag: 'xhr', weight: Weight.NORMAL },\n      { tag: 'browser', weight: Weight.NORMAL },\n    ],\n    similar: ['axios', 'ky', 'superagent', 'redaxios', 'unfetch'],\n  },\n  'icu-message-fromatter': {\n    name: 'ICU message string formatters',\n    tags: [\n      { tag: 'icu', weight: Weight.HIGH },\n      { tag: 'message', weight: Weight.NORMAL },\n      { tag: 'format', weight: Weight.MID },\n      { tag: 'plural', weight: Weight.MID },\n      { tag: 'gender', weight: Weight.MID },\n      { tag: 'parse', weight: Weight.SMALL },\n    ],\n    similar: ['messageformat', 'intl-messageformat'],\n  },\n  'image-color-extraction': {\n    name: 'Image color extraction',\n    tags: [\n      { tag: 'color', weight: Weight.HIGH },\n      { tag: 'image', weight: Weight.HIGH },\n      { tag: 'extract', weight: Weight.HIGH },\n      { tag: 'dominant', weight: Weight.HIGH },\n      { tag: 'palette', weight: Weight.HIGH },\n      { tag: 'pixels', weight: Weight.MID },\n    ],\n    similar: [\n      'img-color-extractor',\n      'color-thief-browser',\n      'colority',\n      'node-vibrant',\n    ],\n  },\n  'immutable-data-structures': {\n    name: 'Immutable Data',\n    tags: [\n      { tag: 'immutable', weight: Weight.HIGH },\n      { tag: 'persistent', weight: Weight.NORMAL },\n      { tag: 'functional', weight: Weight.MID },\n      { tag: 'collection', weight: Weight.NORMAL },\n      { tag: 'structure', weight: Weight.NORMAL },\n      { tag: 'tree', weight: Weight.MID },\n      { tag: 'freeze', weight: Weight.MID },\n      { tag: 'cursor', weight: Weight.MID },\n    ],\n    similar: [\n      'immutable',\n      'seamless-immutable',\n      'immutability-helper',\n      'baobab',\n    ],\n  },\n  'lazy-load-content': {\n    name: 'Lazy Loading Content',\n    tags: [\n      { tag: 'lazy', weight: Weight.NORMAL },\n      { tag: 'load', weight: Weight.NORMAL },\n      { tag: 'lazyload', weight: Weight.HIGH },\n      { tag: 'image', weight: Weight.NORMAL },\n      { tag: 'iframe', weight: Weight.MID },\n      { tag: 'video', weight: Weight.MID },\n    ],\n    similar: ['lazysizes', 'lozad', 'vanilla-lazyload'],\n  },\n  'markdown-parser': {\n    name: 'Markdown parsers',\n    tags: [\n      { tag: 'markdown', weight: Weight.HIGH },\n      { tag: 'parse', weight: Weight.NORMAL },\n      { tag: 'parser', weight: Weight.NORMAL },\n      { tag: 'ast', weight: Weight.MID },\n      { tag: 'abstract syntax tree', weight: Weight.MID },\n      { tag: 'md', weight: Weight.HIGH },\n    ],\n    similar: ['marked', 'markdown-it', 'showdown', 'remarkable', 'snarkdown'],\n  },\n  memoization: {\n    name: 'Memoization',\n    tags: [\n      { tag: 'memoize', weight: Weight.HIGH },\n      { tag: 'cache', weight: Weight.NORMAL },\n      { tag: 'performance', weight: Weight.MID },\n    ],\n    similar: [\n      'memoize',\n      'memoize-one',\n      'lodash.memoize',\n      'mem',\n      'fast-memoize',\n    ],\n  },\n  'number-manipulation': {\n    name: 'Number and Currency Formatting',\n    tags: [\n      { tag: 'format', weight: Weight.NORMAL },\n      { tag: 'manipulate', weight: Weight.NORMAL },\n      { tag: 'currency', weight: Weight.HIGH },\n      { tag: 'money', weight: Weight.HIGH },\n      { tag: 'number', weight: Weight.HIGH },\n    ],\n    similar: ['numeral', 'numbro', 'accounting', 'currency.js', 'dinero.js'],\n  },\n  'pdf-generator': {\n    name: 'Client-side PDF Creation',\n    tags: [\n      { tag: 'pdf', weight: Weight.HIGH },\n      { tag: 'generate', weight: Weight.MID },\n      { tag: 'create', weight: Weight.MID },\n      { tag: 'document', weight: Weight.MID },\n      { tag: 'client', weight: Weight.NORMAL },\n      { tag: 'browser', weight: Weight.NORMAL },\n    ],\n    similar: ['jspdf', 'pdfkit', 'pdfmake', '@react-pdf/renderer'],\n  },\n  'promise-polyfill': {\n    name: 'Promise polyfills',\n    tags: [\n      { tag: 'promise', weight: Weight.HIGH },\n      { tag: 'polyfill', weight: Weight.MID },\n      { tag: 'es6', weight: Weight.MID },\n      { tag: 'aplus', weight: Weight.HIGH },\n      { tag: 'async', weight: Weight.MID },\n      { tag: 'implementation', weight: Weight.MID },\n    ],\n    similar: [\n      'promise',\n      'es6-promise',\n      'promise-polyfill',\n      'es6-promise-polyfill',\n    ],\n  },\n  'react-animation': {\n    name: 'React based animation',\n    tags: [\n      { tag: 'react', weight: Weight.NORMAL },\n      { tag: 'animation', weight: Weight.HIGH },\n      { tag: 'transform', weight: Weight.NORMAL },\n      { tag: 'motion', weight: Weight.NORMAL },\n    ],\n    similar: ['react-spring', 'framer-motion', 'react-motion', 'react-move'],\n  },\n  'react-autocomplete': {\n    name: 'React based autocomplete components',\n    tags: [\n      { tag: 'react', weight: Weight.NORMAL },\n      { tag: 'autocomplete', weight: Weight.NORMAL },\n      { tag: 'autosuggest', weight: Weight.NORMAL },\n      { tag: 'typeahead', weight: Weight.NORMAL },\n    ],\n    similar: [\n      'react-autosuggest',\n      'downshift',\n      'react-autowhatever',\n      'react-autocomplete',\n      'react-select',\n    ],\n  },\n  'react-head-meta': {\n    name: 'React based meta tags management',\n    tags: [\n      { tag: 'react', weight: Weight.NORMAL },\n      { tag: 'head', weight: Weight.MID },\n      { tag: 'document', weight: Weight.MID },\n      { tag: 'title', weight: Weight.MID },\n      { tag: 'meta tags', weight: Weight.MID },\n    ],\n    similar: ['react-helment', 'react-meta-tags', 'react-document-title'],\n  },\n  'react-i18n': {\n    name: 'React based internationalization',\n    tags: [\n      { tag: 'react', weight: Weight.NORMAL },\n      { tag: 'i18n', weight: Weight.HIGH },\n      { tag: 'intl', weight: Weight.HIGH },\n      { tag: 'internation', weight: Weight.HIGH },\n    ],\n    similar: [\n      'react-intl',\n      'react-i18next',\n      'react-intl-universal',\n      'eo-locale',\n      '@lingui/react',\n    ],\n  },\n  'react-form': {\n    name: 'React based form builders & validators',\n    tags: [\n      { tag: 'react', weight: Weight.NORMAL },\n      { tag: 'redux', weight: Weight.MID },\n      { tag: 'form', weight: Weight.HIGH },\n      { tag: 'validate', weight: Weight.SMALL },\n    ],\n    similar: [\n      'formik',\n      'react-final-form',\n      'react-form',\n      'formsy-react',\n      'react-hook-form',\n    ],\n  },\n  'schema-validation': {\n    name: 'JSON schema validation',\n    tags: [\n      { tag: 'JSON', weight: Weight.MID },\n      { tag: 'object', weight: Weight.MID },\n      { tag: 'schema', weight: Weight.NORMAL },\n      { tag: 'validator', weight: Weight.HIGH },\n      { tag: 'JSON', weight: Weight.NORMAL },\n      { tag: 'assert', weight: Weight.SMALL },\n      { tag: 'check', weight: Weight.SMALL },\n      { tag: 'structure', weight: Weight.MID },\n    ],\n    similar: ['jsonschema', 'joi', 'ajv', 'superstruct', 'yup', 'zod'],\n  },\n  'querystring-parser': {\n    name: 'Query String Parsers',\n    tags: [\n      { tag: 'query string', weight: Weight.NORMAL },\n      { tag: 'querystring', weight: Weight.HIGH },\n      { tag: 'parse', weight: Weight.MID },\n      { tag: 'parser', weight: Weight.MID },\n      { tag: 'url', weight: Weight.MID },\n      { tag: 'search params', weight: Weight.MID },\n      { tag: 'qs', weight: Weight.MID },\n      { tag: 'parameter', weight: Weight.NORMAL },\n      { tag: 'params', weight: Weight.NORMAL },\n    ],\n    similar: ['qs', 'query-string', 'querystringify', 'querystring'],\n  },\n  'rich-text-editors': {\n    name: 'Rich Text Editors',\n    tags: [\n      {\n        tag: 'richtext rich text editor WYSIWYG contenteditable',\n        weight: Weight.NORMAL,\n      },\n    ],\n    similar: ['slate', 'quill', 'draft-js', 'medium-editor', 'froala-editor'],\n  },\n  'site-tour': {\n    name: 'Site Tours',\n    tags: [\n      { tag: 'walkthrough', weight: Weight.NORMAL },\n      { tag: 'focus', weight: Weight.MID },\n      { tag: 'tour', weight: Weight.NORMAL },\n      { tag: 'guide', weight: Weight.NORMAL },\n      { tag: 'user', weight: Weight.SMALL },\n      { tag: 'tutorial', weight: Weight.NORMAL },\n      { tag: 'step', weight: Weight.MID },\n    ],\n    similar: ['driver.js', 'shepherd.js', 'intro.js'],\n  },\n  'state-management': {\n    name: 'State Management Libraries',\n    tags: [\n      { tag: 'state management', weight: Weight.NORMAL },\n      { tag: 'copy-on-write', weight: Weight.MID },\n      { tag: 'immutable', weight: Weight.MID },\n      { tag: 'flux', weight: Weight.HIGH },\n      { tag: 'reducer', weight: Weight.HIGH },\n    ],\n    similar: ['mobx', 'redux', 'immer', 'freactal', 'xstate'],\n  },\n  'svg-manipulation': {\n    name: 'SVG manipulation libraries',\n    tags: [\n      { tag: 'svg', weight: Weight.HIGH },\n      { tag: 'vector', weight: Weight.HIGH },\n      { tag: 'manipulate', weight: Weight.NORMAL },\n      { tag: 'graphics', weight: Weight.MID },\n      { tag: 'animation', weight: Weight.MID },\n      { tag: 'javascript', weight: Weight.SMALL },\n      { tag: 'two dimensional', weight: Weight.MID },\n    ],\n    similar: ['raphael', 'snapsvg', 'two.js'],\n  },\n  'timezone-formatting': {\n    name: 'Timezone Formatting',\n    tags: [\n      { tag: 'date', weight: Weight.NORMAL },\n      { tag: 'time', weight: Weight.NORMAL },\n      { tag: 'timezone', weight: Weight.MAX },\n      { tag: 'parse', weight: Weight.MID },\n      { tag: 'format', weight: Weight.MID },\n    ],\n    similar: [\n      'moment-timezone',\n      'date-time-format-timezone',\n      'spacetime',\n      'date-fns-timezone',\n    ],\n  },\n  uuid: {\n    name: 'Unique ID generators',\n    tags: [\n      { tag: 'uuid', weight: Weight.HIGH },\n      { tag: 'guid', weight: Weight.HIGH },\n      { tag: 'random', weight: Weight.MID },\n      { tag: 'unique', weight: Weight.NORMAL },\n      { tag: 'id', weight: Weight.NORMAL },\n    ],\n    similar: ['uuid', 'shortid', 'nanoid', 'cuid'],\n  },\n  'vanilla-tooltip': {\n    name: 'Tooltip Libraries',\n    tags: [\n      { tag: 'tooltip', weight: Weight.MAX },\n      { tag: 'popover', weight: Weight.NORMAL },\n      { tag: 'hint', weight: Weight.NORMAL },\n    ],\n    similar: ['tooltip.js', 'tippy.js', 'balloon-css', 'hint.css', 'microtip'],\n  },\n  'vanilla-carousel': {\n    name: 'Vanilla JS Sliders & Carousels',\n    tags: [\n      { tag: 'touch swipe', weight: Weight.MID },\n      { tag: 'vanilla', weight: Weight.NORMAL },\n      { tag: 'carousel', weight: Weight.MAX },\n      { tag: 'slider', weight: Weight.MAX },\n    ],\n    similar: ['glider-js', 'slick-carousel', 'swiper', 'flickity'],\n  },\n  'virtual-dom-engine': {\n    name: 'Virtual DOM implementations',\n    tags: [\n      { tag: 'virtual', weight: Weight.NORMAL },\n      { tag: 'dom', weight: Weight.NORMAL },\n      { tag: 'render', weight: Weight.NORMAL },\n      { tag: 'dominant', weight: Weight.HIGH },\n      { tag: 'palette', weight: Weight.HIGH },\n      { tag: 'pixels', weight: Weight.MID },\n    ],\n    similar: ['hyperhtml', 'snabbdom', 'virtual-dom'],\n  },\n  'vue-carousel': {\n    name: 'Vue JS Sliders & Carousels',\n    tags: [\n      { tag: 'vue', weight: Weight.HIGH },\n      { tag: 'touch swipe', weight: Weight.MID },\n      { tag: 'carousel', weight: Weight.NORMAL },\n      { tag: 'slider', weight: Weight.NORMAL },\n    ],\n    similar: [\n      'vue-awesome-swiper',\n      'vue-carousel',\n      'vue-slick',\n      'vue-tiny-slider',\n    ],\n  },\n}\n\nmodule.exports = { categories }\n"
  },
  {
    "path": "server/middlewares/similar-packages/similarPackages.middleware.js",
    "content": "const got = require('got')\nconst remark = require('remark')\nconst strip = require('strip-markdown')\nconst natural = require('natural')\nconst { categories } = require('./fixtures')\nconst debugTest = require('debug')('classifier:test')\nconst flatten = require('flatten')\nconst { parsePackageString } = require('../../../utils/common.utils')\nconst logger = require('../../Logger')\nconst debug = require('debug')('bp:similar')\nconst CONFIG = require('../../config')\n\nconst MIN_CUTOFF_SCORE = 12\n\nconst prefixURL = (url, { base, user, project, head, path }) => {\n  if (url.includes('//')) {\n    return url\n  } else {\n    return new URL(\n      (path ? path.replace(/^\\//, '') + '/' : '') +\n        url.replace(/^(\\.?\\/?)/, ''),\n      `${base}/${user}/${project}/${path ? '' : `${head}/`}`\n    )\n  }\n}\n\nasync function getPackageDetails(packageName) {\n  let readme = ''\n  const { body } = await got(\n    `https://ofcncog2cu-dsn.algolia.net/1/indexes/npm-search/${encodeURIComponent(\n      packageName\n    )}?x-algolia-application-id=OFCNCOG2CU&x-algolia-api-key=f54e21fa3a2a0160595bb058179bfb1e`,\n    { json: true }\n  )\n\n  if ('readme' in body && body.readme.trim()) {\n    readme = await stripMarkdown(body.readme)\n  } else {\n    try {\n      let readmeMD = await getReadme(body.repository)\n      readme = await stripMarkdown(readmeMD)\n    } catch (e) {\n      console.error('error getting readme contents for ' + packageName, e)\n    }\n  }\n\n  return { ...body, readme }\n}\n\nasync function getReadme(repository) {\n  const { host, user, project, branch, path } = repository\n  if (host === 'github.com') {\n    const getGithubFile = async fileName =>\n      await got(\n        prefixURL(fileName, {\n          base: 'https://raw.githubusercontent.com',\n          user,\n          project,\n          head: branch,\n          path: path.replace(/\\/tree\\//, ''),\n        })\n      )\n\n    try {\n      const { body } = await getGithubFile('README.md')\n      return body\n    } catch (e) {\n      try {\n        const { body } = await getGithubFile('readme.md')\n        return body\n      } catch (e) {\n        const { body } = await getGithubFile('Readme.md')\n        return body\n      }\n    }\n  } else if (host === 'gitlab.com') {\n    const getGitlabFile = async ({ user, project, branch, filePath }) => {\n      // We need to use the Gitlab API because the raw url does not support cors\n      // https://gitlab.com/gitlab-org/gitlab-ce/issues/25736\n      // So we need to 'translate' raw urls to api urls.\n      // E.g (https://gitlab.com/janslow/gitlab-fetch/raw/master/CHANGELOG.md) -> (https://gitlab.com/api/v4/projects/janslow%2Fgitlab-fetch/repository/files/CHANGELOG.md?ref=master)\n      // Once gitlab adds support, we can get rid of this workaround.\n      const apiUrl = `https://gitlab.com/api/v4/projects/${user}%2F${project}/repository/files/${encodeURIComponent(\n        filePath\n      )}?ref=${branch}`\n      const { body } = await got(apiUrl, { json: true })\n\n      if (body.encoding === 'base64') {\n        return Buffer.from(body.content, 'base64').toString()\n      } else {\n        return body.content\n      }\n    }\n\n    return getGitlabFile({\n      user,\n      project,\n      branch,\n      filePath: `${path}/README.md`,\n    })\n  } else if (host === 'bitbucket.org') {\n    const { body } = await got(\n      `https://bitbucket.org/${user}/${project}${\n        path ? path.replace('src', 'raw') : `/raw/${branch}`\n      }/README.md`\n    )\n    return body\n  }\n}\n\nasync function stripMarkdown(readme) {\n  return new Promise((resolve, reject) => {\n    remark()\n      .use(strip)\n      .process(readme, function (err, file) {\n        if (err) reject(err)\n        resolve(\n          String(file).replace(\n            /\\b(npm|code|library|Node|example|project|license|MIT)\\b/gi,\n            ''\n          )\n        )\n      })\n  })\n}\n\nfunction getScore(categoryTokens, packageTokens) {\n  const packageTokenWithoutDupes = Array.from(new Set(packageTokens))\n  return packageTokenWithoutDupes.reduce((acc, curToken) => {\n    const match = categoryTokens.find(token => token.tag === curToken)\n    if (match) {\n      return acc + match.weight\n    }\n    return acc\n  }, 0)\n}\n\nfunction getInCategoryMap(packageName) {\n  return Object.keys(categories).find(label =>\n    categories[label].similar.some(\n      similarPackage => similarPackage === packageName\n    )\n  )\n}\n\nasync function getCategory(packageName) {\n  if (getInCategoryMap(packageName)) {\n    return {\n      label: getInCategoryMap(packageName),\n      score: 999,\n    }\n  }\n\n  const { description, keywords } = await getPackageDetails(packageName)\n  const tokenizer = new natural.WordTokenizer()\n  const tokenString =\n    (await stripMarkdown(description)) + ' ' + keywords.join(' ')\n  const packageTokens = tokenizer\n    .tokenize(tokenString)\n    .map(token => token.toLowerCase())\n    .map(natural.PorterStemmer.stem)\n    .concat(tokenizer.tokenize(packageName).map(natural.PorterStemmer.stem))\n\n  const scores = {}\n  let maxScoreCategory = {\n    category: '',\n    score: 0,\n  }\n\n  Object.keys(categories).forEach(label => {\n    const categoryTokens = flatten(\n      categories[label].tags.map(tagObj =>\n        tokenizer.tokenize(tagObj.tag).map(tokenizedTag => ({\n          tag: natural.PorterStemmer.stem(tokenizedTag).toLowerCase(),\n          weight: tagObj.weight,\n        }))\n      )\n    )\n\n    const score = getScore(categoryTokens, packageTokens)\n    scores[label] = score\n    if (score > maxScoreCategory.score) {\n      maxScoreCategory = {\n        label,\n        score,\n      }\n    }\n  })\n\n  return maxScoreCategory\n}\n\nasync function test() {\n  Object.keys(categories).forEach(label => {\n    categories[label].similar.forEach(async pack => {\n      const actualCategory = await getCategory(pack)\n\n      if (\n        !actualCategory ||\n        actualCategory.category !== label ||\n        actualCategory.score < MIN_CUTOFF_SCORE\n      ) {\n        debugTest(\n          'Package %s. Category expected: %s, got: %o',\n          pack,\n          label,\n          actualCategory\n        )\n      }\n    })\n  })\n}\n\nasync function similarPackagesMiddleware(ctx) {\n  const { name } = parsePackageString(ctx.query.package)\n\n  try {\n    const matchedCategory = await getCategory(name)\n    debug('Category for %s : %o', name, matchedCategory)\n    if (matchedCategory.label) {\n      const value = categories[matchedCategory.label]\n\n      ctx.cacheControl = {\n        maxAge: CONFIG.CACHE.SIMILAR_API,\n      }\n\n      ctx.body = {\n        name,\n        category: {\n          ...matchedCategory,\n          label: value.name,\n          tags: value.tags,\n          similar: value.similar.filter(pack => pack !== name),\n        },\n      }\n    } else {\n      ctx.body = {\n        name,\n        category: {\n          label: null,\n          score: 0,\n          similarPackages: [],\n        },\n      }\n    }\n  } catch (err) {\n    console.error(err)\n    ctx.status = 500\n    ctx.body = {\n      error: err,\n    }\n\n    logger.error(\n      'SIMILAR_PACKAGES_ERROR',\n      {\n        type: 'SIMIAR_PACKAGES',\n        requestId: ctx.state.id,\n        name,\n        details: err,\n      },\n      `SIMILAR PACKAGES FAILED: ${name}`\n    )\n  }\n}\n\nmodule.exports = similarPackagesMiddleware\n"
  },
  {
    "path": "server/worker.js",
    "content": "const workerpool = require('workerpool')\nconst {\n  getPackageStats,\n  getAllPackageExports,\n  getPackageExportSizes,\n} = require('package-build-stats')\n\n// create a worker and register public functions\nworkerpool.worker({\n  getPackageStats,\n  getAllPackageExports,\n  getPackageExportSizes,\n})\n"
  },
  {
    "path": "stylesheets/base.scss",
    "content": "@import './colors';\n@import './variables';\n@import '../node_modules/normalize.css/normalize';\n@import '../node_modules/balloon-css/balloon';\n\n* > * {\n  box-sizing: border-box;\n}\n\nbody,\nhtml {\n  background: white;\n  font-family: $font-family-body;\n  max-width: 100vw;\n  overflow-x: hidden;\n  color: #212121;\n}\n\ncode {\n  font-family: $font-family-code;\n}\n\n// reset svg value received from normalize\nsvg:not(:root) {\n  overflow: visible;\n}\n\n::-moz-selection {\n  background: rgba(0, 170, 255, 0.2);\n}\n\n::selection {\n  background: rgba(0, 170, 255, 0.2);\n}\n\na {\n  text-decoration: none;\n}\n\n// Tooltips\n\n[data-balloon]::before,\n[data-balloon]::after {\n  transition-delay: 0.15s;\n  white-space: pre;\n}\n\n[data-balloon]::after {\n  background: rgba(60, 60, 60, 0.9);\n}\n\n[data-balloon]:hover::before {\n  opacity: 0.82;\n}\n\ninput {\n  background: transparent;\n  line-height: 1;\n  border: 1px solid $autocomplete-border-color;\n  transition: border-top-left-radius 0.1s, border-top-right-radius 0.1s;\n  border-radius: 0.3em;\n  padding: $global-spacing * 0.5 $global-spacing;\n\n  &:focus {\n    outline: none;\n    caret-color: $pastel-green;\n  }\n\n  &::placeholder {\n    color: lighten($raven, 20%);\n  }\n}\n\n// Safari's caret color is equal to the\n// text color. Hence, we need to set a\n// text color targeting just Safari :(\n@media not all and (min-resolution: 0.001dpcm) {\n  @supports (-webkit-appearance: none) {\n    input {\n      color: #ccc;\n      -webkit-text-stroke: 2px white;\n\n      &::placeholder {\n        -webkit-text-stroke: 0px white;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "stylesheets/colors.scss",
    "content": "//  Use http://www.htmlcsscolor.com to name colors\n$gulf-blue: #2f3f5f;\n$dark-gulf-blue: #152231;\n$neptune: #82b5b3;\n$cornflower-blue: #65a1f8;\n$maya-blue: #65c3f8;\n$pastel-green: #7cd690;\n$dandelion: #fff3cf;\n$carrot-orange: #eb841f;\n$dark-raven: #5c5c66;\n$raven: #666e78;\n\n$autocomplete-border-color: transparentize(black, 0.93);\n"
  },
  {
    "path": "stylesheets/index.scss",
    "content": "// pages\n@import '../pages/compare/ComparePage.scss';\n@import '../pages/package/[...packageString]/components/ExportAnalysisSection/ExportAnalysisSection.scss';\n@import '../pages/package/[...packageString]/components/InterLinksSection/InterLinksSectionCard/InterLinksSectionCard.scss';\n@import '../pages/package/[...packageString]/components/SimilarPackagesSection/SimilarPackagesSection.scss';\n@import '../pages/package/[...packageString]/components/InterLinksSection/InterLinksSection.scss';\n@import '../pages/package/[...packageString]/ResultPage.scss';\n@import '../pages/scan/Scan.scss';\n@import '../pages/scan-results/ScanResults.scss';\n@import '../pages/index.scss';\n\n// components\n@import '../client/components/AnnouncementBanner/AnnouncementBanner.scss';\n@import '../client/components/AutocompleteInput/AutocompleteInput.scss';\n@import '../client/components/AutocompleteInputBox/AutocompleteInputBox.scss';\n@import '../client/components/BarGraph/BarGraph.scss';\n@import '../client/components/BarVersion/BarVersion.scss';\n@import '../client/components/BlogLayout/BlogLayout.scss';\n@import '../client/components/BuildProgressIndicator/BuildProgressIndicator.scss';\n@import '../client/components/JumpingDots/JumpingDots.scss';\n@import '../client/components/Layout/Layout.scss';\n@import '../client/components/ProgressHex/ProgressHex.scss';\n@import '../client/components/QuickStatsBar/QuickStatsBar.scss';\n@import '../client/components/ResultLayout/ResultLayout.scss';\n@import '../client/components/Icons/SideEffectIcon.scss';\n@import '../client/components/Stat/Stat.scss';\n@import '../client/components/SimilarPackageCard/SimilarPackageCard.scss';\n@import '../client/components/Icons/TreeShakeIcon.scss';\n@import '../client/components/Warning/Warning.scss';\n"
  },
  {
    "path": "stylesheets/mixins.scss",
    "content": "@mixin horizontal-scroll-overflow-indicators {\n  overflow-y: scroll;\n  height: 100%;\n  background-image: /* Shadows */ linear-gradient(to right, white, white),\n    linear-gradient(to right, white, white),\n    /* Shadow covers */\n      linear-gradient(to right, rgba(30, 30, 30, 0.08), rgba(255, 255, 255, 0)),\n    linear-gradient(to left, rgba(30, 30, 30, 0.08), rgba(255, 255, 255, 0));\n\n  background-position: left center, right center, left center, right center;\n  background-repeat: no-repeat;\n  background-size: 2vw 100%, 2vw 100%, 1vw 100%, 1vw 100%;\n\n  /* Opera doesn't support this in the shorthand */\n  background-attachment: local, local, scroll, scroll;\n}\n\n@mixin hide-scrollbars {\n  /* this will hide the scrollbar in mozilla based browsers */\n  overflow: -moz-scrollbars-none;\n  scrollbar-width: none;\n  /* this will hide the scrollbar in internet explorers */\n  -ms-overflow-style: none;\n\n  /* this will hide the scrollbar in webkit based browsers - safari, chrome, etc */\n  &::-webkit-scrollbar {\n    width: 0 !important;\n    display: none;\n  }\n}\n"
  },
  {
    "path": "stylesheets/variables.scss",
    "content": "// Font related\n@mixin font-size-xxxl {\n  font-size: 5.5rem;\n\n  @media screen and (max-width: 48em) {\n    font-size: 4rem;\n  }\n\n  @media screen and (max-width: 40em) {\n    font-size: 3rem;\n  }\n}\n\n@mixin font-size-xxl {\n  font-size: 3rem;\n\n  @media screen and (max-width: 48em) {\n    font-size: 2.5rem;\n  }\n\n  @media screen and (max-width: 40em) {\n    font-size: 2rem;\n  }\n}\n\n@mixin font-size-xl {\n  font-size: 2.4rem;\n\n  @media screen and (max-width: 40em) {\n    font-size: 1.4rem;\n  }\n}\n\n@mixin font-size-lg {\n  font-size: 2rem;\n\n  @media screen and (max-width: 40em) {\n    font-size: 1.45rem;\n  }\n}\n\n@mixin font-size-md {\n  font-size: 1.35rem;\n\n  @media screen and (max-width: 48em) {\n    font-size: 1.25rem;\n  }\n\n  @media screen and (max-width: 40em) {\n    font-size: 1.05rem;\n  }\n}\n\n@mixin font-size-md-2 {\n  font-size: 1.7rem;\n\n  @media screen and (max-width: 40em) {\n    font-size: 1.25rem;\n  }\n}\n\n@mixin font-size-reg {\n  font-size: 1rem;\n\n  @media screen and (max-width: 48em) {\n    font-size: 0.9rem;\n  }\n\n  @media screen and (max-width: 40em) {\n    font-size: 0.8rem;\n  }\n}\n\n@mixin font-size-sm {\n  font-size: 0.9rem;\n\n  @media screen and (max-width: 48em) {\n    font-size: 0.8rem;\n  }\n\n  @media screen and (max-width: 40em) {\n    font-size: 0.75rem;\n  }\n}\n\n@mixin font-size-xs {\n  font-size: 0.8rem;\n\n  @media screen and (max-width: 48em) {\n    font-size: 0.75rem;\n  }\n\n  @media screen and (max-width: 40em) {\n    font-size: 0.7rem;\n  }\n}\n\n@mixin font-size-xxs {\n  font-size: 0.7rem;\n\n  @media screen and (max-width: 48em) {\n    font-size: 0.65rem;\n  }\n\n  @media screen and (max-width: 40em) {\n    font-size: 0.65rem;\n  }\n}\n\n$font-weight-hairline: 200;\n$font-weight-thin: 300;\n$font-weight-light: 400;\n$font-weight-bold: 600;\n$font-weight-very-bold: 700;\n\n$font-family-body: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,\n  Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;\n$font-family-code: 'Source Code Pro', 'SF Mono', Consolas, 'Liberation Mono',\n  Menlo, Courier, monospace;\n\n// Padding / Spacing\n$global-spacing: 10px;\n\n// Content width\n$max-content-width: $global-spacing * 85;\n"
  },
  {
    "path": "test-packages/blacklist-error/index.js",
    "content": "console.log(\"I'm not a blacklisted package, hence will throw\")\n"
  },
  {
    "path": "test-packages/blacklist-error/package.json",
    "content": "{\n  \"name\": \"@bundlephobia/test-hack-unlimited-2018\",\n  \"version\": \"0.0.3\",\n  \"license\": \"MIT\",\n  \"private\": false\n}\n"
  },
  {
    "path": "test-packages/build-error/index.js",
    "content": "#!/usr/bin/node node\nconsole.log('This is a cli shell script')\n"
  },
  {
    "path": "test-packages/build-error/package.json",
    "content": "{\n  \"name\": \"@bundlephobia/test-build-error\",\n  \"version\": \"0.0.2\",\n  \"license\": \"MIT\",\n  \"private\": false\n}\n"
  },
  {
    "path": "test-packages/entry-point-error/package.json",
    "content": "{\n  \"name\": \"@bundlephobia/test-entry-point-error\",\n  \"version\": \"0.0.1\",\n  \"license\": \"MIT\",\n  \"private\": false\n}\n"
  },
  {
    "path": "test-packages/entry-point-error/random-js-file.js",
    "content": "console.log(\"I'm not an entry point, hence will throw\")\n"
  },
  {
    "path": "test-packages/missing-dependency-error/index.js",
    "content": "const test = require('missing-package')\nconsole.log('I have missing dependencies')\n"
  },
  {
    "path": "test-packages/missing-dependency-error/package.json",
    "content": "{\n  \"name\": \"@bundlephobia/missing-dependency-error\",\n  \"version\": \"0.0.1\",\n  \"license\": \"MIT\",\n  \"private\": false\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "tsconfig.server.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"target\": \"es2017\"\n  },\n  \"ts-node\": {\n    \"swc\": true\n  }\n}\n"
  },
  {
    "path": "types/amplitude.d.ts",
    "content": "// stub typings for amplitudeScript loaded in the _document\ndeclare global {\n  var amplitude: {\n    getInstance: () => {\n      logEvent: (event: string, data?: Record<string, unknown>) => void\n    }\n  }\n}\n\nexport {}\n"
  },
  {
    "path": "types/index.ts",
    "content": "import React from 'react'\n\nexport type PackageInfo = {\n  name: string\n  description: string\n  repository: string\n  dependencyCount: number\n  isTreeShakeable: boolean\n  hasSideEffects: string[] | boolean\n}\n\nexport type WithClassName = Pick<React.HTMLAttributes<HTMLElement>, 'className'>\n"
  },
  {
    "path": "types/react-contentful.d.ts",
    "content": "import { HookQueryProps } from 'react-contentful'\nimport { Document } from '@contentful/rich-text-types'\n\ndeclare module 'react-contentful' {\n  interface Item {\n    fields: {\n      title: string\n      content: Document\n      slug: string\n      createdAt: string\n    }\n    sys: {\n      createdAt: string\n    }\n  }\n\n  interface Data {\n    items: Item[]\n  }\n\n  interface TypedHookResponse {\n    data?: Data\n    error?: any\n    fetched: boolean\n    loading: boolean\n  }\n\n  export function useContentful(props: HookQueryProps): TypedHookResponse\n}\n"
  },
  {
    "path": "utils/cache.utils.js",
    "content": "require('dotenv-defaults').config()\nconst debug = require('debug')('bp:cache')\nconst axios = require('axios')\nconst logger = require('../server/Logger')\n\nconst API = axios.create({\n  baseURL: process.env.CACHE_SERVICE_ENDPOINT,\n  timeout: 5000,\n})\n\nclass Cache {\n  async getPackageSize({ name, version }) {\n    try {\n      const result = await API.get('/package-cache', {\n        params: { name, version },\n      })\n      return result.data\n    } catch (err) {\n      console.error(err.statusText)\n    }\n  }\n\n  async setPackageSize({ name, version }, result) {\n    debug('set package %O to %O', { name, version }, result)\n    try {\n      await API.post('/package-cache', { name, version, result })\n    } catch (err) {\n      console.error(err.data)\n      logger.error(\n        'CACHE_SET_ERROR',\n        {\n          name,\n          version,\n          error: err.data,\n        },\n        `CACHE ERROR for package ${name}@${version}`\n      )\n    }\n  }\n\n  async getExportsSize({ name, version }) {\n    debug('get exports %s@%s', name, version)\n    try {\n      const result = await API.get('/exports-cache', {\n        params: { name, version },\n      })\n      debug('cache hit')\n      return result.data\n    } catch (err) {}\n  }\n\n  async setExportsSize({ name, version }, result) {\n    debug('set exports %O to %O', { name, version }, result)\n    try {\n      await API.post('/exports-cache', { name, version, result })\n    } catch (err) {\n      console.error(err.data)\n      logger.error(\n        'CACHE_SET_ERROR',\n        {\n          name,\n          version,\n          error: err.data,\n        },\n        `CACHE ERROR for package exports ${name}@${version}`\n      )\n    }\n  }\n}\n\nmodule.exports = Cache\n"
  },
  {
    "path": "utils/common.utils.js",
    "content": "// Used by the server as well as the client\n// Use ES5 only\n\nconst DOMPurify = require('dompurify')\n\nfunction parsePackageString(packageString) {\n  // Scoped packages\n  let name,\n    version,\n    scope,\n    scoped = false\n  const lastAtIndex = packageString.lastIndexOf('@')\n  const firstSlashIndex = packageString.indexOf('/')\n\n  if (packageString.startsWith('@')) {\n    scoped = true\n    scope = packageString.substring(1, firstSlashIndex)\n    if (lastAtIndex === 0) {\n      name = packageString\n      version = null\n    } else {\n      name = packageString.substring(0, lastAtIndex)\n      version = packageString.substring(lastAtIndex + 1)\n    }\n  } else {\n    if (lastAtIndex === -1) {\n      name = packageString\n      version = null\n    } else {\n      name = packageString.substring(0, lastAtIndex)\n      version = packageString.substring(lastAtIndex + 1)\n    }\n  }\n\n  return { name, version, scope, scoped }\n}\n\nfunction daysFromToday(date) {\n  const date1 = new Date()\n  const date2 = new Date(date)\n  const diffTime = Math.abs(date2 - date1)\n  const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))\n  return diffDays\n}\n\nfunction sanitizeHTML(html) {\n  return DOMPurify.sanitize(html, {\n    ALLOWED_TAGS: ['b', 'i', 'div'],\n    ALLOWED_ATTR: [''],\n  })\n}\n\nmodule.exports = { parsePackageString, daysFromToday, sanitizeHTML }\n"
  },
  {
    "path": "utils/draw.utils.js",
    "content": "const fabric = require('fabric/node')\nconst { formatSize, formatTime, getTimeFromSize } = require('./index')\n\nfunction drawStatsImg({\n  name,\n  version,\n  min,\n  gzip,\n  theme = 'dark',\n  wide = false,\n}) {\n  const lightTheme = {\n    backgroundColor: '#fff',\n    separatorColor: '#E7E7E7',\n    separatorOpacity: 1,\n    nameColor: '#000',\n    versionColor: '#979797',\n    versionOpacity: 1,\n    numberColor: '#333',\n    numberOpacity: 1,\n    unitColor: '#7D828C',\n    unitOpacity: 1,\n    labelColor: '#54575C',\n    labelOpacity: 1,\n  }\n\n  const darkTheme = {\n    backgroundColor: '#182330',\n    separatorColor: '#fff',\n    separatorOpacity: 0.12,\n    nameColor: '#fff',\n    versionColor: '#fff',\n    versionOpacity: 0.6,\n    numberColor: '#fff',\n    numberOpacity: 0.8,\n    unitColor: '#E5EEFF',\n    unitOpacity: 0.5,\n    labelColor: '#fff',\n    labelOpacity: 0.55,\n  }\n\n  const width = 624\n  const height = 350\n  const pad = 5\n  const wideBy = 25\n\n  const selectedTheme = theme === 'light' ? lightTheme : darkTheme\n  // fabric.devicePixelRatio = 1.5 // Might need to check if this is still supported or needed the same way\n\n  const canvas = new fabric.StaticCanvas('c', {\n    backgroundColor: selectedTheme.backgroundColor,\n    width: wide ? width + wideBy : width,\n    height: height,\n  })\n\n  // Set export scaling\n  canvas.enableRetinaScaling = true\n  canvas.setDimensions(\n    { width: canvas.width * 1.5, height: canvas.height * 1.5 },\n    { cssOnly: true }\n  )\n  // Wait, fabric.devicePixelRatio affected serialization?\n  // v6 might handle retina scaling differently or via viewportTransform upon export.\n  // For now let's leave devicePixelRatio commented out if unsure or rely on defaults.\n  // Actually createJPEGStream comes from node-canvas (or fabric enhancement).\n\n  const x0 = wide ? wideBy / 2 : 0\n\n  const separatorOptions = {\n    stroke: selectedTheme.separatorColor,\n    strokeWidth: 0.5,\n    opacity: selectedTheme.separatorOpacity,\n  }\n\n  const lineTopHorizontal = new fabric.Line(\n    [x0, 91, width, 91],\n    separatorOptions\n  )\n  const lineCenterVertical = new fabric.Line(\n    [width / 2, 91, width / 2, height],\n    separatorOptions\n  )\n  const lineCenterHorizontal = new fabric.Line(\n    [x0, 91 + (height - 91) / 2, width, 91 + (height - 91) / 2],\n    separatorOptions\n  )\n\n  const packageNameText = new fabric.Text(name, {\n    fontFamily: 'Source Code Pro',\n    fontSize: 45,\n    fill: selectedTheme.nameColor,\n    opacity: 0.8,\n    top: 19,\n  })\n\n  const packageAtText = new fabric.Text('@', {\n    fontFamily: 'Source Code Pro',\n    fontSize: 35,\n    fill: '#91D396',\n    left: packageNameText.width + pad * 2,\n    top: 24,\n  })\n\n  // packageAtText.top =\n  //   packageNameText.top + (packageNameText.height - packageAtText.height) / 2\n\n  const packageVersionText = new fabric.Text(version, {\n    fontFamily: 'Source Code Pro',\n    fontSize: 35,\n    fill: selectedTheme.versionColor,\n    opacity: selectedTheme.versionOpacity,\n    left: packageNameText.width + packageAtText.width + +pad * 4,\n    top: 28,\n  })\n\n  // packageVersionText.top =\n  //   packageNameText.top +\n  //   (packageNameText.height - packageVersionText.height) / 2 +\n  //   pad / 2\n\n  const packageNameGroup = new fabric.Group(\n    [packageNameText, packageAtText, packageVersionText],\n    { selectable: false /* top: 91 / 2, originY: 'center'*/ }\n  )\n\n  // const packageNameGroup = new fabric.Group(\n  //   [packageNameText, packageAtText, packageVersionText],\n  //   { selectable: false, /* top: 91 / 2, originY: 'center'*/ },\n  // )\n\n  function createStatGroup(number, unit, label, opts) {\n    const numberText = new fabric.Text(number.toString(), {\n      fontFamily: 'SF Compact Text',\n      fontSize: 55,\n      fill: selectedTheme.numberColor,\n      fontWeight: 'bold',\n      opacity: selectedTheme.numberOpacity,\n    })\n\n    const unitText = new fabric.Text(unit, {\n      fontFamily: 'SF Compact Text',\n      fontSize: 35,\n      fill: selectedTheme.unitColor,\n      fontWeight: 'bold',\n      opacity: selectedTheme.unitOpacity,\n      left: numberText.width + pad / 2,\n    })\n\n    unitText.top = numberText.top + numberText.height - unitText.height - pad\n\n    const labelText = new fabric.Text(label, {\n      fontFamily: 'SF Compact Text',\n      fontSize: 25,\n      fontWeight: 100,\n      fill: selectedTheme.labelColor,\n      opacity: selectedTheme.labelOpacity,\n      top: numberText.height,\n      left: (numberText.width + unitText.width) / 2,\n      originX: 'center',\n    })\n\n    return new fabric.Group([numberText, unitText, labelText], opts)\n  }\n\n  const minSize = formatSize(min)\n  const gzipSize = formatSize(gzip)\n  const times = getTimeFromSize(gzip)\n  const threeGTime = formatTime(times.threeG)\n  const fourGTime = formatTime(times.fourG)\n\n  const minGroup = createStatGroup(\n    minSize.size.toFixed(2),\n    minSize.unit,\n    'minified',\n    {\n      // originY: 'center',\n      originX: 'center',\n      // top: 91 + (height - 91) / 4,\n      top: 106,\n      left: width / 4,\n    }\n  )\n\n  const gzipGroup = createStatGroup(\n    gzipSize.size.toFixed(2),\n    gzipSize.unit,\n    'gzipped',\n    {\n      // originY: 'center',\n      originX: 'center',\n      // top: 91 + (height - 91) / 4,\n      top: 106,\n      left: width * (3 / 4),\n    }\n  )\n\n  const threeGGroup = createStatGroup(\n    threeGTime.unit === 'ms' ? threeGTime.size : threeGTime.size.toFixed(1),\n    threeGTime.unit,\n    'slow 3G',\n    {\n      // originY: 'center',\n      originX: 'center',\n      // top: 91 + (height - 91) * (3 / 4),\n      top: 235,\n      left: width / 4,\n    }\n  )\n\n  const fourGGroup = createStatGroup(\n    fourGTime.unit === 'ms' ? fourGTime.size : fourGTime.size.toFixed(1),\n    fourGTime.unit,\n    'emerging 4G',\n    {\n      // originY: 'center',\n      originX: 'center',\n      // top: 91 + (height - 91) * (3 / 4),\n      top: 235,\n      left: width * (3 / 4),\n    }\n  )\n\n  canvas\n    .add(lineTopHorizontal)\n    .add(lineCenterVertical)\n    .add(lineCenterHorizontal)\n    .add(packageNameGroup)\n    .add(minGroup)\n    .add(gzipGroup)\n    .add(threeGGroup)\n    .add(fourGGroup)\n\n  canvas.centerObjectH(packageNameGroup)\n  canvas.renderAll()\n  return canvas.createJPEGStream()\n}\n\nmodule.exports = { drawStatsImg }\n"
  },
  {
    "path": "utils/firebase.utils.js",
    "content": "const { decodeFirebaseKey, encodeFirebaseKey } = require('./index')\nconst semver = require('semver')\nconst fetch = require('node-fetch')\nconst axios = require('axios')\nconst firebase = require('firebase')\nconst debug = require('debug')('bp:firebase-util')\n\n// Configurable Firebase key for reading package history\nconst FIREBASE_READ_KEY = process.env.FIREBASE_READ_KEY || 'modules-v2'\n\nif (process.env.FIREBASE_DATABASE_URL) {\n  const firebaseConfig = {\n    apiKey: process.env.FIREBASE_API_KEY,\n    authDomain: process.env.FIREBASE_AUTH_DOMAIN,\n    databaseURL: process.env.FIREBASE_DATABASE_URL,\n  }\n\n  firebase.initializeApp(firebaseConfig)\n}\n\nclass FirebaseUtils {\n  constructor(firebaseInstance, enable = true) {\n    if (enable) {\n      this.firebase = firebaseInstance\n    }\n  }\n\n  setRecentSearch(name, packageInfo) {\n    if (!this.firebase) {\n      return\n    }\n\n    const searches = this.firebase.database().ref().child('searches-v2')\n    searches\n      .child(encodeFirebaseKey(name))\n      .once('value')\n      .then(snapshot => snapshot.val())\n      .then(res => {\n        if (res) {\n          return searches\n            .child(encodeFirebaseKey(name))\n            .update({\n              lastSearched: new Date().getTime(),\n              name: packageInfo.name,\n              count: res.count + 1,\n            })\n            .catch(err => console.log(err))\n        } else {\n          return searches\n            .child(encodeFirebaseKey(name))\n            .set({\n              lastSearched: new Date().getTime(),\n              name: packageInfo.name,\n              version: packageInfo.version,\n              count: 1,\n            })\n            .catch(err => console.log(err))\n        }\n      })\n  }\n\n  async getPackageHistory(name, limit = 15) {\n    if (!this.firebase) {\n      return {}\n    }\n\n    debug('package history %s', name)\n    const packageHistory = {}\n\n    // Helper to get history from a specific Firebase key\n    const getHistoryFromKey = async key => {\n      const ref = this.firebase\n        .database()\n        .ref()\n        .child(key)\n        .child(encodeFirebaseKey(name))\n      return ref.once('value').then(snapshot => snapshot.val())\n    }\n\n    // Try primary key first, then fallback if using v3\n    const firebasePromise = (async () => {\n      const result = await getHistoryFromKey(FIREBASE_READ_KEY)\n      if (result) {\n        debug('package history from %s', FIREBASE_READ_KEY)\n        return result\n      }\n      // Fallback to v2 if reading from v3\n      if (\n        FIREBASE_READ_KEY === 'modules-v3' &&\n        !process.env.DISABLE_FIREBASE_V2_FALLBACK\n      ) {\n        const fallback = await getHistoryFromKey('modules-v2')\n        if (fallback) {\n          debug('package history from modules-v2 (fallback)')\n        }\n        return fallback\n      }\n      return null\n    })()\n\n    const yarnPromise = axios.get(\n      `https://${\n        process.env.ALGOLIA_APP_ID\n      }-dsn.algolia.net/1/indexes/npm-search/${encodeURIComponent(name)}`,\n      {\n        params: {\n          'x-algolia-agent': 'bundlephobia',\n          'x-algolia-application-id': process.env.ALGOLIA_APP_ID,\n          'x-algolia-api-key': process.env.ALGOLIA_API_KEY,\n        },\n      }\n    )\n\n    let firebaseHistory, versions\n    try {\n      const [firebaseResult, yarnInfo] = await Promise.all([\n        firebasePromise,\n        yarnPromise,\n      ])\n\n      firebaseHistory = firebaseResult\n      yarnInfo.data.versions = {\n        [yarnInfo.data.version]: '',\n        ...yarnInfo.data.versions,\n      }\n      versions = Object.keys(yarnInfo.data.versions)\n    } catch (err) {\n      console.error(err)\n      firebaseHistory = await firebasePromise\n      versions = Object.keys(firebaseHistory || {}).map(version =>\n        decodeFirebaseKey(version)\n      )\n    }\n\n    const filteredVersions = versions\n      // We *may not* want all tagged alpha/beta versions\n      .filter(version => !version.includes('-'))\n      .sort((versionA, versionB) => semver.compare(versionA, versionB))\n\n    const limitedVersions = filteredVersions.splice(\n      filteredVersions.length - limit\n    )\n    debug('last npm  %d %s versions %o', limit, name, limitedVersions)\n\n    // Although if the most recent version is tagged,\n    // including it might be of interest\n    if (versions[versions.length - 1].includes('-')) {\n      limitedVersions.shift()\n      limitedVersions.push(versions[versions.length - 1])\n    }\n\n    limitedVersions.forEach(version => {\n      packageHistory[version] = {}\n    })\n\n    if (!firebaseHistory) {\n      return packageHistory\n    }\n\n    // debug('searched history %s %o', name, Object.keys(firebaseHistory))\n    Object.keys(firebaseHistory).forEach(version => {\n      const decodedVersion = decodeFirebaseKey(version)\n      if (limitedVersions.includes(decodedVersion)) {\n        packageHistory[decodedVersion] = firebaseHistory[version]\n      }\n    })\n    return packageHistory\n  }\n\n  getRecentSearches(limit = 10) {\n    if (!this.firebase) {\n      return {}\n    }\n\n    const searches = this.firebase.database().ref().child('searches-v2')\n    const recentSearches = {}\n\n    return searches\n      .orderByChild('lastSearched')\n      .limitToLast(Number(limit))\n      .once('value')\n      .then(snapshot => snapshot.val())\n      .then(result => {\n        if (!result) {\n          return recentSearches\n        }\n\n        Object.keys(result).forEach(search => {\n          recentSearches[decodeFirebaseKey(search)] = result[search]\n        })\n        return recentSearches\n      })\n  }\n\n  async getDailySearches() {\n    if (!this.firebase) {\n      return {}\n    }\n\n    const dailySearches = {}\n    const searches = this.firebase.database().ref().child('searches-v2')\n\n    const snapshot = await searches\n      .orderByChild('lastSearched')\n      .startAt(Date.now() - 1000 * 60 * 60 * 24 * 4, 'lastSearched')\n      .once('value')\n    const packages = snapshot.val()\n\n    if (packages) {\n      Object.keys(packages).forEach(packageName => {\n        dailySearches[decodeFirebaseKey(packageName)] = packages[packageName]\n      })\n    }\n    return dailySearches\n  }\n}\n\nmodule.exports = new FirebaseUtils(\n  firebase,\n  !!process.env.FIREBASE_DATABASE_URL\n)\n"
  },
  {
    "path": "utils/index.js",
    "content": "// Firebase does not accept a\n// few special characters for keys\nfunction encodeFirebaseKey(key) {\n  return key.replace(/[.]/g, ',').replace(/\\//g, '__')\n}\n\nfunction decodeFirebaseKey(key) {\n  return key.replace(/[,]/g, '.').replace(/__/g, '/')\n}\n\nconst formatSize = value => {\n  let unit, size\n  if (Math.log10(value) < 3) {\n    unit = 'B'\n    size = value\n  } else if (Math.log10(value) < 6) {\n    unit = 'kB'\n    size = value / 1024\n  } else {\n    unit = 'MB'\n    size = value / 1024 / 1024\n  }\n\n  return { unit, size }\n}\n\nconst formatTime = value => {\n  let unit, size\n  if (value < 0.0005) {\n    unit = 'μs'\n    size = Math.round(value * 1000000)\n  } else if (value < 0.5) {\n    unit = 'ms'\n    size = Math.round(value * 1000)\n  } else {\n    unit = 's'\n    size = value\n  }\n\n  return { unit, size }\n}\n\n// Picked up from http://www.webpagetest.org/\n// Speed in KB/s\n\nconst DownloadSpeed = {\n  THREE_G: 400 / 8, // Slow 3G\n  FOUR_G: 7000 / 8, // 4G\n}\nconst getTimeFromSize = sizeInBytes => {\n  return {\n    threeG: sizeInBytes / 1024 / DownloadSpeed.THREE_G,\n    fourG: sizeInBytes / 1024 / DownloadSpeed.FOUR_G,\n  }\n}\n\nfunction randomFromArray(arr) {\n  return arr[Math.floor(Math.random() * arr.length)]\n}\n\nfunction zeroToN(n) {\n  return Array.from(Array(n).keys())\n}\n\nfunction resolveBuildError(resultsError) {\n  if (!resultsError) {\n    return {\n      errorName: null,\n      errorBody: null,\n      errorDetails: null,\n    }\n  }\n  const errorName = resultsError.error\n    ? resultsError.error.code\n    : 'InternalServerError'\n  const errorBody = resultsError.error\n    ? resultsError.error.message\n    : 'Something went wrong!'\n  const errorDetails =\n    resultsError.error &&\n    resultsError.error.details &&\n    resultsError.error.details.originalError\n      ? Array.isArray(resultsError.error.details.originalError)\n        ? resultsError.error.details.originalError[0]\n        : resultsError.error.details.originalError.toString()\n      : null\n\n  return {\n    errorName,\n    errorBody,\n    errorDetails,\n  }\n}\n\nmodule.exports = {\n  encodeFirebaseKey,\n  decodeFirebaseKey,\n  formatTime,\n  formatSize,\n  getTimeFromSize,\n  randomFromArray,\n  zeroToN,\n  resolveBuildError,\n  DownloadSpeed,\n}\n"
  },
  {
    "path": "utils/rebuild.utils.js",
    "content": "const { blackList } = require('../server/config')\n\nrequire('dotenv-defaults').config()\nconst firebase = require('firebase')\nconst axios = require('axios')\nconst fetch = require('node-fetch')\nconst fs = require('fs')\nconst path = require('path')\nconst Queue = require('promise-queue-plus')\nconst debug = require('debug')('rebuild:script')\nconst debugWarning = require('debug')('rebuild:warning')\nconst got = require('got')\nconst gitURLParse = require('git-url-parse')\nconst { resolvePackage } = require('./server.utils')\nconst { parsePackageString } = require('./common.utils')\nconst deepEqual = require('lodash.isequal')\nconst childProcess = require('child_process')\nconst mkdir = require('mkdir-promise')\n\nconst patchedDB = {}\n\nfunction commit() {\n  try {\n    const stringified = JSON.stringify(patchedDB, null, 2)\n    fs.writeFileSync('./db-patched.json', stringified, 'utf8')\n  } catch (err) {\n    console.error(err)\n  }\n}\n\nconst queue = new Queue(10, {\n  retry: 2, //Number of retries\n  retryIsJump: false, //retry now?\n  timeout: 0,\n  // queueEnd: () => {\n  //   try {\n  //\n  //     const stringified = JSON.stringify(patchedDB, null, 2)\n  //     fs.writeFileSync('./db-patched.json', stringified, 'utf8');\n  //   } catch (err) {\n  //     console.error(err)\n  //   }\n  //   // console.log('done', patchedDB)\n  // }\n})\n\nconst firebaseConfig = {\n  apiKey: process.env.FIREBASE_API_KEY,\n  authDomain: process.env.FIREBASE_AUTH_DOMAIN,\n  databaseURL: process.env.FIREBASE_DATABASE_URL,\n}\n\nfirebase.initializeApp(firebaseConfig)\n\nfunction encodeFirebaseKey(key) {\n  return key.replace(/[.]/g, ',').replace(/\\//g, '__')\n}\n\nfunction decodeFirebaseKey(key) {\n  return key.replace(/[,]/g, '.').replace(/__/g, '/')\n}\n\nasync function getFirebaseStore() {\n  try {\n    const snapshot = await firebase.database().ref('modules-v2').once('value')\n    return snapshot.val()\n  } catch (err) {\n    console.log(err)\n    return {}\n  }\n}\n\nasync function getPackageResult({ name, version }) {\n  const ref = firebase\n    .database()\n    .ref()\n    .child('modules-v2')\n    .child(encodeFirebaseKey(name))\n    .child(encodeFirebaseKey(version))\n\n  const snapshot = await ref.once('value')\n  return snapshot.val()\n}\n\nfunction filterBlacklistedPackages() {\n  blackList\n}\n\nasync function trim(packages) {\n  let canTrim = 0\n  const trimsMap = {}\n  Object.keys(packages).forEach(name => {\n    const versions = Object.keys(packages[name])\n\n    if (versions.length > 15) {\n      canTrim += versions.length - 15\n\n      versions.slice(-15).forEach(version => {\n        if (name in trimsMap) {\n          trimsMap[name].push(version)\n        } else {\n          trimsMap[name] = [version]\n        }\n      })\n    }\n  })\n\n  console.log('trims are', trimsMap)\n  console.log('can trim', canTrim)\n}\n\nasync function run() {\n  let packages = []\n\n  const packs = require('../modules-v2.json')\n  const packsNew = require('../modules-v2-new.json')\n  //await getFirebaseStore()\n  // // fs.writeFileSync('./db.json', JSON.stringify(packs, null, 2))\n  //\n  //\n  // Object.keys(packs).forEach(packName => {\n  //   Object.keys(packs[packName]).forEach(version => {\n  //     // if (packName !== 'react') return\n  //     //\n  //     if (blackList.some(entry => entry.test(packName))) {\n  //       return\n  //     }\n  //\n  //     const packageString = `${decodeFirebaseKey(packName)}@${decodeFirebaseKey(version)}`\n  //     queue.push(() => resolvePackage(parsePackageString(packageString)))\n  //       .then(async (packInfo) => {\n  //         const { description, repository } = packInfo\n  //         let truncatedDescription = ''\n  //         let repositoryURL = ''\n  //         try {\n  //           repositoryURL = gitURLParse(repository.url || repository).toString(\"https\");\n  //         } catch (e) {\n  //           console.error('failed to parse repository url', repository)\n  //         }\n  //\n  //         try {\n  //           truncatedDescription = description.length > 330 ? description.substring(0, 330) + '…' : description\n  //         } catch (e) {\n  //           console.error('failed to parse description', description)\n  //         }\n  //\n  //         debug('%s fetched', `${packName}@${version}`)\n  //         console.log('GOT', truncatedDescription, '\\n', repositoryURL)\n  //\n  //         if (!patchedDB[packName]) {\n  //           patchedDB[packName] = {}\n  //         }\n  //         patchedDB[packName][version] = {\n  //           description: truncatedDescription,\n  //           repository: repositoryURL,\n  //           ...packs[packName][version]\n  //         }\n  //\n  //         if (packName === 'zzzz-npm-test') {\n  //           commit()\n  //         }\n  //       })\n  //       .catch((err) => {\n  //         // patchedDB[packName][version] = {\n  //         //   description: '',\n  //         //   repository: '',\n  //         //   ...packs[packName][version]\n  //         // }\n  //         if (packName === 'zzzz-npm-test') {\n  //           commit()\n  //         }\n  //         console.log('fetch for ' + packageString, err)\n  //       })\n  //   })\n  // })\n  const failIndexes = []\n\n  const startIndex = 2000\n  const endIndex = 4239\n  Object.keys(packs).forEach(packName => {\n    Object.keys(packs[packName]).forEach(version => {\n      packages.push({ packName, version })\n    })\n  })\n\n  packages = packages.filter(pack => {\n    const oldPkg = packs[pack.packName][pack.version]\n    const newPkg = packsNew[pack.packName][pack.version]\n\n    return deepEqual(oldPkg, newPkg)\n  })\n\n  console.log('package count is', packages.length)\n\n  packages.slice(startIndex, endIndex).forEach((pack, index) => {\n    const packStr = `${decodeFirebaseKey(pack.packName)}@${decodeFirebaseKey(\n      pack.version\n    )}`\n    queue.push(() =>\n      got(`http://127.0.0.1:5000/api/size?package=${packStr}&force=true`, {\n        json: true,\n      })\n        .then(async r => {\n          const res = r.body\n          const gzipDiff = Math.abs(\n            res.gzip - packs[pack.packName][pack.version].gzip\n          )\n          const minDiff = Math.abs(\n            res.size - packs[pack.packName][pack.version].size\n          )\n          debug(\n            '%d fetched %s, diff: %d KB',\n            startIndex + index,\n            packStr,\n            Math.round(gzipDiff / 1024)\n          )\n\n          if (\n            gzipDiff / packs[pack.packName][pack.version].gzip > 0.05 &&\n            gzipDiff > 4000\n          ) {\n            debugWarning(\n              'GZIP sizes for %s vary more than %d. Old: %d KB, After rebuild: %d KB',\n              packStr,\n              gzipDiff,\n              Math.round(packs[pack.packName][pack.version].gzip / 1024),\n              Math.round(res.gzip / 1024)\n            )\n          }\n          if (\n            minDiff / packs[pack.packName][pack.version].size > 0.05 &&\n            minDiff > 7000\n          ) {\n            debugWarning(\n              'MIN sizes for %s vary more than %d. Old: %d KB, After rebuild: %d KB',\n              packStr,\n              minDiff,\n              Math.round(packs[pack.packName][pack.version].size / 1024),\n              Math.round(res.size / 1024)\n            )\n          }\n        })\n        .catch(err => {\n          failIndexes.push(startIndex + index)\n          console.log('fetch for ' + packStr + ' failed', err)\n          throw err\n        })\n    )\n  })\n  queue.start()\n  // fs.writeFileSync(`./failures-${startIndex}-${endIndex}.json`, JSON.stringify({failures: failIndexes}), 'utf8')\n}\n\nasync function installPackage(packageName, installPath) {\n  let flags, command\n  flags = [\n    // Setting cache is required for concurrent `npm install`s to work\n    'cache=/tmp/tmp-build/cache',\n    'no-package-lock',\n    'no-shrinkwrap',\n    'no-optional',\n    'no-bin-links',\n    'prefer-offline',\n    'progress false',\n    'loglevel error',\n    'ignore-scripts',\n    'save-exact',\n    //\"fetch-retry-factor 0\",\n    //\"fetch-retries 0\",\n    'json',\n  ]\n  command = `npm install ${packageName} --${flags.join(' --')}`\n\n  debug('install start %s', packageName)\n\n  try {\n    await exec(command, {\n      cwd: installPath,\n    })\n    debug('install finish %s', packageName)\n  } catch (err) {\n    console.log(err)\n    if (err.includes('code E404')) {\n      throw new Error('PackageNotFoundError', err)\n    } else {\n      throw new Error('InstallError', err)\n    }\n  }\n}\n\nfunction exec(command, options) {\n  return new Promise((resolve, reject) => {\n    childProcess.exec(command, options, function (error, stdout, stderr) {\n      if (error) {\n        reject(stderr)\n      } else {\n        resolve(stdout)\n      }\n    })\n  })\n}\n\nasync function getExports(name, version) {\n  const packageName = `${name}@${version}`\n  const pathtmp = `/tmp/build/${packageName\n    .replace(/@/g, '-')\n    .replace(/\\//g, '-')\n    .replace(/\\./g, '')}`\n  console.log(pathtmp, 'pathtmp')\n  await mkdir(pathtmp)\n\n  fs.writeFileSync(\n    path.join(pathtmp, 'package.json'),\n    JSON.stringify({ dependencies: {} })\n  )\n\n  fs.writeFileSync(\n    path.join(pathtmp, 'index.js'),\n    JSON.stringify({ dependencies: {} })\n  )\n\n  await installPackage(packageName, pathtmp)\n  const exprts = Object.keys(require(path.join(pathtmp, 'node_modules', name)))\n  return exprts\n}\n\nasync function rebuildTopLevelExports() {\n  const packs = require('../modules-v2.json')\n  const packages = []\n\n  Object.keys(packs).forEach(name => {\n    Object.keys(packs[name]).forEach(version => {\n      packages.push({ name, version })\n    })\n  })\n\n  packages.forEach(pack => {\n    queue.push(() =>\n      getExports(\n        decodeFirebaseKey(pack.name),\n        decodeFirebaseKey(pack.version)\n      ).then(exprts => {\n        debug('got exports for %s %s %o', pack.name, pack.version, exprts)\n        return axios.post('localhost:7001/cache', {\n          name: pack.name,\n          version: pack.version,\n          result: {\n            ...packs[pack.name][pack.version],\n            topLevelExports: exprts,\n          },\n        })\n      })\n    )\n  })\n  queue.start()\n}\n\nrun()\n// trim(require('../modules-v2.json'))\n\n// rebuildTopLevelExports()\n"
  },
  {
    "path": "utils/server.utils.js",
    "content": "require('dotenv-defaults').config()\nconst semver = require('semver')\nconst axios = require('axios')\nconst fetch = require('node-fetch')\nconst pacote = require('pacote')\nconst Queue = require('../server/Queue')\n\nconst CustomError = require('../server/CustomError')\n/**\n * Given a package string\n * this function resolves to a valid version and name.\n */\nasync function resolvePackage(packageString) {\n  try {\n    return await pacote.manifest(packageString, { fullMetadata: true })\n  } catch (err) {\n    if (err.code === 'ETARGET') {\n      throw new CustomError('PackageVersionMismatchError', null, {\n        validVersions: Object.keys(err.distTags).concat(err.versions),\n      })\n    } else {\n      throw new CustomError('PackageNotFoundError', err)\n    }\n  }\n}\n\nfunction getRequestPriority(ctx) {\n  const client = ctx.headers['x-bundlephobia-user']\n\n  switch (client) {\n    case 'bundlephobia website':\n      return Queue.priority.HIGH\n      break\n    case 'yarn website':\n      return Queue.priority.LOW\n      break\n\n    default:\n      return Queue.priority.MEDIUM\n  }\n}\n\nmodule.exports = { getRequestPriority, resolvePackage }\n"
  }
]