Repository: pastelsky/bundlephobia Branch: bundlephobia Commit: cdd5899bb265 Files: 199 Total size: 406.0 KB Directory structure: gitextract_j7l_cmv1/ ├── .eslintrc.json ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── 1-package-build-failure---inaccurate-sizes.md │ │ ├── 2-similar-package-suggestion.md │ │ ├── 3-feature-request---improvement.md │ │ └── 4-bug_report.md │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .travis.yml ├── .yarnrc.yml ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── __tests__/ │ ├── errors-cache.test.js │ ├── stress-test.sh │ └── utils.test.js ├── bin/ │ ├── generate-sitemap.js │ ├── getResults.js │ └── updateHistoricalData.js ├── build-service/ │ ├── .yarnrc.yml │ ├── index.js │ └── package.json ├── cache-service/ │ ├── .gitignore │ ├── cache.utils.js │ ├── index.js │ ├── middlewares/ │ │ ├── exports-size.middleware.js │ │ └── package-size.middleware.js │ └── package.json ├── client/ │ ├── analytics.ts │ ├── api.ts │ ├── assets/ │ │ └── public/ │ │ ├── browserconfig.xml │ │ ├── manifest.json │ │ ├── open-search-description.xml │ │ ├── robots.txt │ │ └── sitemap.xml │ ├── components/ │ │ ├── AnnouncementBanner/ │ │ │ ├── AnnouncementBanner.scss │ │ │ ├── AnnouncementBanner.tsx │ │ │ └── index.ts │ │ ├── AutocompleteInput/ │ │ │ ├── AutocompleteInput.scss │ │ │ ├── AutocompleteInput.tsx │ │ │ ├── components/ │ │ │ │ └── SuggestionItem.tsx │ │ │ ├── hooks/ │ │ │ │ ├── useAutocompleteInput.ts │ │ │ │ └── useFontSize.ts │ │ │ └── index.ts │ │ ├── AutocompleteInputBox/ │ │ │ ├── AutocompleteInputBox.scss │ │ │ ├── AutocompleteInputBox.tsx │ │ │ └── index.ts │ │ ├── BarGraph/ │ │ │ ├── BarGraph.scss │ │ │ ├── BarGraph.tsx │ │ │ └── index.ts │ │ ├── BarVersion/ │ │ │ ├── BarVersion.scss │ │ │ └── BarVersion.tsx │ │ ├── BlogLayout/ │ │ │ ├── BlogLayout.scss │ │ │ ├── BlogLayout.tsx │ │ │ └── index.ts │ │ ├── BuildProgressIndicator/ │ │ │ ├── BuildProgressIndicator.scss │ │ │ ├── BuildProgressIndicator.tsx │ │ │ └── index.ts │ │ ├── Header/ │ │ │ ├── Header.tsx │ │ │ └── index.ts │ │ ├── Icons/ │ │ │ ├── SearchIcon.tsx │ │ │ ├── SideEffectIcon.scss │ │ │ ├── SideEffectIcon.tsx │ │ │ ├── TreeShakeIcon.scss │ │ │ └── TreeShakeIcon.tsx │ │ ├── JumpingDots/ │ │ │ ├── JumpingDots.scss │ │ │ ├── JumpingDots.tsx │ │ │ └── index.ts │ │ ├── Layout/ │ │ │ ├── Layout.scss │ │ │ ├── Layout.tsx │ │ │ └── index.ts │ │ ├── MetaTags.tsx │ │ ├── PageNav/ │ │ │ ├── PageNav.tsx │ │ │ └── index.ts │ │ ├── ProgressHex/ │ │ │ ├── ProgressHex.scss │ │ │ ├── ProgressHex.tsx │ │ │ ├── index.ts │ │ │ └── progress-hex-timeline.ts │ │ ├── QuickStatsBar/ │ │ │ ├── QuickStatsBar.scss │ │ │ ├── QuickStatsBar.tsx │ │ │ └── index.ts │ │ ├── ResultLayout/ │ │ │ ├── ResultLayout.scss │ │ │ ├── ResultLayout.tsx │ │ │ └── index.ts │ │ ├── Separator.tsx │ │ ├── SimilarPackageCard/ │ │ │ ├── SimilarPackageCard.scss │ │ │ ├── SimilarPackageCard.tsx │ │ │ └── index.ts │ │ ├── Stat/ │ │ │ ├── Stat.scss │ │ │ ├── Stat.tsx │ │ │ └── index.ts │ │ ├── Treemap/ │ │ │ ├── Treemap.tsx │ │ │ ├── TreemapSquare.tsx │ │ │ ├── index.ts │ │ │ └── squarify.js │ │ └── Warning/ │ │ ├── Warning.scss │ │ ├── Warning.tsx │ │ └── index.ts │ └── config/ │ ├── colors.ts │ └── scanBlacklist.ts ├── index.js ├── index.ts ├── next.config.js ├── nodemon.json ├── package.json ├── pages/ │ ├── _app.page.tsx │ ├── _document.page.tsx │ ├── blog/ │ │ ├── components/ │ │ │ ├── Article.tsx │ │ │ ├── ContentfulProvider.tsx │ │ │ └── Post.tsx │ │ ├── digital-ocean-partnership.page.tsx │ │ └── index.page.tsx │ ├── compare/ │ │ ├── ComparePage.js │ │ ├── ComparePage.scss │ │ └── index.js │ ├── index.page.tsx │ ├── index.scss │ ├── package/ │ │ └── [...packageString]/ │ │ ├── ResultPage.js │ │ ├── ResultPage.scss │ │ ├── components/ │ │ │ ├── ExportAnalysisSection/ │ │ │ │ ├── ExportAnalysisSection.js │ │ │ │ ├── ExportAnalysisSection.scss │ │ │ │ └── index.js │ │ │ ├── InterLinksSection/ │ │ │ │ ├── InterLinksSection.js │ │ │ │ ├── InterLinksSection.scss │ │ │ │ ├── InterLinksSectionCard/ │ │ │ │ │ ├── InterLinksSectionCard.js │ │ │ │ │ ├── InterLinksSectionCard.scss │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ ├── SimilarPackagesSection/ │ │ │ │ ├── SimilarPackagesSection.js │ │ │ │ ├── SimilarPackagesSection.scss │ │ │ │ └── index.js │ │ │ └── TreemapSection.js │ │ └── index.page.js │ ├── scan/ │ │ ├── Scan.js │ │ ├── Scan.scss │ │ └── index.page.js │ └── scan-results/ │ ├── ScanResults.js │ ├── ScanResults.scss │ └── index.page.js ├── process.yml ├── scripts/ │ ├── README.md │ ├── cleanup-old-keys.ts │ ├── generate-top-packages.js │ ├── package.json │ └── populate-v3.js ├── server/ │ ├── CustomError.js │ ├── Logger.js │ ├── Queue.js │ ├── api/ │ │ └── BuildService.js │ ├── config.js │ ├── data/ │ │ └── similar-packages/ │ │ ├── date-time.js │ │ ├── index.js │ │ ├── markdown.js │ │ └── storage.js │ ├── init.js │ ├── middlewares/ │ │ ├── exports.middleware.js │ │ ├── exportsSizes.middleware.js │ │ ├── generateImg.middleware.js │ │ ├── jsonCache.middleware.js │ │ ├── rateLimit.middleware.js │ │ ├── requestLogger.middleware.js │ │ ├── results/ │ │ │ ├── blockBlacklist.middleware.js │ │ │ ├── build.middleware.js │ │ │ ├── cachedResponse.middleware.js │ │ │ ├── error.middleware.js │ │ │ ├── index.js │ │ │ └── resolvePackage.middleware.js │ │ └── similar-packages/ │ │ ├── fixtures.js │ │ └── similarPackages.middleware.js │ └── worker.js ├── stylesheets/ │ ├── base.scss │ ├── colors.scss │ ├── index.scss │ ├── mixins.scss │ └── variables.scss ├── test-packages/ │ ├── blacklist-error/ │ │ ├── index.js │ │ └── package.json │ ├── build-error/ │ │ ├── index.js │ │ └── package.json │ ├── entry-point-error/ │ │ ├── package.json │ │ └── random-js-file.js │ └── missing-dependency-error/ │ ├── index.js │ └── package.json ├── tsconfig.json ├── tsconfig.server.json ├── types/ │ ├── amplitude.d.ts │ ├── index.ts │ └── react-contentful.d.ts └── utils/ ├── cache.utils.js ├── common.utils.js ├── draw.utils.js ├── firebase.utils.js ├── index.js ├── rebuild.utils.js └── server.utils.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": ["next", "prettier"], "plugins": [], "root": true, "rules": {} } ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: pastelsky open_collective: bundlephobia ko_fi: # Replace with a single Ko-fi username ================================================ FILE: .github/ISSUE_TEMPLATE/1-package-build-failure---inaccurate-sizes.md ================================================ --- name: Package Build failure / inaccurate sizes about: Unexpected failures that are not explained by the FAQ's page - https://github.com/pastelsky/bundlephobia#faq title: ': fails to ' labels: '' assignees: pastelsky --- ## Package name ### Entire (stringified) error that I see in my browser console ``` ``` ================================================ FILE: .github/ISSUE_TEMPLATE/2-similar-package-suggestion.md ================================================ --- name: Similar package suggestion about: Suggest a better alternative to a popular package title: 'Package suggestion: for ' labels: similar suggestion assignees: '' --- **Package name** **Alternative to** **Quality check** - [ ] Package has sufficient overlap in functionality to act as a replacement. - [ ] Package is actively maintained, and/or stable for use. - [ ] Package has at least 1000 weekly downloads on NPM or is relatively popular on GitHub. - [ ] This package is a better alternative to what is already suggested for this category (please explain why), or the category is new. ================================================ FILE: .github/ISSUE_TEMPLATE/3-feature-request---improvement.md ================================================ --- name: Feature request / Improvement about: Suggest an idea for this project title: '' labels: Improvement assignees: '' --- **Please describe the feature/suggestion** **Describe the solution you'd like** **Describe any alternatives you've considered** ================================================ FILE: .github/ISSUE_TEMPLATE/4-bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** **To Reproduce** **Expected behavior** ================================================ FILE: .github/workflows/ci.yml ================================================ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: CI on: push: branches: [bundlephobia] pull_request: branches: [bundlephobia] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [24.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Enable Corepack run: corepack enable - name: Install project dependencies run: yarn install - name: Run lint run: yarn lint - run: yarn build env: CI: true ================================================ FILE: .gitignore ================================================ .next .env build out now.json # package directories node_modules # Serverless directories .serverless # Logs yarn-error.log logs **/.yarn/cache **/.yarn/install-state.gz # JetBrains IDE .idea # TypeScript tsconfig.tsbuildinfo next-env.d.ts # keys scripts/keys # Maintenance scripts and logs maintenance_logs/ top-packages.json populate-v3-*.json ================================================ FILE: .npmrc ================================================ registry=https://registry.npmjs.org ================================================ FILE: .nvmrc ================================================ v24.13.0 ================================================ FILE: .prettierignore ================================================ .next bin ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - '24' ================================================ FILE: .yarnrc.yml ================================================ enableInlineBuilds: true nodeLinker: node-modules npmRegistryServer: 'https://registry.npmjs.org' ================================================ FILE: CONTRIBUTING.md ================================================ Thanks for looking to help 👋. Have a nice time contributing to bundlephobia. If you've any queries regarding setup or contributing, feel free to open an issue. I'll try my best to answer as soon as I can. Note: This repository only contains the frontend, and the request server. If 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) ## Running locally ### Adding the necessary keys (Optional) Add a `.env` file to the root with Algolia credentials. The server should still run without this, but some features might be disabled. ```ini # App Id for NPM Registry ALGOLIA_APP_ID=OFCNCOG2CU # API Key ALGOLIA_API_KEY= ``` In addition, one can specify - ```ini BUILD_SERVICE_ENDPOINT= ``` In the absence of such an endpoint, packages will be built locally using the [`getPackageStats` function](https://github.com/pastelsky/package-build-stats) and ```ini CACHE_SERVICE_ENDPOINT= FIREBASE_API_KEY= FIREBASE_AUTH_DOMAIN= FIREBASE_DATABASE_URL= ``` for caching to work (optional). ### Canvas compile issues Bundlephobia 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). ### Commands | script | description | | ---------------- | :--------------------------------- | | `yarn run dev` | Start a development server locally | | `yarn run build` | Build for production | | `yarn run prod` | Start a production server locally | ================================================ FILE: ISSUE_TEMPLATE.md ================================================ ## Type ## Package name ### Entire error (stringified) I see in my browser console ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Shubham Kanodia Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

bundlephobia.com

Know the performance impact of including an npm package in your app's bundle.

Bundlephobia's looking for contributors and co-maintainers

## Features - Works with ES6 packages - Can build css and scss packages as well (beta) - Reports historical trends - See package composition ## Badges - [badgen.net](https://badgen.net/#bundlephobia) - example size of react: ![react](https://badgen.net/bundlephobia/minzip/react) - [shields.io](https://shields.io/#/examples/size) - example size of react: ![react](https://img.shields.io/bundlephobia/minzip/react.svg) ## Built using bundlephobia - Size in browser - As seen on package searches at [yarnpkg.com](https://yarnpkg.com) - [bundlephobia-cli](https://github.com/AdrieanKhisbe/bundle-phobia-cli) - A Command Line client for bundlephobia - [importcost](https://atom.io/packages/importcost) - An Atom plugin to display size of imported packages - [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. - [npmcharts.com](https://npmcharts.com/compare/bundle-phobia-cli) - bundle size stats at top of page - [Rollpkg](https://github.com/rafgraph/rollpkg) - A build tool to create packages with Rollup and TypeScript ## Support Liked bundlephobia? Used it's API to build something cool? Let us know! We could use some 💛 and sponsorship on – ## FAQ #### 1. Why does search for package X throw `MissingDependencyError` ? This 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. In 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` #### 2. I see a `BuildError` for package X, but I'm not sure why. You 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. ## Contributing See [Contributing](https://github.com/pastelsky/bundlephobia/blob/bundlephobia/CONTRIBUTING.md) ## Sponsors ================================================ FILE: __tests__/errors-cache.test.js ================================================ const fetch = require('node-fetch') const baseURL = 'http://127.0.0.1:5000/api/size?package=' describe('build api', () => { beforeEach(function () { jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000 }) it('builds correct packages', async done => { const resultURL = baseURL + 'react@16.5.0' const result = await fetch(resultURL) const errorJSON = await result.json() expect(result.status).toBe(200) expect(result.headers.get('cache-control')).toBe('max-age=86400') expect(errorJSON).toEqual({ scoped: false, name: 'react', version: '16.5.0', description: 'React is a JavaScript library for building user interfaces.', repository: 'https://github.com/facebook/react', dependencyCount: 4, hasJSNext: false, hasJSModule: false, hasSideEffects: true, size: 5951, gzip: 2528, dependencySizes: [{ name: 'react', approximateSize: 5957 }], }) done() }) it('handles hash bang in the beginning of packages', async done => { const resultURL = baseURL + '@bundlephobia/test-build-error' const result = await fetch(resultURL) const resultJSON = await result.json() expect(result.status).toBe(200) expect(result.headers.get('cache-control')).toBe('max-age=86400') expect(resultJSON.size).toBe(183) expect(resultJSON.gzip).toBe(153) done() }) it('gives right error messages on when trying to build blocklisted packages', async done => { const resultURL = baseURL + 'polymer-cli' const result = await fetch(resultURL) const errorJSON = await result.json() expect(result.status).toBe(403) expect(result.headers.get('cache-control')).toBe('max-age=60') expect(errorJSON.error.code).toBe('BlocklistedPackageError') expect(errorJSON.error.message).toBe( 'The package you were looking for is blocklisted due to suspicious activity in the past' ) done() }) it('gives right error messages on when trying to build entry point error ', async done => { const resultURL = baseURL + '@bundlephobia/test-entry-point-error' const result = await fetch(resultURL) const errorJSON = await result.json() expect(result.status).toBe(500) expect(result.headers.get('cache-control')).toBe('max-age=3600') expect(errorJSON.error.code).toBe('EntryPointError') expect(errorJSON.error.message).toBe( "We could not guess a valid entry point for this package. Perhaps the author hasn't specified one in its package.json ?" ) done() }) it('ignores errors when trying to build packages with missing dependency errors', async done => { const resultURL = baseURL + '@bundlephobia/missing-dependency-error' const result = await fetch(resultURL) const resultJSON = await result.json() expect(result.status).toBe(200) expect(result.headers.get('cache-control')).toBe('max-age=86400') expect(resultJSON.size).toBe(243) expect(resultJSON.gzip).toBe(178) expect(resultJSON.ignoredMissingDependencies).toStrictEqual([ 'missing-package', ]) done() }) it("gives right error messages on when trying to build packages that don't exist", async done => { const resultURL = baseURL + '@bundlephobia/does-not-exist' const result = await fetch(resultURL) const errorJSON = await result.json() expect(result.status).toBe(404) expect(result.headers.get('cache-control')).toBe('max-age=60') expect(errorJSON.error.code).toBe('PackageNotFoundError') expect(errorJSON.error.message).toBe( "The package you were looking for doesn't exist." ) done() }) it("gives right error messages on when trying to build packages versions that don't exist", async done => { const resultURL = baseURL + '@bundlephobia/test-entry-point-error@459.0.0' const result = await fetch(resultURL) const errorJSON = await result.json() expect(result.status).toBe(404) expect(result.headers.get('cache-control')).toBe('max-age=60') expect(errorJSON.error.code).toBe('PackageVersionMismatchError') done() }) }) ================================================ FILE: __tests__/stress-test.sh ================================================ (curl localhost:5000/api/size?package=react && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=preact && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=d3 && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=c3 && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=inferno && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=react-router && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=localforage && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=request && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=lodash && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=moment && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=antd && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=vue && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=react-clipboard && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=react-ink && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=glamorous && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=preact-compat && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=inferno-compat && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=immutable && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=redux && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=mobx && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=mobx-react && echo '\n') & sleep 3 (curl localhost:5000/api/size?package=styled-components && echo '\n') & wait #sleep 5 #(curl localhost:5000/api/size?package=backbone && echo '\n') & #sleep 5 #(curl localhost:5000/api/size?package=jquery && echo '\n') & #sleep 5 #(curl localhost:5000/api/size?package=formik && echo '\n') & #sleep 5 #(curl localhost:5000/api/size?package=animejs && echo '\n') & #sleep 5 #(curl localhost:5000/api/size?package=three && echo '\n') & #sleep 5 #(curl localhost:5000/api/size?package=babylonjs && echo '\n') & #sleep 5 #(curl localhost:5000/api/size?package=emotion && echo '\n') & #sleep 5 #(curl localhost:5000/api/size?package=react-treeview && echo '\n') & #sleep 5 #(curl localhost:5000/api/size?package=react-bootstrap && echo '\n') & #sleep 5 #(curl localhost:5000/api/size?package=vuex && echo '\n') & #sleep 5 #(curl localhost:5000/api/size?package=mojs && echo '\n') & #sleep 5 #(curl localhost:5000/api/size?normalize.css && echo '\n') & ================================================ FILE: __tests__/utils.test.js ================================================ import { parsePackageString } from '../utils/common.utils' describe('parsePackageString', () => { it('handles scoped packages correctly', () => { expect(parsePackageString('@babel/core@9.8.0')).toEqual({ scoped: true, name: '@babel/core', version: '9.8.0', }) }) it('handles scoped packages without versions correctly', () => { expect(parsePackageString('@babel/core')).toEqual({ scoped: true, name: '@babel/core', version: null, }) }) it('handles regular packages correctly', () => { expect(parsePackageString('react@15.6.1')).toEqual({ scoped: false, name: 'react', version: '15.6.1', }) }) it('handles regular packages without version correctly', () => { expect(parsePackageString('react')).toEqual({ scoped: false, name: 'react', version: null, }) }) it('handles special characters in name properly', () => { expect(parsePackageString('chart.js@5.6.0')).toEqual({ scoped: false, name: 'chart.js', version: '5.6.0', }) }) it('handles special characters in version properly', () => { expect(parsePackageString('chart.js@0.7.0-beta')).toEqual({ scoped: false, name: 'chart.js', version: '0.7.0-beta', }) }) }) ================================================ FILE: bin/generate-sitemap.js ================================================ const { SitemapStream, streamToPromise } = require( 'sitemap' ) const { Readable } = require( 'stream' ) const { writeFileSync } = require('fs') const path = require('path') // Source: https://analytics.amplitude.com/bundlephobia/chart/3tbq2vm/edit/jmy3u6h const popularPackages = [ "react", "moment", "lodash", "react-dom", "axios", "@material-ui/core", "date-fns", "dayjs", "vue", "redux", "react-query", "swiper", "react-hook-form", "styled-components", "formik", "framer-motion", "yup", "angular", "antd", "preact", "react-spring", "rxjs", "jquery", "chart.js", "firebase", "glider-js", "chroma-js", "react-select", "@google/model-viewer", "bootstrap", "react-redux", "@apollo/client", "three", "luxon", "uuid", "tailwindcss", "swr", "mobx", "react-slick", "d3", "react-router-dom", "@angular/core", "recoil", "immer", "express", "classnames", "react-datepicker", "recharts", "svelte", "@chakra-ui/react", "react-final-form", "xstate", "zustand", "slick-carousel", "next", "@reduxjs/toolkit", "react-transition-group", "ramda", "gsap", "lodash-es", "query-string", "emotion", "react-beautiful-dnd", "react-router", "react-dnd", "react-bootstrap", "react-window", "@react-google-maps/api", "qs", "react-motion", "joi", "slate", "moment-timezone", "chartist", "react-i18next", "react-table", "react-virtualized", "lottie-web", "node-fetch", "@emotion/styled", "react-toastify", "xlsx", "i18next", "flickity", "@emotion/react", "@material-ui/styles", "animejs", "react-intl", "@hookstate/core", "dompurify", "@popperjs/core", "graphql", "highcharts", "js-cookie", "vuetify", "clsx", "@sentry/browser", "draft-js", "zod", "material-ui", "superstruct", "downshift", "redux-saga", "@headlessui/react", "redux-thunk", "nanoid", "libphonenumber-js", "redux-toolkit", "react-markdown", "react-day-picker", "react-use", "urql", "quill", "@material-ui/icons", "react-modal", "marked", "react-icons", "momentjs", "ajv", "jspdf", "react-dropzone", "react-popper", "jotai", "react-tooltip", "sanitize-html", "crypto-js", "react-move", "react-helmet", "apexcharts", "react-chartjs-2", "react-multi-carousel", "fuse.js", "alpinejs", "aws-sdk", "react-intersection-observer", "react-responsive-modal", "core-js", "tippy.js", "react-responsive-carousel", "react-dates", "popper.js", "graphql-request", "frappe-charts", "exceljs", "chartjs", "react-form", "vuex", "uplot", "date-fns-tz", "phin", "react-player", "keen-slider", "underscore", "final-form", "@angular/material", "react-number-format", "immutable", "xss", "chakra-ui", "lodash.debounce", "typescript", "echarts", "bulma", "jsonschema", "@xstate/react", "react-pdf", "got", "victory", "lazysizes", "validator", "lit-html", "effector", "numeral", "lit-element", "mobx-react", "polished" ] const otherPages = ['', '/scan'] const links = [ ...otherPages.map(page => ({ url: page, changefreq: 'weekly', priority: 1 })), ...popularPackages.map(package => ({ url: `/package/${package}`, changefreq: 'weekly', priority: 0.7 })), ] // Create a stream to write to const stream = new SitemapStream( { hostname: 'https://bundlephobia.com' } ) // Return a promise that resolves with your XML string const sitemapPromise = streamToPromise(Readable.from(links).pipe(stream)).then((data) => data.toString() ) sitemapPromise .then((sitemap) => { writeFileSync(path.join(__dirname, '..', 'client', 'assets', 'public', 'sitemap.xml'), sitemap, 'utf8') }) .catch((err) => { console.error(err) process.exit(1) }) ================================================ FILE: bin/getResults.js ================================================ const firebase = require('firebase') const { encodeFirebaseKey, decodeFirebaseKey } = require('../utils/index') const fs = require('fs') require('dotenv').config() const firebaseConfig = { apiKey: process.env.FIREBASE_API_KEY, authDomain: process.env.FIREBASE_AUTH_DOMAIN, databaseURL: process.env.FIREBASE_DATABASE_URL, } firebase.initializeApp(firebaseConfig) function getFirebaseStoreFromDisk() { try { return require('./data/firebase-modules.json') } catch (err) { console.log('not found on disk') return null } } async function getFirebaseStoreFromNetwork() { const modulesRef = firebase.database().ref('modules-v2') const lastEntry = Object.keys( await modulesRef .limitToLast(1) .once('value') .then(snapshot => snapshot.val()) )[0] const firstEntry = Object.keys( await modulesRef .limitToFirst(1) .once('value') .then(snapshot => snapshot.val()) )[0] let currentLastEntry = firstEntry let allData = {} let counter = 0 console.log('fetching from ', firstEntry, ' to ', lastEntry) while (currentLastEntry !== lastEntry) { counter += 20000 const snapshot = await firebase .database() .ref('modules-v2') .orderByKey() .startAt(currentLastEntry) .limitToFirst(20000) .once('value') .then(snapshot => snapshot.val()) const packageNames = Object.keys(snapshot) currentLastEntry = packageNames[packageNames.length - 1] console.log( 'Fetched records till ', counter, currentLastEntry, 'total of ', Object.keys(snapshot), ' packages.' ) allData = { ...allData, ...snapshot } } fs.mkdirSync(__dirname + '/data', { recursive: true }) fs.writeFileSync( __dirname + '/data/firebase-modules.json', JSON.stringify(allData, null, 2), 'utf8' ) return allData } async function getResults() { let firebaseStore = getFirebaseStoreFromDisk() console.log('loaded firebase store') return Object.keys(firebaseStore).flatMap(packageName => Object.keys(firebaseStore[packageName]).map( version => firebaseStore[packageName][version] ) ) } async function getPackages() { let firebaseStore = getFirebaseStoreFromDisk() || (await getFirebaseStoreFromNetwork()) const packages = Object.keys(firebaseStore).map( packageName => firebaseStore[packageName] ) console.log('fetched ', Object.keys(firebaseStore), ' packages ') return packages } module.exports = { getResults, getPackages } ================================================ FILE: bin/updateHistoricalData.js ================================================ #!/usr/bin/env node const firebase = require('firebase') const FirebaseUtils = require('../utils/firebase.utils') const trending = require('trending-github') const fetch = require('node-fetch') const debug = require('debug')('bp:trending-fetch') const GithubAPI = require('github') const isEmptyObject = require('is-empty-object') const promiseSeries = require('promise.series') require('dotenv').config() const github = new GithubAPI({ debug: false }) github.authenticate({ type: 'oauth', key: process.env.GITHUB_CLIENT_ID, secret: process.env.GITHUB_CLIENT_SECRET }) const firebaseConfig = { apiKey: process.env.FIREBASE_API_KEY, authDomain: process.env.FIREBASE_AUTH_DOMAIN, databaseURL: process.env.FIREBASE_DATABASE_URL } firebase.initializeApp(firebaseConfig) const firebaseUtils = new FirebaseUtils(firebase) const port = process.env.PORT || 5000 async function getPackageFromRepo(author, name) { const { data: { content } } = await github.repos.getContent({ repo: author, owner: name, path: 'package.json' }) if (content) { const decodedContent = Buffer.from(content, 'base64').toString('utf8') return JSON.parse(decodedContent).name } } async function getGithubTrendingPackages() { const repos = await trending('daily', 'javascript') const packages = await Promise.all( repos.map(repo => getPackageFromRepo(repo.author, repo.name)) ) return packages.filter(pack => pack) } async function getTrendingSearches() { const limit = 20 let trendingSearches = [] const searches = await firebaseUtils.getDailySearches() if (searches) { trendingSearches = Object.keys(searches) .sort( (packageA, packageB) => searches[packageB].count - searches[packageA].count ) .slice(0, limit) } return trendingSearches } async function updateHistoricalData() { try { const [githubTrendingPackages, searchTrendingPackages] = await Promise.all([ getGithubTrendingPackages(), getTrendingSearches() ]) const popularPackages = [ ...new Set(githubTrendingPackages.concat(searchTrendingPackages)) ] console.log('popular', popularPackages) } catch (err) { console.log(err) } } async function getVersionsToBuild(name) { const versionsToBuild = [] const res = await fetch( `http://localhost:${port}/api/package-history?package=${name}` ) const versionInfo = await res.json() Object.keys(versionInfo).forEach(version => { if (isEmptyObject(versionInfo[version])) { versionsToBuild.push(version) } }) return versionsToBuild } async function getVersionsToBuild(name) { const versionsToBuild = [] const res = await fetch( `http://localhost:${port}/api/package-history?package=${name}` ) const versionInfo = await res.json() Object.keys(versionInfo).forEach(version => { if (isEmptyObject(versionInfo[version])) { versionsToBuild.push(version) } }) return versionsToBuild } async function buildPackage(name, version) { debug('building package %s %s', name, version) const versionsToBuild = [] const res = await fetch( `http://localhost:${port}/api/size?package=${name + '@' + version}` ) debug('result %s %s %O', name, version, await res.json()) } async function buildPackageFromGithub(name, author) { debug('building repo %s', name) const packageName = await getPackageFromRepo(name, author) if (packageName) { const versions = await getVersionsToBuild(packageName) debug('versions to build for %s — %o', packageName, versions) await promiseSeries( versions.map(version => () => buildPackage(packageName, version)) ) } else { debug('skipped repo %s', name) } } async function mostPopuplarGithubRepos() { const repos = await github.search.repos({ q: 'language:javascript+npm in:readme+size:1000..50000+mirror:false', sort: 'stars', page: 1, per_page: 10 }) debug( 'Popular GitHub Repos %o', repos.data.items.map(r => r.name) ) try { const promises = repos.data.items.map(({ name, owner }) => () => buildPackageFromGithub(name, owner.login) ) await promiseSeries(promises) } catch (err) { console.log(err) } } mostPopuplarGithubRepos() ================================================ FILE: build-service/.yarnrc.yml ================================================ compressionLevel: mixed enableGlobalCache: false ================================================ FILE: build-service/index.js ================================================ import 'dotenv-defaults/config.js' import Fastify from 'fastify' import { getPackageStats, getAllPackageExports, getPackageExportSizes, eventQueue, } from 'package-build-stats' import Amplitude from '@amplitude/node' const fastify = Fastify() if (process.env.AMPLITUDE_API_KEY) { const client = Amplitude.init(process.env.AMPLITUDE_API_KEY) eventQueue.on('*', (event, details) => { client.logEvent({ event_type: event, user_id: 'build-service', event_properties: { ...details, }, }) }) setInterval(() => { client.flush() }, 5000) } fastify.get('/size', async (req, res) => { const packageString = decodeURIComponent(req.query.p) try { const result = await getPackageStats(packageString, { installTimeout: 60000, }) return res.code(200).send(result) } catch (err) { console.log(err) const errorToSend = 'toJSON' in err ? err.toJSON() : err return res.code(500).send(errorToSend) } }) fastify.get('/exports-sizes', async (req, res) => { const packageString = decodeURIComponent(req.query.p) try { const result = await getPackageExportSizes(packageString, { installTimeout: 60000, }) return res.code(200).send(result) } catch (err) { console.log(err) const errorToSend = 'toJSON' in err ? err.toJSON() : err return res.code(500).send(errorToSend) } }) fastify.get('/exports', async (req, res) => { const packageString = decodeURIComponent(req.query.p) try { const result = await getAllPackageExports(packageString, { installTimeout: 60000, }) return res.code(200).send(result) } catch (err) { console.log(err) const errorToSend = 'toJSON' in err ? err.toJSON() : err return res.code(500).send(errorToSend) } }) fastify .listen({ port: 7002 }) .then(() => { console.log(`server listening on ${fastify.server.address().port}`) }) .catch(err => { console.error(err) process.exit(1) }) ================================================ FILE: build-service/package.json ================================================ { "name": "build-service", "version": "1.0.0", "type": "module", "main": "index.js", "license": "MIT", "dependencies": { "@amplitude/node": "^1.10.2", "dotenv-defaults": "^2.0.2", "fastify": "^4.25.2", "now-logs": "^0.0.7", "package-build-stats": "8.2.5" }, "engines": { "node": ">= 9.x.x", "npm": ">= 5.5.x" }, "scripts": { "start": "DEBUG=bp:* node index", "dev": "DEBUG=bp:* node index" } } ================================================ FILE: cache-service/.gitignore ================================================ ================================================ FILE: cache-service/cache.utils.js ================================================ function encodeFirebaseKey(key) { return key.replace(/[.]/g, ',').replace(/\//g, '__') } module.exports = { encodeFirebaseKey } ================================================ FILE: cache-service/index.js ================================================ require('dotenv-defaults').config() const firebase = require('firebase') const fastify = require('fastify')() const { getPackageSizeMiddlware, postPackageSizeMiddlware, } = require('./middlewares/package-size.middleware') const { getExportsSizeMiddlware, postExportsSizeMiddleware, } = require('./middlewares/exports-size.middleware') const firebaseConfig = { apiKey: process.env.FIREBASE_API_KEY, authDomain: process.env.FIREBASE_AUTH_DOMAIN, databaseURL: process.env.FIREBASE_DATABASE_URL, } firebase.initializeApp(firebaseConfig) fastify.get('/package-cache', getPackageSizeMiddlware) fastify.post('/package-cache', postPackageSizeMiddlware) fastify.get('/exports-cache', getExportsSizeMiddlware) fastify.post('/exports-cache', postExportsSizeMiddleware) fastify .listen({ port: 7001 }) .then(() => { console.log(`server listening on ${fastify.server.address().port}`) }) .catch(err => { console.error(err) process.exit(1) }) ================================================ FILE: cache-service/middlewares/exports-size.middleware.js ================================================ require('dotenv-defaults').config() const LRU = require('lru-cache') const firebase = require('firebase') const debug = require('debug')('bp:cache') const { encodeFirebaseKey } = require('../cache.utils') const LRUCache = new LRU({ max: 1500 }) // Configurable Firebase keys for read/write operations const FIREBASE_READ_KEY_EXPORTS = process.env.FIREBASE_READ_KEY_EXPORTS || 'exports-v3' const FIREBASE_WRITE_KEY_EXPORTS = process.env.FIREBASE_WRITE_KEY_EXPORTS || 'exports-v3' debug( 'Firebase config (exports): READ from %s (with fallback: %s), WRITE to %s', FIREBASE_READ_KEY_EXPORTS, FIREBASE_READ_KEY_EXPORTS === 'exports-v3' ? 'yes, to exports' : 'no', FIREBASE_WRITE_KEY_EXPORTS ) async function getPackageResultFromKey(key, { name, version }) { const ref = firebase .database() .ref() .child(key) .child(encodeFirebaseKey(name)) .child(encodeFirebaseKey(version)) const snapshot = await ref.once('value') return snapshot.val() } async function getPackageResult({ name, version, readKey }) { const targetReadKey = readKey || FIREBASE_READ_KEY_EXPORTS // Try primary read key first const result = await getPackageResultFromKey(targetReadKey, { name, version }) if (result) { debug('cache hit: firebase (%s)', targetReadKey) return result } // If reading from default v3 and not found, fall back to "exports" (v2) if ( targetReadKey === 'exports-v3' && !readKey && !process.env.DISABLE_FIREBASE_V2_FALLBACK ) { const fallbackResult = await getPackageResultFromKey('exports', { name, version, }) if (fallbackResult) { debug('cache hit: firebase (fallback to exports)') } return fallbackResult } return null } async function setPackageResult({ name, version, result }) { const modules = firebase.database().ref().child(FIREBASE_WRITE_KEY_EXPORTS) return modules .child(encodeFirebaseKey(name)) .child(encodeFirebaseKey(version)) .set(result) } async function getExportsSizeMiddlware(req, res) { const name = decodeURIComponent(req.query.name) const version = decodeURIComponent(req.query.version) const readKey = req.query.readKey if (!name || !version) { return res.code(422).send() } debug('get exports %s@%s (readKey: %s)', name, version, readKey) // Use memory cache only if no explicit readKey is provided if (!readKey) { const lruCacheEntry = LRUCache.get(`${name}@${version}`) if (lruCacheEntry) { debug('cache hit: memory') return res.code(200).send(lruCacheEntry) } } const result = await getPackageResult({ name, version, readKey }) if (result) { debug('cache hit: firebase') if (!readKey) { LRUCache.set(`${name}@${version}`, result) } return res.code(200).send(result) } return res.code(404).send() } async function postExportsSizeMiddleware(req, res) { const { name, version, result } = req.body if (!name || !version || !result) return res.code(422).send() debug('set exports %O to %O', { name, version }, result) LRUCache.set(`${name}@${version}`, result) try { await setPackageResult({ name, version, result }) return res.code(201).send() } catch (err) { console.log(err) return res.code(500).send({ error: err }) } } module.exports = { getExportsSizeMiddlware, postExportsSizeMiddleware } ================================================ FILE: cache-service/middlewares/package-size.middleware.js ================================================ require('dotenv-defaults').config() const firebase = require('firebase') const LRU = require('lru-cache') const debug = require('debug')('bp:cache') const { encodeFirebaseKey } = require('../cache.utils') const LRUCache = new LRU({ max: 3000 }) // Configurable Firebase keys for read/write operations // This allows safe migration from modules-v2 (old) to modules-v3 (new package-build-stats 8.x) // When FIREBASE_READ_KEY is 'modules-v3', it will try v3 first, then fall back to v2 // When FIREBASE_READ_KEY is 'modules-v2', it will only read from v2 const FIREBASE_READ_KEY = process.env.FIREBASE_READ_KEY || 'modules-v3' const FIREBASE_WRITE_KEY = process.env.FIREBASE_WRITE_KEY || 'modules-v3' debug( 'Firebase config: READ from %s (with fallback: %s), WRITE to %s', FIREBASE_READ_KEY, FIREBASE_READ_KEY === 'modules-v3' ? 'yes, to modules-v2' : 'no', FIREBASE_WRITE_KEY ) async function getPackageResultFromKey(key, { name, version }) { const ref = firebase .database() .ref() .child(key) .child(encodeFirebaseKey(name)) .child(encodeFirebaseKey(version)) const snapshot = await ref.once('value') return snapshot.val() } async function getPackageResult({ name, version, readKey }) { const targetReadKey = readKey || FIREBASE_READ_KEY // Try primary read key first const result = await getPackageResultFromKey(targetReadKey, { name, version }) if (result) { debug('cache hit: firebase (%s)', targetReadKey) return result } // If reading from default v3 and not found, fall back to v2 if ( targetReadKey === 'modules-v3' && !readKey && !process.env.DISABLE_FIREBASE_V2_FALLBACK ) { const fallbackResult = await getPackageResultFromKey('modules-v2', { name, version, }) if (fallbackResult) { debug('cache hit: firebase (fallback to modules-v2)') } return fallbackResult } return null } async function setPackageResult({ name, version, result }) { const modules = firebase.database().ref().child(FIREBASE_WRITE_KEY) return modules .child(encodeFirebaseKey(name)) .child(encodeFirebaseKey(version)) .set(result) } async function getPackageSizeMiddlware(req, res) { const name = decodeURIComponent(req.query.name) const version = decodeURIComponent(req.query.version) const readKey = req.query.readKey if (!name || !version) { return res.code(422).send() } debug('get package %s@%s (readKey: %s)', name, version, readKey) // Use memory cache only if no explicit readKey is provided if (!readKey) { const lruCacheEntry = LRUCache.get(`${name}@${version}`) if (lruCacheEntry) { debug('cache hit: memory') return res.code(200).send(lruCacheEntry) } } const result = await getPackageResult({ name, version, readKey }) if (result) { debug('cache hit: firebase') if (!readKey) { LRUCache.set(`${name}@${version}`, result) } return res.code(200).send(result) } return res.code(404).send() } async function postPackageSizeMiddlware(req, res) { const { name, version, result } = req.body if (!name || !version || !result) return res.code(422).send() debug('set package %O to %O', { name, version }, result) LRUCache.set(`${name}@${version}`, result) try { await setPackageResult({ name, version, result }) return res.code(201).send() } catch (err) { console.log(err) return res.code(500).send({ error: err }) } } module.exports = { getPackageSizeMiddlware, postPackageSizeMiddlware } ================================================ FILE: cache-service/package.json ================================================ { "name": "cache", "version": "1.0.0", "main": "index.js", "license": "MIT", "dependencies": { "debug": "^4.3.4", "dotenv": "^8.6.0", "dotenv-defaults": "^2.0.2", "fastify": "^4.25.2", "firebase": "^8.10.1", "lru-cache": "^5.1.1" }, "scripts": { "start": "DEBUG=bp* node index", "dev": "DEBUG=bp* node index" } } ================================================ FILE: client/analytics.ts ================================================ type HasPackageName = { packageName: string } type HasTimeTaken = { timeTaken: number } type HasIsDisabled = { isDisabled: boolean } type HasSuccessRatio = { successRatio: number } type HasPackageNameAndTimeTaken = HasPackageName & HasTimeTaken export default class Analytics { static pageView(pageType: string) { amplitude.getInstance().logEvent(`Viewed ${pageType}`, { path: window.location.pathname, }) } static performedSearch(packageName: string) { amplitude.getInstance().logEvent('Search Performed', { package: packageName, }) } static searchSuccess({ packageName, timeTaken }: HasPackageNameAndTimeTaken) { amplitude.getInstance().logEvent('Search Successful', { package: packageName, timeTaken, }) } static searchFailure({ packageName, timeTaken }: HasPackageNameAndTimeTaken) { amplitude.getInstance().logEvent('Search Failed', { package: packageName, timeTaken, }) } static graphBarClicked({ packageName, isDisabled, }: HasPackageName & HasIsDisabled) { amplitude.getInstance().logEvent('Bar Graph Clicked', { package: packageName, isDisabled, }) } static scanPackageJsonDropped(itemCount: number) { amplitude.getInstance().logEvent('Scan packageJSON dropped', { itemCount, }) } static performedScan() { amplitude.getInstance().logEvent('Scan Performed') } static scanParseError() { amplitude.getInstance().logEvent('Scan Parse Error') } static scanCompleted({ timeTaken, successRatio, }: HasTimeTaken & HasSuccessRatio) { amplitude.getInstance().logEvent('Scan Parse Completed', { successRatio, timeTaken, }) } static performedExportsAnalysis(packageName: string) { amplitude.getInstance().logEvent('Exports Analysis Performed', { package: packageName, }) } static exportsAnalysisSuccess({ packageName, timeTaken, }: HasPackageNameAndTimeTaken) { amplitude.getInstance().logEvent('Exports Analysis Successful', { package: packageName, timeTaken, }) } static exportsAnalysisFailure({ packageName, timeTaken, }: HasPackageNameAndTimeTaken) { amplitude.getInstance().logEvent('Exports Analysis Failed', { package: packageName, timeTaken, }) } static exportsSizesSuccess({ packageName, timeTaken, }: HasPackageNameAndTimeTaken) { amplitude.getInstance().logEvent('Exports Size Calculated', { package: packageName, timeTaken, }) } static exportsSizesFailure({ packageName, timeTaken, }: HasPackageNameAndTimeTaken) { amplitude.getInstance().logEvent('Exports Size Failed', { package: packageName, timeTaken, }) } } ================================================ FILE: client/api.ts ================================================ import fetch from 'unfetch' type PackageSuggestion = { searchScore: number score: { detail: { popularity: number } } } type RecentSearch = { [key: string]: { name: string version: string lastSearched: number count: number } } export default class API { static get(url: string, isInternal = true): Promise { const headers: Record = { Accept: 'application/json', } if (isInternal) { headers['X-Bundlephobia-User'] = 'bundlephobia website' } return fetch(url, { headers }).then(res => { if (!res.ok) { try { return res.json().then(err => Promise.reject(err)) } catch (e) { if (res.status === 503) { return Promise.reject({ error: { code: 'TimeoutError', message: 'This is taking unusually long. Check back in a couple of minutes?', }, }) } return Promise.reject({ error: { code: 'BuildError', message: "Oops, something went wrong and we don't have an appropriate error for this. Open an issue maybe?", }, }) } } return res.json() }) } static getInfo(packageString: string) { return API.get(`/api/size?package=${packageString}&record=true`) } static getExports(packageString: string) { return API.get(`/api/exports?package=${packageString}`) } static getExportsSizes(packageString: string) { return API.get(`/api/exports-sizes?package=${packageString}`) } static getHistory(packageString: string, limit: number) { return API.get( `/api/package-history?package=${packageString}&limit=${limit}` ) } static getRecentSearches(limit: number) { return API.get(`/api/recent?limit=${limit}`) } static getSimilar(packageName: string) { return API.get(`/api/similar-packages?package=${packageName}`) } static getSuggestions(query: string) { const suggestionSort = ( packageA: PackageSuggestion, packageB: PackageSuggestion ) => { // Rank closely matching packages followed // by most popular ones if ( Math.abs( Math.log(packageB.searchScore) - Math.log(packageA.searchScore) ) > 1 ) { return packageB.searchScore - packageA.searchScore } else { return ( packageB.score.detail.popularity - packageA.score.detail.popularity ) } } return API.get( `https://api.npms.io/v2/search/suggestions?q=${query}`, false ).then(result => result.sort(suggestionSort)) //backup when npms.io is down //return API.get(`/-/search?text=${query}`) // .then(result => result.objects // .sort(suggestionSort) // .map(suggestion => { // const name = suggestion.package.name // const hasMatch = name.includes(query) // const startIndex = name.indexOf(query) // const endIndex = startIndex + query.length // let highlight // // if (hasMatch) { // highlight = // name.substring(0, startIndex) + // '' + name.substring(startIndex, endIndex) + '' + // name.substring(endIndex) // } else { // highlight = name // } // // return { // ...suggestion, // highlight, // } // }), // ) } } ================================================ FILE: client/assets/public/browserconfig.xml ================================================ #2b5797 ================================================ FILE: client/assets/public/manifest.json ================================================ { "name": "Bundlephobia", "icons": [ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/android-chrome-256x256.png", "sizes": "256x256", "type": "image/png" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone", "orientation": "portrait" } ================================================ FILE: client/assets/public/open-search-description.xml ================================================ bundlephobia Search npm packages on bundlephobia UTF-8 UTF-8 en-us https://bundlephobia.com/favicon-32x32.png bundlephobia https://bundlephobia.com ================================================ FILE: client/assets/public/robots.txt ================================================ # * User-agent: * Allow: / Disallow: /scan-results # Host Host: https://bundlephobia.com # Sitemaps Sitemap: https://bundlephobia.com/sitemap.xml ================================================ FILE: client/assets/public/sitemap.xml ================================================ https://bundlephobia.com/ weekly 1.0 https://bundlephobia.com/scan weekly 1.0 https://bundlephobia.com/package/react weekly 0.7 https://bundlephobia.com/package/moment weekly 0.7 https://bundlephobia.com/package/lodash weekly 0.7 https://bundlephobia.com/package/react-dom weekly 0.7 https://bundlephobia.com/package/axios weekly 0.7 https://bundlephobia.com/package/@material-ui/core weekly 0.7 https://bundlephobia.com/package/date-fns weekly 0.7 https://bundlephobia.com/package/dayjs weekly 0.7 https://bundlephobia.com/package/vue weekly 0.7 https://bundlephobia.com/package/redux weekly 0.7 https://bundlephobia.com/package/react-query weekly 0.7 https://bundlephobia.com/package/swiper weekly 0.7 https://bundlephobia.com/package/react-hook-form weekly 0.7 https://bundlephobia.com/package/styled-components weekly 0.7 https://bundlephobia.com/package/formik weekly 0.7 https://bundlephobia.com/package/framer-motion weekly 0.7 https://bundlephobia.com/package/yup weekly 0.7 https://bundlephobia.com/package/angular weekly 0.7 https://bundlephobia.com/package/antd weekly 0.7 https://bundlephobia.com/package/preact weekly 0.7 https://bundlephobia.com/package/react-spring weekly 0.7 https://bundlephobia.com/package/rxjs weekly 0.7 https://bundlephobia.com/package/jquery weekly 0.7 https://bundlephobia.com/package/chart.js weekly 0.7 https://bundlephobia.com/package/firebase weekly 0.7 https://bundlephobia.com/package/glider-js weekly 0.7 https://bundlephobia.com/package/chroma-js weekly 0.7 https://bundlephobia.com/package/react-select weekly 0.7 https://bundlephobia.com/package/@google/model-viewer weekly 0.7 https://bundlephobia.com/package/bootstrap weekly 0.7 https://bundlephobia.com/package/react-redux weekly 0.7 https://bundlephobia.com/package/@apollo/client weekly 0.7 https://bundlephobia.com/package/three weekly 0.7 https://bundlephobia.com/package/luxon weekly 0.7 https://bundlephobia.com/package/uuid weekly 0.7 https://bundlephobia.com/package/tailwindcss weekly 0.7 https://bundlephobia.com/package/swr weekly 0.7 https://bundlephobia.com/package/mobx weekly 0.7 https://bundlephobia.com/package/react-slick weekly 0.7 https://bundlephobia.com/package/d3 weekly 0.7 https://bundlephobia.com/package/react-router-dom weekly 0.7 https://bundlephobia.com/package/@angular/core weekly 0.7 https://bundlephobia.com/package/recoil weekly 0.7 https://bundlephobia.com/package/immer weekly 0.7 https://bundlephobia.com/package/express weekly 0.7 https://bundlephobia.com/package/classnames weekly 0.7 https://bundlephobia.com/package/react-datepicker weekly 0.7 https://bundlephobia.com/package/recharts weekly 0.7 https://bundlephobia.com/package/svelte weekly 0.7 https://bundlephobia.com/package/@chakra-ui/react weekly 0.7 https://bundlephobia.com/package/react-final-form weekly 0.7 https://bundlephobia.com/package/xstate weekly 0.7 https://bundlephobia.com/package/zustand weekly 0.7 https://bundlephobia.com/package/slick-carousel weekly 0.7 https://bundlephobia.com/package/next weekly 0.7 https://bundlephobia.com/package/@reduxjs/toolkit weekly 0.7 https://bundlephobia.com/package/react-transition-group weekly 0.7 https://bundlephobia.com/package/ramda weekly 0.7 https://bundlephobia.com/package/gsap weekly 0.7 https://bundlephobia.com/package/lodash-es weekly 0.7 https://bundlephobia.com/package/query-string weekly 0.7 https://bundlephobia.com/package/emotion weekly 0.7 https://bundlephobia.com/package/react-beautiful-dnd weekly 0.7 https://bundlephobia.com/package/react-router weekly 0.7 https://bundlephobia.com/package/react-dnd weekly 0.7 https://bundlephobia.com/package/react-bootstrap weekly 0.7 https://bundlephobia.com/package/react-window weekly 0.7 https://bundlephobia.com/package/@react-google-maps/api weekly 0.7 https://bundlephobia.com/package/qs weekly 0.7 https://bundlephobia.com/package/react-motion weekly 0.7 https://bundlephobia.com/package/joi weekly 0.7 https://bundlephobia.com/package/slate weekly 0.7 https://bundlephobia.com/package/moment-timezone weekly 0.7 https://bundlephobia.com/package/chartist weekly 0.7 https://bundlephobia.com/package/react-i18next weekly 0.7 https://bundlephobia.com/package/react-table weekly 0.7 https://bundlephobia.com/package/react-virtualized weekly 0.7 https://bundlephobia.com/package/lottie-web weekly 0.7 https://bundlephobia.com/package/node-fetch weekly 0.7 https://bundlephobia.com/package/@emotion/styled weekly 0.7 https://bundlephobia.com/package/react-toastify weekly 0.7 https://bundlephobia.com/package/xlsx weekly 0.7 https://bundlephobia.com/package/i18next weekly 0.7 https://bundlephobia.com/package/flickity weekly 0.7 https://bundlephobia.com/package/@emotion/react weekly 0.7 https://bundlephobia.com/package/@material-ui/styles weekly 0.7 https://bundlephobia.com/package/animejs weekly 0.7 https://bundlephobia.com/package/react-intl weekly 0.7 https://bundlephobia.com/package/@hookstate/core weekly 0.7 https://bundlephobia.com/package/dompurify weekly 0.7 https://bundlephobia.com/package/@popperjs/core weekly 0.7 https://bundlephobia.com/package/graphql weekly 0.7 https://bundlephobia.com/package/highcharts weekly 0.7 https://bundlephobia.com/package/js-cookie weekly 0.7 https://bundlephobia.com/package/vuetify weekly 0.7 https://bundlephobia.com/package/clsx weekly 0.7 https://bundlephobia.com/package/@sentry/browser weekly 0.7 https://bundlephobia.com/package/draft-js weekly 0.7 https://bundlephobia.com/package/zod weekly 0.7 https://bundlephobia.com/package/material-ui weekly 0.7 https://bundlephobia.com/package/superstruct weekly 0.7 https://bundlephobia.com/package/downshift weekly 0.7 https://bundlephobia.com/package/redux-saga weekly 0.7 https://bundlephobia.com/package/@headlessui/react weekly 0.7 https://bundlephobia.com/package/redux-thunk weekly 0.7 https://bundlephobia.com/package/nanoid weekly 0.7 https://bundlephobia.com/package/libphonenumber-js weekly 0.7 https://bundlephobia.com/package/redux-toolkit weekly 0.7 https://bundlephobia.com/package/react-markdown weekly 0.7 https://bundlephobia.com/package/react-day-picker weekly 0.7 https://bundlephobia.com/package/react-use weekly 0.7 https://bundlephobia.com/package/urql weekly 0.7 https://bundlephobia.com/package/quill weekly 0.7 https://bundlephobia.com/package/@material-ui/icons weekly 0.7 https://bundlephobia.com/package/react-modal weekly 0.7 https://bundlephobia.com/package/marked weekly 0.7 https://bundlephobia.com/package/react-icons weekly 0.7 https://bundlephobia.com/package/momentjs weekly 0.7 https://bundlephobia.com/package/ajv weekly 0.7 https://bundlephobia.com/package/jspdf weekly 0.7 https://bundlephobia.com/package/react-dropzone weekly 0.7 https://bundlephobia.com/package/react-popper weekly 0.7 https://bundlephobia.com/package/jotai weekly 0.7 https://bundlephobia.com/package/react-tooltip weekly 0.7 https://bundlephobia.com/package/sanitize-html weekly 0.7 https://bundlephobia.com/package/crypto-js weekly 0.7 https://bundlephobia.com/package/react-move weekly 0.7 https://bundlephobia.com/package/react-helmet weekly 0.7 https://bundlephobia.com/package/apexcharts weekly 0.7 https://bundlephobia.com/package/react-chartjs-2 weekly 0.7 https://bundlephobia.com/package/react-multi-carousel weekly 0.7 https://bundlephobia.com/package/fuse.js weekly 0.7 https://bundlephobia.com/package/alpinejs weekly 0.7 https://bundlephobia.com/package/aws-sdk weekly 0.7 https://bundlephobia.com/package/react-intersection-observer weekly 0.7 https://bundlephobia.com/package/react-responsive-modal weekly 0.7 https://bundlephobia.com/package/core-js weekly 0.7 https://bundlephobia.com/package/tippy.js weekly 0.7 https://bundlephobia.com/package/react-responsive-carousel weekly 0.7 https://bundlephobia.com/package/react-dates weekly 0.7 https://bundlephobia.com/package/popper.js weekly 0.7 https://bundlephobia.com/package/graphql-request weekly 0.7 https://bundlephobia.com/package/frappe-charts weekly 0.7 https://bundlephobia.com/package/exceljs weekly 0.7 https://bundlephobia.com/package/chartjs weekly 0.7 https://bundlephobia.com/package/react-form weekly 0.7 https://bundlephobia.com/package/vuex weekly 0.7 https://bundlephobia.com/package/uplot weekly 0.7 https://bundlephobia.com/package/date-fns-tz weekly 0.7 https://bundlephobia.com/package/phin weekly 0.7 https://bundlephobia.com/package/react-player weekly 0.7 https://bundlephobia.com/package/keen-slider weekly 0.7 https://bundlephobia.com/package/underscore weekly 0.7 https://bundlephobia.com/package/final-form weekly 0.7 https://bundlephobia.com/package/@angular/material weekly 0.7 https://bundlephobia.com/package/react-number-format weekly 0.7 https://bundlephobia.com/package/immutable weekly 0.7 https://bundlephobia.com/package/xss weekly 0.7 https://bundlephobia.com/package/chakra-ui weekly 0.7 https://bundlephobia.com/package/lodash.debounce weekly 0.7 https://bundlephobia.com/package/typescript weekly 0.7 https://bundlephobia.com/package/echarts weekly 0.7 https://bundlephobia.com/package/bulma weekly 0.7 https://bundlephobia.com/package/jsonschema weekly 0.7 https://bundlephobia.com/package/@xstate/react weekly 0.7 https://bundlephobia.com/package/react-pdf weekly 0.7 https://bundlephobia.com/package/got weekly 0.7 https://bundlephobia.com/package/victory weekly 0.7 https://bundlephobia.com/package/lazysizes weekly 0.7 https://bundlephobia.com/package/validator weekly 0.7 https://bundlephobia.com/package/lit-html weekly 0.7 https://bundlephobia.com/package/effector weekly 0.7 https://bundlephobia.com/package/numeral weekly 0.7 https://bundlephobia.com/package/lit-element weekly 0.7 https://bundlephobia.com/package/mobx-react weekly 0.7 https://bundlephobia.com/package/polished weekly 0.7 ================================================ FILE: client/components/AnnouncementBanner/AnnouncementBanner.scss ================================================ @import '../../../stylesheets/colors'; @import '../../../stylesheets/variables'; .announcement-banner { background: #fff; color: $dark-gulf-blue; padding: 0; position: fixed; bottom: 24px; right: 24px; left: auto; top: auto; transform: none; width: 300px; margin: 0; border-radius: 8px; z-index: 1000; border: 1.5px solid $raven; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } .announcement-banner__content { display: flex; align-items: center; justify-content: center; max-width: 1200px; margin: 0 auto; padding: 10px 48px 10px 20px; gap: 12px; @media (max-width: 600px) { padding: 10px 40px 10px 12px; gap: 8px; } } .announcement-banner__icon { font-size: 18px; flex-shrink: 0; } .announcement-banner__text { margin: 0; font-size: 14px; line-height: 1.5; line-height: 1.5; text-align: left; font-family: $font-family-body; @media (max-width: 600px) { font-size: 13px; text-align: left; } strong { font-weight: 600; color: $gulf-blue; } a { color: $cornflower-blue; font-weight: 500; text-decoration: underline; text-decoration-color: rgba($cornflower-blue, 0.4); text-underline-offset: 2px; transition: all 0.2s ease; &:hover { text-decoration-color: $cornflower-blue; color: darken($cornflower-blue, 10%); } } } .announcement-banner__close { position: absolute; right: 8px; top: 8px; transform: none; background: transparent; border: none; color: $raven; font-size: 24px; font-weight: 300; width: 28px; height: 28px; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; line-height: 1; padding: 0; &:hover { color: $dark-gulf-blue; background: rgba($raven, 0.1); } &:focus { outline: none; color: $dark-gulf-blue; } } ================================================ FILE: client/components/AnnouncementBanner/AnnouncementBanner.tsx ================================================ import React, { useState, useEffect } from 'react' const STORAGE_KEY = 'bundlephobia_rspack_banner_dismissed' const EXPIRY_DATE = new Date('2026-07-18T00:00:00Z') // 6 months from January 18, 2026 export const AnnouncementBanner: React.FC = () => { const [isVisible, setIsVisible] = useState(false) useEffect(() => { // Check if banner should be shown const now = new Date() // Don't show after expiry date if (now >= EXPIRY_DATE) { return } // Check if already dismissed try { const dismissed = localStorage.getItem(STORAGE_KEY) if (dismissed === 'true') { return } } catch (e) { // localStorage not available } setIsVisible(true) }, []) const handleDismiss = () => { setIsVisible(false) try { localStorage.setItem(STORAGE_KEY, 'true') } catch (e) { // localStorage not available } } if (!isVisible) { return null } return (
🚀

New: Now uses{' '} Rspack {' '} — much faster results, better tree-shaking, accuracy and reliability !

) } export default AnnouncementBanner ================================================ FILE: client/components/AnnouncementBanner/index.ts ================================================ export { AnnouncementBanner } from './AnnouncementBanner' export { default } from './AnnouncementBanner' ================================================ FILE: client/components/AutocompleteInput/AutocompleteInput.scss ================================================ @use "sass:math"; @import '../../../stylesheets/colors'; @import '../../../stylesheets/variables'; .autocomplete-input__container { position: relative; width: 100%; } .autocomplete-input { width: 40vw; border: none; border-radius: 50px; color: rgba(0, 0, 0, 0); } .autocomplete-input, .autocomplete-input__dummy-input { @include font-size-lg; padding: $global-spacing * 1.5 (25px + $global-spacing * 2) $global-spacing * 1.5 $global-spacing * 3; font-family: $font-family-code; font-weight: $font-weight-thin; width: 100%; box-sizing: border-box; letter-spacing: -0.7px; margin: 0; @media screen and (max-width: 40em) { padding: $global-spacing (20px + $global-spacing) $global-spacing $global-spacing; } } .autocomplete-input__dummy-input { position: absolute; left: 0; right: 0; top: 0; bottom: 0; pointer-events: none; display: flex; align-items: center; white-space: nowrap; overflow: hidden; } .dummy-input__package-name { color: #1d1d1d; font-size: inherit; font-weight: inherit; margin: 0; } .dummy-input__package-version { color: #636363; } .dummy-input__at-separator { color: $pastel-green; } .autocomplete-input__suggestions-menu { border: 1px solid $autocomplete-border-color; border-top: 0; background: rgba(255, 255, 255, 0.96); font-size: 90%; position: absolute; overflow: auto; z-index: 10; max-height: 35vh; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; animation: unroll 0.2s cubic-bezier(0.305, 0.42, 0.205, 1.2); // align borders, negating nesting due to border on parent left: -1px; // hide rounded borders of the input margin-top: -5px; width: 100%; width: calc(100% + 2px); &:empty { border: 0; } } @keyframes unroll { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0px); } } .autocomplete-input__suggestion { padding: 10px 32px; color: #333; font-size: 15px; cursor: pointer; font-family: 'Source Code Pro', monospace; font-weight: $font-weight-light; letter-spacing: -0.5px; &:not(:last-of-type) { border-bottom: 1px solid #f5f5f5; } @media screen and (max-width: 40em) { padding: math.div($global-spacing, 1.5) $global-spacing * 1.5; } em { font-weight: $font-weight-bold; font-style: normal; color: #444; } } .autocomplete-input__suggestion--highlight { background: rgb(212, 243, 255); } .autocomplete-input__suggestion-description { @include font-size-xs; width: 100%; min-width: 260px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-family: $font-family-body; font-weight: $font-weight-thin; color: #666; padding-top: 5px; letter-spacing: 0; @media screen and (max-width: 40em) { font-weight: $font-weight-light; } } .autocomplete-input__form { display: flex; align-items: baseline; position: relative; } .autocomplete-input__search-icon { position: absolute; right: $global-spacing * 2.5; z-index: 1; cursor: pointer; top: 0; bottom: 0; margin: auto; width: 25px; height: 25px; border: none; background: none; padding: 0; @media screen and (max-width: 40em) { width: 16px; height: 16px; right: $global-spacing * 1.5; } svg { width: 100%; height: 100%; path { transition: all 0.2s; fill: #666; } } &:hover { path { fill: $pastel-green; stroke: $pastel-green; stroke-width: 4px; } } } ================================================ FILE: client/components/AutocompleteInput/AutocompleteInput.tsx ================================================ import React from 'react' import cx from 'classnames' import AutoComplete from 'react-autocomplete' import SearchIcon from '../Icons/SearchIcon' import { parsePackageString } from '../../../utils/common.utils' import { useAutocompleteInput } from './hooks/useAutocompleteInput' import { SuggestionItem } from './components/SuggestionItem' import { useFontSize } from './hooks/useFontSize' type AutocompleteInputProps = { initialValue?: string renderAsH1?: boolean className?: string containerClass?: string autoFocus?: boolean onSearchSubmit: (value: string) => void } export const AutocompleteInput = ({ initialValue = '', renderAsH1 = false, className, containerClass, autoFocus, onSearchSubmit, }: AutocompleteInputProps) => { const searchInput = React.useRef(null) const { value, isMenuVisible, suggestions, handleSubmit, handleInputChange, setIsMenuVisible, setSuggestions, } = useAutocompleteInput({ initialValue, onSubmit: onSearchSubmit }) const { searchFontSize } = useFontSize({ value }) const { name, version } = React.useMemo( () => parsePackageString(value), [value] ) return (
item.package.name} inputProps={{ placeholder: 'find package', className: 'autocomplete-input', autoCorrect: 'off', autoFocus: autoFocus, autoCapitalize: 'off', spellCheck: false, style: { fontSize: searchFontSize! }, }} onMenuVisibilityChange={isOpen => setIsMenuVisible(isOpen)} onChange={handleInputChange} ref={searchInput} value={value} items={suggestions} onSelect={(value, item) => { setSuggestions([item]) onSearchSubmit(value) }} renderMenu={(items, value, inbuiltStyles) => { return (
{items as any}
) }} wrapperStyle={{ display: 'inline-block', width: '100%', position: 'relative', }} renderItem={(item, isHighlighted) => (
)} />
{name} {version !== null && ( <> @ {version} )}
) } type PackageNameElementProps = React.HTMLAttributes & { isHeading?: boolean } export function PackageNameElement({ isHeading, ...props }: PackageNameElementProps) { return isHeading ?

: } ================================================ FILE: client/components/AutocompleteInput/components/SuggestionItem.tsx ================================================ import cx from 'classnames' interface SuggestionItemProps { item: { highlight: string | null package: { name: string description: string } } isHighlighted: boolean } export function SuggestionItem({ item, isHighlighted }: SuggestionItemProps) { return (
{item.highlight != null ? (
) : (
{item.package.name}
)}
{item.package.description}
) } ================================================ FILE: client/components/AutocompleteInput/hooks/useAutocompleteInput.ts ================================================ import React from 'react' import debounce from 'debounce' import { parsePackageString } from '../../../../utils/common.utils' import API from '../../../api' interface UseAutocompleteInputArgs { initialValue: string onSubmit: (value: string) => void } export function useAutocompleteInput({ initialValue, onSubmit, }: UseAutocompleteInputArgs) { const [value, setValue] = React.useState(initialValue) const [suggestions, setSuggestions] = React.useState([]) const [isMenuVisible, setIsMenuVisible] = React.useState(false) const getSuggestions = React.useMemo( () => debounce((value: string) => { API.getSuggestions(value).then(result => { setSuggestions(result) }) }, 150), [] ) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() onSubmit(value) } const handleInputChange = ( e: React.ChangeEvent, value: string ) => { setValue(e.target.value) const trimmedValue = e.target.value.trim() const { name } = parsePackageString(trimmedValue) if (trimmedValue.length > 1) { getSuggestions(name) } if (!trimmedValue) { setSuggestions([]) } } return { value, suggestions, isMenuVisible, handleSubmit, handleInputChange, setIsMenuVisible, setSuggestions, } } ================================================ FILE: client/components/AutocompleteInput/hooks/useFontSize.ts ================================================ import React from 'react' export function useFontSize({ value }: { value: string }) { const searchFontSize = React.useMemo(() => { const baseFontSize = typeof window !== 'undefined' && window.innerWidth < 640 ? 22 : 35 const maxFullSizeChars = typeof window !== 'undefined' && window.innerWidth < 640 ? 15 : 20 const searchFontSize = value.length < maxFullSizeChars ? null : `${baseFontSize - (value.length - maxFullSizeChars) * 0.8}px` return searchFontSize }, [value]) return { searchFontSize } } ================================================ FILE: client/components/AutocompleteInput/index.ts ================================================ export { AutocompleteInput } from './AutocompleteInput' ================================================ FILE: client/components/AutocompleteInputBox/AutocompleteInputBox.scss ================================================ @import '../../../stylesheets/colors'; @import '../../../stylesheets/variables'; .autocomplete-input-box { border: 1px solid $autocomplete-border-color; border-radius: 10px; background: transparent; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 700px; min-width: 550px; @media screen and (max-width: 48em) { width: 85vw; max-width: 550px; min-width: auto; } @media screen and (max-width: 40em) { width: 85vw; min-width: auto; } } .autocomplete-input-box__footer { position: relative; &::after { content: ''; position: absolute; width: 80%; margin: auto; height: 1px; } } ================================================ FILE: client/components/AutocompleteInputBox/AutocompleteInputBox.tsx ================================================ import React, { Component } from 'react' import cx from 'classnames' import { WithClassName } from '../../../types' type AutocompleteInputBoxProps = React.PropsWithChildren & WithClassName & { footer?: React.ReactNode } class AutocompleteInputBox extends Component { render() { const { children, footer, className } = this.props return (
{children} {footer && (
{footer}
)}
) } } export default AutocompleteInputBox ================================================ FILE: client/components/AutocompleteInputBox/index.ts ================================================ import AutocompleteInputBox from './AutocompleteInputBox' export default AutocompleteInputBox ================================================ FILE: client/components/BarGraph/BarGraph.scss ================================================ @use "sass:math"; @import '../../../stylesheets/colors'; @import '../../../stylesheets/variables'; $bar-width: 1.6vw; $min-bar-width: 20px; $bar-grow-duration: 0.4s; .bar-graph-container { display: flex; flex-direction: column; justify-content: center; width: 100%; height: 48vh; } .bar-graph { height: 40vh; padding-bottom: 6vh; display: flex; margin: 0; justify-content: center; } .bar-graph__bar-group { position: relative; height: 100%; margin: 0 3px; display: flex; width: $bar-width; min-width: $min-bar-width; justify-content: flex-end; flex-direction: column; animation: grow $bar-grow-duration cubic-bezier(0.305, 0.42, 0.205, 1.2); transform-origin: 100% 100%; } .bar-graph__bar-symbols { display: flex; flex-direction: column; margin-top: -500%; // don't know why this works :/ } .bar-graph__bar-symbol { text-align: center; svg { height: $global-spacing * 1.8; width: auto; } & + & { margin-top: math.div($global-spacing, 3); } } .bar-graph__bar, .bar-graph__bar2, .bar-graph__bar[data-balloon], .bar-graph__bar2[data-balloon] { width: 100%; left: 0; bottom: 0; transition: background 0.2s; cursor: pointer; } .bar-graph__bar, .bar-graph__bar[data-balloon] { background: $maya-blue; border-radius: 5px 5px 0 0; .bar-graph__bar-group:not(.bar-graph__bar-group--disabled):hover & { background: darken($maya-blue, 5%); } .bar-graph__bar-group--disabled & { background: lighten($raven, 45%); border-radius: 5px; &:hover { background: lighten($raven, 35%); } } } .bar-graph__bar2 { background: $cornflower-blue; z-index: 1; pointer-events: none; border-radius: 0 0 5px 5px; .bar-graph__bar-group:hover & { background: darken($cornflower-blue, 5%); } } .bar-graph__bar-version, .bar-graph__bar-symbols, .bar-graph__legend { animation: fade-in 0.5s $bar-grow-duration * 0.9 both cubic-bezier(0.305, 0.42, 0.205, 1.2); } .bar-graph__legend { @include font-size-xs; padding-top: $global-spacing; display: flex; text-transform: uppercase; justify-content: center; color: lighten($raven, 20%); } .bar-graph__legend__colorbox { width: $global-spacing * 1.5; height: $global-spacing * 1.5; margin-right: $global-spacing; border-radius: 3px; .bar-graph__legend__bar1 & { background: $maya-blue; } .bar-graph__legend__bar2 & { background: $cornflower-blue; } } .bar-graph__legend__bar1, .bar-graph__legend__bar2 { display: flex; align-items: center; } .bar-graph__legend__bar1 { margin-right: $global-spacing * 4; } @keyframes grow { from { transform: scaleY(0); } to { transform: scaleY(1); } } @keyframes fade-in { from { opacity: 0; } } ================================================ FILE: client/components/BarGraph/BarGraph.tsx ================================================ import React, { PureComponent } from 'react' import { formatSize } from '../../../utils' import TreeShakeIcon from '../Icons/TreeShakeIcon' import SideEffectIcon from '../Icons/SideEffectIcon' import { BarVersion } from '../BarVersion/BarVersion' export type Reading = { version: string size: number gzip: number disabled: boolean hasSideEffects: boolean hasJSModule: boolean hasJSNext: boolean isModuleType: boolean } type BarGraphProps = { readings: Reading[] onBarClick: (reading: Reading) => void } export default class BarGraph extends PureComponent { getScale = () => { const { readings } = this.props const gzipValues = readings .filter(reading => !reading.disabled) .map(reading => reading.gzip) const sizeValues = readings .filter(reading => !reading.disabled) .map(reading => reading.size) const maxValue = Math.max(...[...gzipValues, ...sizeValues]) return 100 / maxValue } getFirstSideEffectFreeIndex = () => { const { readings } = this.props const sideEffectFreeIntroducedRecently = !readings.every( reading => !reading.hasSideEffects ) const firstSideEffectFreeIndex = readings.findIndex( reading => !(reading.disabled || reading.hasSideEffects) ) return sideEffectFreeIntroducedRecently ? firstSideEffectFreeIndex : -1 } getFirstTreeshakeableIndex = () => { const { readings } = this.props const treeshakingIntroducedRecently = !readings.every( reading => reading.hasJSModule ) const firstTreeshakingIndex = readings.findIndex( reading => !reading.disabled && (reading.hasJSModule || reading.hasJSNext || reading.isModuleType) ) return treeshakingIntroducedRecently ? firstTreeshakingIndex : -1 } renderDisabledBar = (reading: Reading) => (
this.props.onBarClick(reading)} >
) renderActiveBar = ( reading: Reading, scale: number, options: { isFirstTreeshakeable: boolean; isFirstSideEffectFree: boolean } ) => { const getTooltipMessage = (reading: Reading) => { const formattedSize = formatSize(reading.size) const formattedGzip = formatSize(reading.gzip) return `Minified: ${parseFloat(formattedSize.size).toFixed(1)}${ formattedSize.unit } | Gzipped: ${parseFloat(formattedGzip.size).toFixed(1)}${ formattedGzip.unit }` } return (
this.props.onBarClick(reading)} key={reading.version} className="bar-graph__bar-group" >
{options.isFirstTreeshakeable && (
)} {options.isFirstSideEffectFree && (
)}
) } render() { const { readings } = this.props const graphScale = this.getScale() const firstTreeshakeableIndex = this.getFirstTreeshakeableIndex() const firstSideEffectFreeIndex = this.getFirstSideEffectFreeIndex() return (
{readings.map((reading, index) => reading.disabled ? this.renderDisabledBar(reading) : this.renderActiveBar(reading, graphScale, { isFirstTreeshakeable: index === firstTreeshakeableIndex, isFirstSideEffectFree: index === firstSideEffectFreeIndex, }) )}
Min
GZIP
) } } ================================================ FILE: client/components/BarGraph/index.ts ================================================ import BarGraph from './BarGraph' export default BarGraph ================================================ FILE: client/components/BarVersion/BarVersion.scss ================================================ @import '../../../stylesheets/colors'; @import '../../../stylesheets/variables'; .bar-graph__bar-version { @include font-size-xs; z-index: 33; font-weight: $font-weight-light; transform: rotate(-90deg) translateX(#{-$global-spacing * 1.5}); font-variant-numeric: tabular-nums; color: lighten($raven, 15%); transition: opacity 0.2s, color 0.2s; font-family: $font-family-code; letter-spacing: -1px; line-height: 1; cursor: pointer; height: $bar-width; text-align: end; display: flex; justify-content: flex-end; align-items: center; .bar-graph-container:hover & { color: $raven; } .bar-graph__bar-group:hover & { color: darken($raven, 20%); } } ================================================ FILE: client/components/BarVersion/BarVersion.tsx ================================================ import truncate from 'truncate' interface Props { version: string } export function BarVersion({ version }: Props) { return (
{truncate(version, 7)}
) } ================================================ FILE: client/components/BlogLayout/BlogLayout.scss ================================================ @import '../../../stylesheets/colors'; @import '../../../stylesheets/variables'; .page-container.blog { .page-content { justify-content: normal; } } .blog-layout__container { max-width: 80ch; padding: 0 2rem; width: 100%; h1 { margin-bottom: 0; } img, iframe { max-width: 100%; object-fit: contain; } } .blog-post__preview-read-more { @include font-size-xs(); letter-spacing: 1px; color: $gulf-blue; font-weight: $font-weight-bold; text-transform: uppercase; &:hover { color: lighten($gulf-blue, 20%); } } .blog-post__preview { h2 { font-weight: $dark-gulf-blue; color: #5c5c66; margin: 0; &:hover { color: lighten($gulf-blue, 10%); } } & + & { margin-top: 4rem; } } .blog-post__preview-content { color: darken($dark-raven, 10%); line-height: 1.6; @include font-size-reg(); a { color: darken($maya-blue, 20%); border-bottom: 1px solid $cornflower-blue; transition: all 0.2s; &:hover { color: darken($maya-blue, 40%); border-bottom: 1px dashed $cornflower-blue; } } } .blog-post__preview-date { @include font-size-sm; margin: 0.7rem 0 0; color: $raven; font-weight: $font-weight-light; } ================================================ FILE: client/components/BlogLayout/BlogLayout.tsx ================================================ import React from 'react' import { WithClassName } from '../../../types' import ResultLayout from '../ResultLayout' type BlogLayoutProps = React.PropsWithChildren & WithClassName const BlogLayout = ({ className, children }: BlogLayoutProps) => (
{children}
) export default BlogLayout ================================================ FILE: client/components/BlogLayout/index.ts ================================================ export { default } from './BlogLayout' ================================================ FILE: client/components/BuildProgressIndicator/BuildProgressIndicator.scss ================================================ @import '../../../stylesheets/colors'; @import '../../../stylesheets/variables'; .build-progress-indicator { display: flex; justify-content: center; align-items: center; flex-direction: column; flex-grow: 1; padding: 0 $global-spacing * 2; text-align: center; } .build-progress-indicator__text { font-size: 0.7rem; margin-top: $global-spacing * 3; color: lighten($raven, 15%); text-transform: uppercase; font-weight: $font-weight-bold; letter-spacing: 2px; line-height: 1.5; } ================================================ FILE: client/components/BuildProgressIndicator/BuildProgressIndicator.tsx ================================================ import React, { Component } from 'react' import ProgressHex from '../ProgressHex' const OptimisticLoadTimeout = 700 type BuildProgressIndicatorProps = { isDone: boolean onDone: () => void } type BuildProgressIndicatorState = { started: boolean progressText?: string } const order = ['resolving', 'building', 'minifying', 'calculating'] as const export default class BuildProgressIndicator extends Component< BuildProgressIndicatorProps, BuildProgressIndicatorState > { stage: number timeoutId?: ReturnType constructor(props: BuildProgressIndicatorProps) { super(props) this.stage = 0 this.state = { started: false, } } componentDidMount() { setTimeout(() => { if (!this.props.isDone) { this.setState({ started: true }) this.setMessage() } }, OptimisticLoadTimeout) } componentWillReceiveProps(nextProps: BuildProgressIndicatorProps) { if (nextProps.isDone) { this.stage = 3 this.props.onDone() } } shouldComponentUpdate( props: BuildProgressIndicatorProps, nextState: BuildProgressIndicatorState ) { return this.state.progressText !== nextState.progressText } componentWillUnmount() { clearTimeout(this.timeoutId) } getProgressText = (stage: typeof order[number]) => { const progressText = { resolving: 'Resolving version and dependencies', building: 'Bundling package', minifying: 'Minifying, GZipping', calculating: 'Calculating file sizes', } return progressText[stage] } setMessage = (stage = 0) => { const timings = { resolving: 3 + Math.random() * 2, building: 5 + Math.random() * 3, minifying: 3 + Math.random() * 2, calculating: 20, } if (this.stage === order.length) { //this.props.onDone() return } this.setState({ progressText: this.getProgressText(order[this.stage]), }) this.timeoutId = setTimeout(() => { if (this.stage < order.length) { this.stage += 1 } this.setMessage(this.stage) }, timings[order[stage]] * 1000) } render() { const { progressText, started } = this.state if (!started) { return null } return (

{progressText}

) } } ================================================ FILE: client/components/BuildProgressIndicator/index.ts ================================================ import BuildProgressIndicator from './BuildProgressIndicator' export default BuildProgressIndicator ================================================ FILE: client/components/Header/Header.tsx ================================================ import React, { Component } from 'react' import Sidebar from 'react-sidebar' import Link from 'next/link' import { WithClassName } from '../../../types' import GithubLogo from '../../assets/github-logo.svg' type HeaderProps = WithClassName type HeaderState = { sidebarDocked: boolean sidebarOpen: boolean } export default class Header extends Component { mql!: MediaQueryList constructor(props: HeaderProps) { super(props) this.state = { sidebarDocked: false, sidebarOpen: false, } } componentDidMount() { this.mql = window.matchMedia(`(min-width: 800px)`) this.setState({ sidebarDocked: this.mql.matches }) this.mql.addListener(this.mediaQueryChanged) } componentWillUnmount() { this.mql.removeListener(this.mediaQueryChanged) } onSetSidebarOpen(open: boolean) { this.setState({ sidebarOpen: open }) } mediaQueryChanged() { this.setState({ sidebarDocked: this.mql.matches, sidebarOpen: false }) } render() { return ( Sidebar content} open={this.state.sidebarOpen} docked={this.state.sidebarDocked} onSetOpen={this.onSetSidebarOpen} >
Bundle Phobia
Main content
) } } ================================================ FILE: client/components/Header/index.ts ================================================ import Header from './Header' export default Header ================================================ FILE: client/components/Icons/SearchIcon.tsx ================================================ import React from 'react' import { WithClassName } from '../../../types' export default function SearchIcon({ className }: WithClassName) { return ( ) } ================================================ FILE: client/components/Icons/SideEffectIcon.scss ================================================ svg.sideeffect-icon-animated { overflow: hidden; .side-effect-icon-svg__circle, .side-effect-icon-svg__arrows { transform-origin: 50% 50%; transition: all 0.2s; } &:hover { .side-effect-icon-svg__arrows { transform: scale(1.3); stroke-width: 0.3px; } .side-effect-icon-svg__circle { transform: scale(1.2); stroke-width: 0.6px; } } } @keyframes shrink-arrows { from { transform: scale(2); } to { transform: scale(1); } } @keyframes grow-circle { from { transform: scale(1.5); } to { transform: scale(1); } } ================================================ FILE: client/components/Icons/SideEffectIcon.tsx ================================================ import React from 'react' import cx from 'classnames' import { WithClassName } from '../../../types' import SideEffectIconSVG from '../../assets/side-effect.svg' export default function TreeShakeIcon({ className }: WithClassName) { return ( ) } ================================================ FILE: client/components/Icons/TreeShakeIcon.scss ================================================ svg.treeshake-icon-animated { .tree-shake-icon-svg__bush { transition: transform 0.3s; transform-origin: 50% 100%; } &:hover { .tree-shake-icon-svg__shake { transform-origin: 50% 50%; animation: move-to-sides 0.3s, shake 0.3s 0.15s; } .tree-shake-icon-svg__bush { transform: scaleY(1.2); } } } @keyframes move-to-sides { from { transform: scale(0); } to { transform: scale(1); } } @keyframes shake { 10%, 100% { transform: translate3d(-0.5px, 0, 0); } 80% { transform: translate3d(1px, 0, 0); } 30%, 70% { transform: translate3d(-1px, 0, 0); } 60% { transform: translate3d(1px, 0, 0); } } ================================================ FILE: client/components/Icons/TreeShakeIcon.tsx ================================================ import React from 'react' import cx from 'classnames' import { WithClassName } from '../../../types' import TreeShakeIconSVG from '../../assets/tree-shake.svg' export default function TreeShakeIcon({ className }: WithClassName) { return ( ) } ================================================ FILE: client/components/JumpingDots/JumpingDots.scss ================================================ @import '../../../stylesheets/variables'; .jumping-dots { position: relative; text-align: center; padding: 0 $global-spacing * 0.5; } .jumping-dots__dot { display: inline-block; width: 2px; height: 2px; border-radius: 50%; margin-right: 3px; background: #303131; animation: dots-wave 1s linear infinite; &:nth-child(2) { animation-delay: -0.9s; } &:nth-child(3) { animation-delay: -0.8s; } } @keyframes dots-wave { 0%, 60%, 100% { transform: initial; } 30% { transform: translateY(-8px); } } ================================================ FILE: client/components/JumpingDots/JumpingDots.tsx ================================================ import React from 'react' export default function JumpingDots() { return ( ) } ================================================ FILE: client/components/JumpingDots/index.ts ================================================ export { default } from './JumpingDots' ================================================ FILE: client/components/Layout/Layout.scss ================================================ @use "sass:math"; @import '../../../stylesheets/variables'; @import '../../../stylesheets/base'; @import '../../../stylesheets/colors'; $footer-content-max-width: 800px; .layout { max-width: 100%; } footer { background: #222; width: 100%; display: flex; align-items: center; justify-content: center; padding: 0 0 $global-spacing * 5; color: lighten($raven, 27%); flex-direction: column; a { color: lighten($raven, 27%); transition: color 0.2s; &:hover { color: lighten($raven, 40%); } } } .footer__recent-search-bar { width: 100%; background: darken($raven, 22%); padding: 0 $global-spacing * 2; } .footer__recent-search-bar__wrap { max-width: $footer-content-max-width; display: flex; align-items: center; justify-content: center; margin: auto; h4 { @include font-size-xs; color: lighten($raven, 27%); margin: 0; line-height: 1.2; } } .footer__recent-search-list { @include font-size-xs; display: flex; padding: 0; margin: 0 0 0 $global-spacing * 1.5; flex-grow: 1; max-width: 960px; @media screen and (max-width: 40em) { margin: 0; } li { list-style: none; position: relative; flex-grow: 1; text-align: center; font-family: $font-family-code; letter-spacing: 0.5px; a { padding: $global-spacing $global-spacing * 2; @media screen and (max-width: 48em) { padding: $global-spacing $global-spacing * 0.5; } } &:not(:first-of-type) { &::after { content: ''; width: 1px; height: 60%; background: rgba(255, 255, 255, 0.1); position: absolute; left: 0; top: 0; bottom: 0; margin: auto; } } @media screen and (max-width: 48em) { &:nth-child(n + 5) { display: none; } } @media screen and (max-width: 40em) { &:nth-child(n + 4) { display: none; } } } } .footer__split { display: flex; max-width: $footer-content-max-width; padding: $global-spacing * 3 $global-spacing; margin: auto; @media screen and (max-width: 40em) { padding: $global-spacing * 3 $global-spacing * 2.5; flex-direction: column; } } .footer__hosting-credits { @include font-size-xs; border-top: 1px solid darken($raven, 25%); color: lighten($raven, 70%); text-transform: uppercase; letter-spacing: 2px; padding-top: $global-spacing; padding-right: $global-spacing; font-size: 12px; width: $global-spacing * 20; @media screen and (max-width: 40em) { text-align: center; padding-top: $global-spacing * 1.5; margin: $global-spacing * 3 auto auto; } } .footer__zeit-logo { width: $global-spacing; height: $global-spacing; margin: 0 $global-spacing * 0.5; } .footer__credits { display: flex; flex-direction: column; align-items: center; flex-basis: 33%; p { margin: 0; } @media screen and (max-width: 40em) { margin-top: $global-spacing * 2; } } .footer__description { @include font-size-xs; color: lighten($raven, 20%); flex-basis: 66%; p { text-align: left; line-height: 1.4; a { display: inline; } code { font-family: $font-family-code; padding: 0 math.div($global-spacing, 3) 0 $global-spacing; opacity: 0.9; } } } .footer__credits__heart { width: 8vw; height: 8vw; max-width: 100px; path { fill: mix($raven, $maya-blue); } &:hover { path { animation: pulse 10s infinite both; } } } @keyframes pulse { 0% { fill: mix($raven, $maya-blue); } 25% { fill: mix($raven, $cornflower-blue); } 50% { fill: $raven; } 75% { fill: mix($raven, $maya-blue); } } .footer__credits-fork-button { @include font-size-xs; cursor: pointer; margin-top: $global-spacing; border: 2px solid lighten($raven, 30%); background: transparent; border-radius: 10px; padding: math.div($global-spacing, 1.5) $global-spacing; display: block; transition: background 0.2s; color: lighten($raven, 30%); text-transform: uppercase; letter-spacing: 1.5px; font-size: 10px; font-weight: $font-weight-bold; &:hover { background: lighten($raven, 30%); color: #212121; } @media screen and (max-width: 48em) { padding: $global-spacing $global-spacing * 2; margin-top: $global-spacing * 2; } } .footer__credits-profile { margin-top: -$global-spacing * 1.5; margin-bottom: $global-spacing * 0.5; } .footer__sponsor-logo { margin-top: $global-spacing; } footer { p { text-align: center; } a { display: block; text-decoration: none; } } ================================================ FILE: client/components/Layout/Layout.tsx ================================================ import React, { Component } from 'react' import Link from 'next/link' import API from '../../api' import Heart from '../../assets/heart.svg' import DigitalOceanLogo from '../../assets/digital-ocean-logo.svg' import { AnnouncementBanner } from '../AnnouncementBanner' import { WithClassName } from '../../../types' type LayoutProps = React.PropsWithChildren & WithClassName type LayoutState = { recentSearches: string[] } export default class Layout extends Component { state = { recentSearches: [], } componentDidMount() { API.getRecentSearches(5).then(searches => { this.setState({ recentSearches: Object.keys(searches), }) }) } render() { const { children, className } = this.props const { recentSearches } = this.state return (
{children}

Recent searches

    {recentSearches.map(search => (
  • {search}
  • ))}

What does Bundlephobia do?

JavaScript bloat is more real today than it ever was. Sites continuously get bigger as more (often redundant) libraries are thrown to solve new problems. Until of-course, the{' '} big rewrite happens.

Bundlephobia lets you understand the performance cost of npm install ing a new npm package before it becomes a part of your bundle. Analyze size, compositions and exports

Credits to{' '} {' '} @thekitze{' '} for the name.

Hosted on
) } } ================================================ FILE: client/components/Layout/index.ts ================================================ import Layout from './Layout' export default Layout ================================================ FILE: client/components/MetaTags.tsx ================================================ import React from 'react' import Head from 'next/head' export const DEFAULT_DESCRIPTION_START = 'Bundlephobia helps you find the performance impact of npm packages.' type MetaTagsProps = { title: string canonicalPath: string description?: string twitterDescription?: string image?: string isLargeImage?: boolean } export default function MetaTags({ description, twitterDescription, title, canonicalPath, image, isLargeImage, }: MetaTagsProps) { const defaultDescription = `${DEFAULT_DESCRIPTION_START} Find the size of any javascript package and its effect on your frontend bundle.` const defaultImage = 'https://bundlephobia.com/android-chrome-256x256.png' const origin = typeof window === 'undefined' ? 'https://bundlephobia.com' : window.location.origin return ( {title} {twitterDescription && ( )} {isLargeImage ? ( ) : ( )} ) } ================================================ FILE: client/components/PageNav/PageNav.tsx ================================================ import Link from 'next/link' import React from 'react' import GithubLogo from '../../assets/github-logo.svg' type PageNavProps = { minimal?: boolean } const PageNav = ({ minimal }: PageNavProps) => (
{!minimal && (
Bundle Phobia
)}
) export default PageNav ================================================ FILE: client/components/PageNav/index.ts ================================================ export { default } from './PageNav' ================================================ FILE: client/components/ProgressHex/ProgressHex.scss ================================================ .progress-hex { width: 8rem; height: 8rem; contain: strict; will-change: transform; circle { fill: #212121; transform-box: view-box; transform-origin: 50% 50%; } } .progress-hex__trail { stroke-width: 1px; } ================================================ FILE: client/components/ProgressHex/ProgressHex.tsx ================================================ import React, { Component } from 'react' import ProgressHexAnimator from './progress-hex-timeline' type ProgressHexProps = { compact?: boolean } class ProgressHex extends Component { svgRef: React.RefObject animator?: ProgressHexAnimator timeline?: ReturnType constructor(props: ProgressHexProps) { super(props) this.svgRef = React.createRef() } componentDidMount() { this.animator = new ProgressHexAnimator({ svg: this.svgRef.current! }) this.timeline = this.animator.createTimeline() this.timeline.play() } componentWillUnmount() { this.timeline?.pause() } render() { const { compact } = this.props return ( {!compact && ( )} ) } } export default ProgressHex ================================================ FILE: client/components/ProgressHex/index.ts ================================================ export { default } from './ProgressHex' ================================================ FILE: client/components/ProgressHex/progress-hex-timeline.ts ================================================ import anime, { AnimeAnimParams } from 'animejs' import colors from '../../config/colors' import { randomFromArray, zeroToN } from '../../../utils' const DURATION = 1000 type Circle = { cx: number; cy: number; ringNumber: number } type CirclesMap = Map type ProgressHexAnimatorProps = { svg: SVGSVGElement } export default class ProgressHexAnimator { circlesMap: CirclesMap circles: NodeListOf rings: NodeListOf trailBlaze: Trailblaze width: number height: number constructor({ svg }: ProgressHexAnimatorProps) { this.circlesMap = new Map() this.circles = svg.querySelectorAll('circle') this.rings = svg.querySelectorAll('g') this.trailBlaze = new Trailblaze({ linesCount: 55, svg, circlesMap: this.circlesMap, ringsCount: this.rings.length, }) Array.from(this.circles).forEach(circle => { const cx = parseFloat(circle.getAttribute('cx')!) const cy = parseFloat(circle.getAttribute('cy')!) circle.style.transformOrigin = `${cx}px ${cy}px` this.circlesMap.set(circle, { cx, cy, ringNumber: parseInt(circle.parentElement!.id.match(/.+(\d+)/)![1]) - 1, }) }) this.width = parseFloat(svg.getAttribute('width')!) this.height = parseFloat(svg.getAttribute('height')!) } getTranslation(circle: SVGCircleElement, distance: number) { const { cx, cy } = this.circlesMap.get(circle)! const { x, y } = this.pointAtDistance( cx, cy, this.width / 2, this.height / 2, distance ) return { x: x - cx, y: y - cy } } pointAtDistance(x1: number, y1: number, x2: number, y2: number, d: number) { const curDistanceBetweenPoints = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) if (curDistanceBetweenPoints === 0) return { x: x1, y: y1 } const t = d / curDistanceBetweenPoints const x = (x1 - t * x2) / (1 - t) const y = (y1 - t * y2) / (1 - t) return { x, y } } createTimeline() { const fadeInTimeline = anime.timeline({ duration: DURATION, autoplay: false, loop: false, }) const quakeTimeline = anime.timeline({ duration: DURATION, autoplay: false, loop: true, }) const fadeInRings = { targets: this.rings, opacity: [0, 1], delay: anime.stagger(DURATION / 5, { from: 'last' }), duration: DURATION / 2, easing: 'linear', } const quakeCircles = { targets: this.circles, scale: (el: SVGCircleElement) => this.circlesMap.get(el)!.ringNumber === 0 ? 3 : 1.5, translateY: (circle: SVGCircleElement) => this.getTranslation(circle, 4).y, translateX: (circle: SVGCircleElement) => this.getTranslation(circle, 4).x, delay: ((el: SVGCircleElement) => (Math.pow(this.circlesMap.get(el)!.ringNumber, 0.6) * DURATION) / 4 + (this.circlesMap.get(el)!.ringNumber > 0 ? DURATION / 2.5 : 0)) as unknown as AnimeAnimParams['delay'], duration: DURATION, easing: () => (t: number) => Math.sin(t * Math.PI), changeBegin: () => this.trailBlaze.start(), } fadeInTimeline.add(fadeInRings) quakeTimeline.add(quakeCircles) return { ...quakeTimeline, play: () => { fadeInTimeline.play() setTimeout(() => { quakeTimeline.play() }, DURATION) }, } } } type TrailblazeProps = { svg: SVGSVGElement circlesMap: CirclesMap linesCount: number ringsCount: number } class Trailblaze { circlesMap: CirclesMap ringsCount: number lines: SVGLineElement[] constructor({ linesCount, svg, circlesMap, ringsCount }: TrailblazeProps) { this.lines = [] this.circlesMap = circlesMap this.ringsCount = ringsCount for (let i = 0; i < linesCount; i++) { const line = this.createTrail() this.lines.push(line) svg.insertBefore(line, svg.children[0]) } } createTrail() { const line = document.createElementNS('http://www.w3.org/2000/svg', 'line') line.setAttribute('stroke-width', '0.5') line.setAttribute('class', 'progress-hex__trail') return line } setLineCoords(line: SVGLineElement, x1 = 0, x2 = 0, y1 = 0, y2 = 0) { line.setAttribute('x1', `${x1}`) line.setAttribute('x2', `${x2}`) line.setAttribute('y1', `${y1}`) line.setAttribute('y2', `${y2}`) } getCirclesInRing(ringNumber: number) { const circles: Circle[] = [] this.circlesMap.forEach(value => { if (value.ringNumber === ringNumber) { circles.push(value) } }) return circles } distanceBetweenCircles(c1: Circle, c2: Circle) { return Math.sqrt((c2.cx - c1.cx) ** 2 + (c2.cy - c1.cy) ** 2) } getRandomConnection() { const rings = zeroToN(this.ringsCount) const sourceRingNumber = randomFromArray(rings.slice(0, -1)) const destinationRingNumber = sourceRingNumber + 1 const eligibleSourceCircles = this.getCirclesInRing(sourceRingNumber) const sourceCircle = randomFromArray(eligibleSourceCircles) const eligibleDestinationCircles = this.getCirclesInRing( destinationRingNumber ) const destinationCircleDistances = eligibleDestinationCircles.map( (circle, index) => ({ index, distance: this.distanceBetweenCircles(sourceCircle, circle), }) ) const eligibleDistancesMin = Math.min( ...destinationCircleDistances.map(a => a.distance) ) const eligibleDestinationIndexes = destinationCircleDistances .filter(c => Math.abs(eligibleDistancesMin - c.distance) < 2) .map(d => d.index) const destinationCircle = eligibleDestinationCircles[randomFromArray(eligibleDestinationIndexes)] return { source: sourceCircle, destination: destinationCircle, } } getDashOffset = (element: SVGElement | HTMLElement | null) => { if (!element) return 0 try { return anime.setDashoffset(element) } catch (err) { // Called before the element was rendered console.error(err) return 0 } } start() { const lineMap = new WeakMap< SVGLineElement, { source: Circle; destination: Circle } >() this.lines.forEach(line => { const { source, destination } = this.getRandomConnection() lineMap.set(line, { source, destination }) line.setAttribute('stroke', randomFromArray(colors)) this.setLineCoords( line, source.cx, destination.cx, source.cy, destination.cy ) }) anime({ targets: this.lines, opacity: [1, 0.9, 0], strokeDashoffset: [(el: SVGLineElement) => this.getDashOffset(el), 0], x1: (el: SVGLineElement) => lineMap.get(el)!.source.cx, x2: (el: SVGLineElement) => lineMap.get(el)!.destination.cx, y1: (el: SVGLineElement) => lineMap.get(el)!.source.cy, y2: (el: SVGLineElement) => lineMap.get(el)!.destination.cy, duration: 500, delay: () => anime.random(0, DURATION / 5), easing: 'easeOutCubic', }) } } ================================================ FILE: client/components/QuickStatsBar/QuickStatsBar.scss ================================================ @use "sass:math"; @import '../../../stylesheets/colors'; @import '../../../stylesheets/variables'; .quick-stats-bar { display: flex; align-content: center; @include font-size-xs; color: lighten($raven, 15%); background: lighten($raven, 55%); border-radius: 0 0 10px 10px; overflow: hidden; } .quick-stats-bar__stat { padding: math.div($global-spacing, 1.5) $global-spacing * 1.5; display: flex; align-content: center; position: relative; justify-content: center; flex: 1 1 auto; white-space: nowrap; margin: auto 0; & > * { margin: auto 0; } &:not(:first-of-type)::before { content: ''; width: 1px; height: 60%; position: absolute; background: transparentize(black, 0.95); top: 0; bottom: 0; margin: auto; left: 0; } &:first-of-type, &:last-of-type { //padding-left: $global-spacing; } } .quick-stats-bar__stat--optional { @media screen and (max-width: 48em) { display: none; } @media screen and (max-width: 40em) { display: none; } } .quick-stats-bar__stat--description { overflow: hidden; text-overflow: ellipsis; display: block; flex-grow: 1; @media screen and (max-width: 40em) { display: none; } } .quick-stats-bar__stat--description-content { margin-left: $global-spacing; } .quick-stats-bar__stat-icon { margin-right: $global-spacing; } .quick-stats-bar__logo-icon { vertical-align: middle; &.quick-stats-bar__logo-icon--npm { width: 36px; } &.quick-stats-bar__logo-icon--github { height: 18px; width: 18px; } path { transition: fill 0.2s; fill: lighten($raven, 15%); } } .quick-stats-bar__link { margin: auto $global-spacing * 0.5; &:hover { .quick-stats-bar__logo-icon--github { path { fill: #333; } } .quick-stats-bar__logo-icon--npm { path { fill: #cb3837; } } } } ================================================ FILE: client/components/QuickStatsBar/QuickStatsBar.tsx ================================================ import React, { Component } from 'react' import { sanitizeHTML } from '../../../utils/common.utils' import TreeShakeIcon from '../../assets/tree-shake.svg' import SideEffectIcon from '../../assets/side-effect.svg' import DependencyIcon from '../../assets/dependency.svg' import GithubIcon from '../../assets/github-logo.svg' import NPMIcon from '../../assets/npm-logo.svg' import InfoIcon from '../../assets/info.svg' import { PackageInfo } from '../../../types' type QuickStatsBarProps = Pick< PackageInfo, | 'name' | 'description' | 'repository' | 'dependencyCount' | 'isTreeShakeable' | 'hasSideEffects' > class QuickStatsBar extends Component { static defaultProps = { description: '', } getStatItemCount = () => { const { isTreeShakeable, hasSideEffects } = this.props let statItemCount = 0 if (isTreeShakeable) statItemCount += 1 if (hasSideEffects !== true) statItemCount += 1 return statItemCount } getTrimmedDescription = () => { const { description } = this.props const trimmed = description.trim() if (trimmed.endsWith('.')) { return trimmed.substring(0, trimmed.length - 1) } else { return trimmed } } render() { const { isTreeShakeable, hasSideEffects, dependencyCount, name, repository, } = this.props const statItemCount = this.getStatItemCount() const description = this.getTrimmedDescription() return (
{statItemCount < 2 && ( )}
{isTreeShakeable && (
{' '} tree-shakeable
)} {!(hasSideEffects === true) && (
{' '} {!(hasSideEffects === false) && hasSideEffects.length ? 'some side-effects' : 'side-effect free'}
)}
{dependencyCount === 0 ? ( 'no dependencies' ) : ( {dependencyCount}{' '} {dependencyCount > 1 ? 'dependencies' : 'dependency'} )}
{repository && ( )}
) } } export default QuickStatsBar ================================================ FILE: client/components/QuickStatsBar/index.ts ================================================ import QuickStatsBar from './QuickStatsBar' export default QuickStatsBar ================================================ FILE: client/components/ResultLayout/ResultLayout.scss ================================================ @import '../../../stylesheets/variables'; @import '../../../stylesheets/colors'; .page-header { padding: $global-spacing * 3; padding-bottom: $global-spacing * 2; display: flex; align-items: center; @media screen and (max-width: 40em) { padding: $global-spacing * 2; } } .page-header--right-section { margin-left: auto; display: flex; align-items: center; } .github-logo { width: 30px; height: 30px; @media screen and (max-width: 40em) { width: 20px; height: 20px; } path { fill: #666; transition: fill 0.2s; } &:hover { path { fill: black; } } } .logo-small { @include font-size-reg; text-transform: uppercase; font-weight: $font-weight-very-bold; letter-spacing: 3px; user-select: none; cursor: pointer; color: #212121; } .logo-small__alt { color: #888; } .page-container { display: flex; flex-direction: column; min-height: 100vh; min-height: calc(100vh - 6px); flex-gorw: 1; } .page-content { display: flex; align-items: center; justify-content: center; flex-direction: column; flex-grow: 1; @media screen and (max-width: 40em) { padding: 0 $global-spacing * 2; } } .page-header__quicklinks { list-style: none; margin: 0 2rem 0 0; font-weight: $font-weight-light; display: flex; a { @include font-size-xs; text-transform: uppercase; letter-spacing: 0.3px; font-weight: $font-weight-bold; opacity: 0.55; color: $raven; transition: opacity 0.2s; &:hover { opacity: 1; } } li + li { margin-left: $global-spacing * 2; } @media screen and (max-width: 40em) { max-width: 40vw; overflow: scroll; align-items: center; justify-content: flex-end; overflow: scroll; } } ================================================ FILE: client/components/ResultLayout/ResultLayout.tsx ================================================ import React, { Component } from 'react' import cx from 'classnames' import Layout from '../../components/Layout' import PageNav from '../PageNav' import { WithClassName } from '../../../types' export default class ResultLayout extends Component< React.PropsWithChildren & WithClassName > { render() { const { children, className } = this.props return (
{children}
) } } ================================================ FILE: client/components/ResultLayout/index.ts ================================================ import ResultLayout from './ResultLayout' export default ResultLayout ================================================ FILE: client/components/Separator.tsx ================================================ import React from 'react' type SeparatorProps = { text?: string align?: React.CSSProperties['justifyContent'] showLeft?: boolean containerStyles?: React.CSSProperties } export default function Separator({ text = 'or', align = 'center', showLeft = true, containerStyles, }: SeparatorProps) { const commonStyles = { display: 'flex', justifyContent: align, alignItems: 'center', } return (
{showLeft && (
)}
{!showLeft && (
)}
{text && ( {text} )}
) } ================================================ FILE: client/components/SimilarPackageCard/SimilarPackageCard.scss ================================================ @use "sass:math"; @import '../../../stylesheets/colors'; @import '../../../stylesheets/variables'; .similar-package-card { border: 1px solid $autocomplete-border-color; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); border-radius: 10px; width: calc(25% - #{$global-spacing * 2}); display: flex; flex-direction: column; margin: $global-spacing; color: inherit; transition: all 0.2s; &:hover { transform: scale(1.01); box-shadow: 0 5px 10px rgba(0, 0, 0, 0.06); border: 1px solid fade-in($autocomplete-border-color, 0.02); } &.similar-package-card--empty { border-style: dashed; background: transparentize($raven, 0.97); } @media screen and (max-width: 64em) { margin: math.div($global-spacing, 1.5); width: calc(33.3% - #{$global-spacing * 2}); } @media screen and (max-width: 48em) { margin: math.div($global-spacing, 1.5); width: calc(50% - #{$global-spacing * 1.5}); } @media screen and (max-width: 40em) { width: 100%; } } .similar-package-card--empty { color: transparentize($raven, 0.5); align-items: center; } .similar-package-card__wrap { padding: $global-spacing * 2; flex-grow: 1; .similar-package-card--empty & { padding: $global-spacing * 5 $global-spacing * 2; align-items: center; text-align: center; } } .similar-package-card__header { display: flex; } .similar-package-card__name { margin: 0; font-family: $font-family-code; font-weight: $font-weight-light; flex-grow: 1; word-break: break-word; } .similar-package-card__description { @include font-size-sm; color: $dark-raven; line-height: 1.5; margin: $global-spacing 0 0 0; word-break: break-word; img { height: auto; max-width: 100%; } .similar-package-card--empty & { text-transform: uppercase; letter-spacing: 1px; font-weight: $font-weight-bold; color: transparentize($raven, 0.4); } } .similar-package-card__footer { background: lighten($raven, 54%); display: flex; padding: $global-spacing $global-spacing * 2; align-items: center; border-radius: 0 0 10px 10px; } .similar-package-card__stat { & + & { margin-left: $global-spacing * 2; } } .similar-package-card__number { @include font-size-md; font-weight: $font-weight-very-bold; } .similar-package-card__comparison--positive { color: $pastel-green; } .similar-package-card__comparison--negative { color: $carrot-orange; } .similar-package-card__comparison--similar { color: $raven; } .similar-package-card__label { @include font-size-xs; text-transform: uppercase; letter-spacing: 1px; line-height: 1.5; .similar-package-card__size & { color: $raven; } } .similar-package-card__treeshake { height: $global-spacing * 2.5; width: auto; margin-left: auto; } .similar-package-card__shrink { font-size: 75%; .similar-package-card__size & { color: $raven; } } .similar-package-card__github-icon { height: $global-spacing * 2; width: auto; opacity: 0.5; transition: all 0.2s; vertical-align: middle; &:hover { opacity: 1; } } .similar-package-card__plus { width: 35%; height: auto; margin-bottom: $global-spacing * 1.5; path { fill: transparentize($raven, 0.7); } } ================================================ FILE: client/components/SimilarPackageCard/SimilarPackageCard.tsx ================================================ import React, { Component } from 'react' import cx from 'classnames' import Link from 'next/link' import queryString from 'query-string' import { formatSize } from '../../../utils' import { sanitizeHTML } from '../../../utils/common.utils' import TreeShakeIcon from '../../assets/tree-shake.svg' import PlusIcon from '../../assets/plus.svg' import GithubIcon from '../../assets/github-logo.svg' import GitIcon from '../../assets/git-logo.svg' type SimilarPackageCardProps = { category?: string } & ( | { pack: any; comparisonSizePercent: number } | { isEmpty: true } ) export default class SimilarPackageCard extends Component { getSuggestionIssueUrl = () => { const params = queryString.stringify({ labels: 'similar suggestion', template: '2-similar-package-suggestion.md', title: `Package suggestion: for \`${this.props.category}\``, }) return `https://github.com/pastelsky/bundlephobia/issues/new?${params}` } render() { if ('isEmpty' in this.props) { return (

Suggest another

) } const { pack, comparisonSizePercent } = this.props const { size, unit } = formatSize(pack.gzip) const sizeDiff = Math.abs( (comparisonSizePercent / 100) * pack.gzip - pack.gzip ) const getComparisonNumber = (comparisonSizePercent: number) => { if (sizeDiff < 1500) { return (
Similar
size
) } else if (Math.abs(comparisonSizePercent) > 100) { return (
{(1 + Math.abs(comparisonSizePercent) / 100).toFixed(1)}{' '} ×
{comparisonSizePercent > 0 ? 'Larger' : 'Smaller'}
) } else { return (
{Math.abs(comparisonSizePercent).toFixed(0)}{' '} %
{comparisonSizePercent > 0 ? 'Larger' : 'Smaller'}
) } } const footer = (
0, })} > {getComparisonNumber(comparisonSizePercent)}
{size.toFixed(2)} {unit}
Min + Gzip
{(pack.hasJSModule || pack.hasJSNext || pack.isModuleType) && ( )}
) return ( {footer} ) } } ================================================ FILE: client/components/SimilarPackageCard/index.ts ================================================ import SimilarPackageCard from './SimilarPackageCard' export default SimilarPackageCard ================================================ FILE: client/components/Stat/Stat.scss ================================================ @import '../../../stylesheets/variables'; @import '../../../stylesheets/colors'; .stat-container { margin: 0 24px; @media screen and (max-width: 40em) { margin: 0 $global-spacing; } } .stat-container--compact { margin: 0; } .stat-container__value-container { display: flex; align-items: baseline; justify-content: center; padding: 5px 15px; .stat-container--compact & { padding-top: 0; } } .stat-container__value { @include font-size-xxl; font-weight: bold; color: #212121; background: inherit; position: relative; .stat-container--compact & { @include font-size-lg; font-weight: $font-weight-light; } } .stat-container__value.time::before { content: attr(data-value); position: absolute; z-index: 2; overflow: hidden; color: $pastel-green; white-space: nowrap; width: 0%; transition: width 0.3s; } .stat-container__value.time:hover::before { width: 100%; transition-duration: inherit; } .stat-container__unit { @include font-size-xl; color: $raven; font-weight: bold; margin-left: 4px; .stat-container--compact & { @include font-size-sm; font-weight: $font-weight-thin; } } .stat-container__footer { display: flex; justify-content: center; align-items: center; margin-top: 10px; .stat-container--compact & { margin-top: 0; } } .stat-container__label { @include font-size-reg; color: $raven; text-transform: uppercase; letter-spacing: 2px; text-align: center; .stat-container--compact & { @include font-size-sm; letter-spacing: 1px; } } .stat-container__info-text { margin-left: $global-spacing * 0.5; border: 1px solid rgba(40, 40, 40, 0.5); color: rgba(40, 40, 40, 0.5); width: 12px; height: 13px; font-family: $font-family-code; font-size: 12px; display: flex; align-items: center; justify-content: center; border-radius: 2px; transition: background 0.2s; cursor: help; &:hover { background: rgba(40, 40, 40, 0.6); color: white; } &::after, &::before { font-family: $font-family-body; } @media screen and (max-width: 40em) { display: none; } } ================================================ FILE: client/components/Stat/Stat.tsx ================================================ import React from 'react' import cx from 'classnames' import { formatSize, formatTime } from '../../../utils' import { WithClassName } from '../../../types' const Type = { SIZE: 'size', TIME: 'time', } as const type StatProps = WithClassName & { value: number type: 'size' | 'time' label: string infoText?: string compact?: boolean } export default function Stat({ value, label, type, infoText, compact, className, }: StatProps) { const roundedValue = type === Type.SIZE ? parseFloat(formatSize(value).size.toFixed(1)) : parseFloat(formatTime(value).size.toFixed(2)) return (
{roundedValue}
{type === Type.SIZE ? formatSize(value).unit : formatTime(value).unit}{' '}
{label}
{infoText && (
i
)}
) } Stat.type = Type ================================================ FILE: client/components/Stat/index.ts ================================================ export { default } from './Stat' ================================================ FILE: client/components/Treemap/Treemap.tsx ================================================ import React, { Component } from 'react' import squarify from './squarify' type TreeMapProps = { width: number height: number } & React.PropsWithChildren & React.HTMLAttributes class TreeMap extends Component { render() { const { width, height, children, ...others } = this.props const values = React.Children.map(children, square => React.isValidElement(square) ? square.props.value : square ) const squared = squarify(values, width, height, 0, 0) const getBorderRadius = (index: number) => { const topLeftRadius = squared[index][0] || squared[index][1] ? '0px' : '10px' const topRightRadius = squared[index][1] === 0 && squared[index][2] === width ? '10px' : '0px' const bottomLeftRadius = squared[index][3] === height && squared[index][0] === 0 ? '10px' : '0px' const bottomRightRadius = Math.round(squared[index][3]) === height && Math.round(squared[index][2]) === width ? '10px' : '0px' return `${topLeftRadius} ${topRightRadius} ${bottomRightRadius} ${bottomLeftRadius}` } return (
{React.Children.map(children, (child, index) => { if (!React.isValidElement(child)) { return child } const childProps = { left: `${(squared[index][0] / width) * 100}%`, top: `${(squared[index][1] / height) * 100}%`, width: `${ ((squared[index][2] - squared[index][0]) / width) * 100 }%`, height: `${ ((squared[index][3] - squared[index][1]) / height) * 100 }%`, borderRadius: getBorderRadius(index), data: squared[index], } return React.cloneElement(child, childProps) })}
) } } export default TreeMap ================================================ FILE: client/components/Treemap/TreemapSquare.tsx ================================================ import React from 'react' type TreemapSquareProps = { style: React.CSSProperties data?: any } & React.PropsWithChildren & Pick< React.CSSProperties, 'left' | 'top' | 'width' | 'height' | 'borderRadius' > function TreemapSquare({ children, left, top, width, height, borderRadius, data, style, ...other }: TreemapSquareProps) { return (
{children}
) } export default TreemapSquare ================================================ FILE: client/components/Treemap/index.ts ================================================ import Treemap from './Treemap' import TreemapSquare from './TreemapSquare' export { Treemap, TreemapSquare } ================================================ FILE: client/components/Treemap/squarify.js ================================================ /* * treemap-squarify.js - open source implementation of squarified treemaps * * Treemap Squared 0.5 - Treemap Charting library * * https://github.com/imranghory/treemap-squared/ * * Copyright (c) 2012 Imran Ghory (imranghory@gmail.com) * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. * * * Implementation of the squarify treemap algorithm described in: * * Bruls, Mark; Huizing, Kees; van Wijk, Jarke J. (2000), "Squarified treemaps" * in de Leeuw, W.; van Liere, R., Data Visualization 2000: * Proc. Joint Eurographics and IEEE TCVG Symp. on Visualization, Springer-Verlag, pp. 33–42. * * Paper is available online at: http://www.win.tue.nl/~vanwijk/stm.pdf * * The code in this file is completeley decoupled from the drawing code so it should be trivial * to port it to any other vector drawing library. Given an array of datapoints this library returns * an array of cartesian coordinates that represent the rectangles that make up the treemap. * * The library also supports multidimensional data (nested treemaps) and performs normalization on the data. * * See the README file for more details. */ function Container(xoffset, yoffset, width, height) { this.xoffset = xoffset // offset from the the top left hand corner this.yoffset = yoffset // ditto this.height = height this.width = width this.shortestEdge = function () { return Math.min(this.height, this.width) } // getCoordinates - for a row of boxes which we've placed // return an array of their cartesian coordinates this.getCoordinates = function (row) { const coordinates = [] var subxoffset = this.xoffset, subyoffset = this.yoffset //our offset within the container var areawidth = sumArray(row) / this.height var areaheight = sumArray(row) / this.width var i if (this.width >= this.height) { for (i = 0; i < row.length; i++) { coordinates.push([ subxoffset, subyoffset, subxoffset + areawidth, subyoffset + row[i] / areawidth, ]) subyoffset = subyoffset + row[i] / areawidth } } else { for (i = 0; i < row.length; i++) { coordinates.push([ subxoffset, subyoffset, subxoffset + row[i] / areaheight, subyoffset + areaheight, ]) subxoffset = subxoffset + row[i] / areaheight } } return coordinates } // cutArea - once we've placed some boxes into an row we then need to identify the remaining area, // this function takes the area of the boxes we've placed and calculates the location and // dimensions of the remaining space and returns a container box defined by the remaining area this.cutArea = function (area) { var newcontainer if (this.width >= this.height) { var areawidth = area / this.height var newwidth = this.width - areawidth newcontainer = new Container( this.xoffset + areawidth, this.yoffset, newwidth, this.height ) } else { var areaheight = area / this.width var newheight = this.height - areaheight newcontainer = new Container( this.xoffset, this.yoffset + areaheight, this.width, newheight ) } return newcontainer } } // normalize - the Bruls algorithm assumes we're passing in areas that nicely fit into our // container box, this method takes our raw data and normalizes the data values into // area values so that this assumption is valid. function normalize(data, area) { var normalizeddata = [] var sum = sumArray(data) var multiplier = area / sum var i for (i = 0; i < data.length; i++) { normalizeddata[i] = data[i] * multiplier } return normalizeddata } // treemapSingledimensional - simple wrapper around squarify export default function treemapSingledimensional( data, width, height, xoffset, yoffset ) { xoffset = typeof xoffset === 'undefined' ? 0 : xoffset yoffset = typeof yoffset === 'undefined' ? 0 : yoffset var rawtreemap = squarify( normalize(data, width * height), [], new Container(xoffset, yoffset, width, height), [] ) return flattenTreemap(rawtreemap) } // flattenTreemap - squarify implementation returns an array of arrays of coordinates // because we have a new array everytime we switch to building a new row // this converts it into an array of coordinates. function flattenTreemap(rawtreemap) { var flattreemap = [] var i, j for (i = 0; i < rawtreemap.length; i++) { for (j = 0; j < rawtreemap[i].length; j++) { flattreemap.push(rawtreemap[i][j]) } } return flattreemap } // squarify - as per the Bruls paper // plus coordinates stack and containers so we get // usable data out of it function squarify(data, currentrow, container, stack) { var length var nextdatapoint var newcontainer if (data.length === 0) { stack.push(container.getCoordinates(currentrow)) return } length = container.shortestEdge() nextdatapoint = data[0] if (improvesRatio(currentrow, nextdatapoint, length)) { currentrow.push(nextdatapoint) squarify(data.slice(1), currentrow, container, stack) } else { newcontainer = container.cutArea(sumArray(currentrow), stack) stack.push(container.getCoordinates(currentrow)) squarify(data, [], newcontainer, stack) } return stack } // improveRatio - implements the worse calculation and comparision as given in Bruls // (note the error in the original paper; fixed here) function improvesRatio(currentrow, nextnode, length) { var newrow if (currentrow.length === 0) { return true } newrow = currentrow.slice() newrow.push(nextnode) var currentratio = calculateRatio(currentrow, length) var newratio = calculateRatio(newrow, length) // the pseudocode in the Bruls paper has the direction of the comparison // wrong, this is the correct one. return currentratio >= newratio } // calculateRatio - calculates the maximum width to height ratio of the // boxes in this row function calculateRatio(row, length) { var min = Math.min.apply(Math, row) var max = Math.max.apply(Math, row) var sum = sumArray(row) return Math.max( (Math.pow(length, 2) * max) / Math.pow(sum, 2), Math.pow(sum, 2) / (Math.pow(length, 2) * min) ) } // isArray - checks if arr is an array function isArray(arr) { return arr && arr.constructor === Array } // sumArray - sums a single dimensional array function sumArray(arr) { var sum = 0 var i for (i = 0; i < arr.length; i++) { sum += arr[i] } return sum } ================================================ FILE: client/components/Warning/Warning.scss ================================================ @import '../../../stylesheets/colors'; @import '../../../stylesheets/variables'; .warning-bar { @include font-size-xs; background: lighten($dandelion, 5%); padding: $global-spacing * 0.5 $global-spacing; border-radius: 4px; margin-top: 2vh; color: darken(desaturate($dandelion, 35%), 35%); a { @include font-size-xxs; color: inherit; font-weight: $font-weight-bold; opacity: 0.8; padding-left: $global-spacing; text-transform: uppercase; } } ================================================ FILE: client/components/Warning/Warning.tsx ================================================ import React, { Component } from 'react' class Warning extends Component { render() { return
{this.props.children}
} } export default Warning ================================================ FILE: client/components/Warning/index.ts ================================================ export { default } from './Warning' ================================================ FILE: client/config/colors.ts ================================================ export default [ '#718af0', '#6e98e6', '#79c0f2', '#7dd6fa', '#6ed0db', '#59b3aa', '#7ebf80', '#9bc26b', '#dee675', '#fff080', '#ffd966', '#ffbf66', '#ff8a66', '#ed7872', '#db6b8f', '#bd66cc', '#cae0eb', ] as const ================================================ FILE: client/config/scanBlacklist.ts ================================================ /** * Packages that are unlikely to be useful in * determining size of frontend bundles. */ export default [ /*** Config ****/ /dotenv/, /*** CLI Tools ***/ /gulp/, /cli/, /*** Build Tools ****/ /webpack/, /react-native/, /babel/, /rollup/, /autoprefixer/, /css-nano/, /node-sass/, /next/, /create-react-app/, /react-scripts/, /-loader/, /extract-plugin/, /**** Testing ****/ /jest/, /enzyme/, /mocha/, /ava/, /nightwatch/, /**** Server libraries ****/ /koa/, /express/, /pm2/, /nodemon/, /supervisor/, /**** Common dev dependencies ****/ /prop-types/, /devtools/, ] as const ================================================ FILE: index.js ================================================ const { register } = require('esbuild-register/dist/node') const tsconfig = require('./tsconfig.server.json') register({ tsconfigRaw: tsconfig, target: tsconfig.compilerOptions.target, }) require('./index.ts') ================================================ FILE: index.ts ================================================ require('dotenv-defaults').config() import next from 'next' import exec from 'execa' import { parse } from 'url' import Koa, { Context } from 'koa' import proxy from 'koa-proxy' import serve from 'koa-static' import Router from '@koa/router' import compress from 'koa-compress' import cacheControl from 'koa-cache-control' import requestId from 'koa-requestid' import auth from 'koa-basic-auth' import bodyParser from 'koa-bodyparser' import invariant from 'ts-invariant' import Cache from './utils/cache.utils' import { parsePackageString } from './utils/common.utils' import firebaseUtils from './utils/firebase.utils' import logger from './server/Logger' import limit from './server/middlewares/rateLimit.middleware' import exportsMiddlware from './server/middlewares/exports.middleware' import exportsSizesMiddlware from './server/middlewares/exportsSizes.middleware' import resolvePackageMiddleware from './server/middlewares/results/resolvePackage.middleware' import cachedResponseMiddleware from './server/middlewares/results/cachedResponse.middleware' import buildMiddleware from './server/middlewares/results/build.middleware' import errorMiddleware from './server/middlewares/results/error.middleware' import blockBlacklistMiddleware from './server/middlewares/results/blockBlacklist.middleware' import requestLoggerMiddleware from './server/middlewares/requestLogger.middleware' import similarPackagesMiddleware from './server/middlewares/similar-packages/similarPackages.middleware' import generateImgMiddleware from './server/middlewares/generateImg.middleware' import jsonCacheMiddleware from './server/middlewares/jsonCache.middleware' import config from './server/config' function getEnv(env: Record) { invariant( env.BASIC_AUTH_PASSWORD, 'Environment variable BASIC_AUTH_PASSWORD is required' ) invariant(env.NODE_ENV, 'Environment variable NODE_ENV is required') return { basicAuthPassword: env.BASIC_AUTH_PASSWORD, port: env.PORT ? parseInt(env.PORT) : config.DEFAULT_DEV_PORT, nodeEnv: env.NODE_ENV, } } const env = getEnv(process.env) const cache = new Cache() const port = env.port const dev = env.nodeEnv !== 'production' const app = next({ dev }) const handle = app.getRequestHandler() app.prepare().then(() => { const server = new Koa() const router = new Router() server.use(requestId()) server.use(bodyParser()) server.use(requestLoggerMiddleware) server.use(cacheControl()) if (!dev) { server.use( limit({ duration: 1000 * 60 * 5, // 5 mins max: 60, whiteList: ['127.0.0.1', '::1'], }) ) } server.use(async (ctx, next) => { try { await next() } catch (err: any) { if (401 == err.status) { ctx.status = 401 ctx.set('WWW-Authenticate', 'Basic') ctx.body = 'Permission denied' } else { throw err } } }) server.use( compress({ filter: function (contentType) { return /(text|json|javascript|svg)/.test(contentType) }, threshold: 2048, gzip: { flush: require('zlib').Z_SYNC_FLUSH, }, }) ) server.use( serve('./client/assets/public', { maxage: config.CACHE.PUBLIC_ASSETS * 1000, }) ) server.use( proxy({ match: /^\/-\/search/, host: 'https://www.npmjs.com', }) ) type Key = { name: string version: string } router.get( '/api/size', jsonCacheMiddleware({ get: (key: Key) => cache.getPackageSize(key), set: (key: Key, value: string) => cache.setPackageSize(key, value), hash: (ctx: Context) => ({ name: ctx.state.resolved.name, version: ctx.state.resolved.version, }), }), errorMiddleware, resolvePackageMiddleware, blockBlacklistMiddleware, cachedResponseMiddleware, buildMiddleware ) router.get( '/api/exports', errorMiddleware, resolvePackageMiddleware, blockBlacklistMiddleware, exportsMiddlware ) router.get( '/api/exports-sizes', jsonCacheMiddleware({ get: (key: Key) => cache.getExportsSize(key), set: (key: Key, value: string) => cache.setExportsSize(key, value), hash: (ctx: Context) => ({ name: ctx.state.resolved.name, version: ctx.state.resolved.version, }), }), errorMiddleware, resolvePackageMiddleware, blockBlacklistMiddleware, cachedResponseMiddleware, exportsSizesMiddlware ) router.get('/api/recent', async ctx => { try { ctx.cacheControl = { maxAge: config.CACHE.RECENTS_API, } ctx.body = await firebaseUtils.getRecentSearches(Number(ctx.query.limit)) } catch (err: any) { console.error('in /api/recent', err) logger.error('RECENT', err, 'RECENT FAILED: failed') ctx.status = 422 ctx.body = { type: err.name, message: err.message } } }) router.get('/api/package-history', async ctx => { const { name } = parsePackageString(ctx.query.package) try { ctx.cacheControl = { maxAge: config.CACHE.PACKAGE_HISTORY_API, } ctx.body = await firebaseUtils.getPackageHistory( name, Number(ctx.query.limit) ) } catch (err: any) { console.error(err) logger.error( 'HISTORY', err, 'HISTORY FAILED: for package' + ctx.query.package ) ctx.status = 422 ctx.body = { type: err.name, message: err.message } } }) router.get('/api/similar-packages', similarPackagesMiddleware) router.get('/api/stats-image', generateImgMiddleware) router.get( '/admin/restart', auth({ name: 'bundlephobia', pass: env.basicAuthPassword }), async (ctx, next) => { try { const { stdout, stderr } = await exec.command('pm2 reload all') ctx.body = 'Server restarted' + stdout } catch (err) { console.error('Failed to restart', err) ctx.status = 500 ctx.body = err } } ) router.post('/admin/restart', async ctx => { const { name, pass } = <{ name?: string; pass?: string }>ctx.request.body if (name !== 'bundlephobia' || pass !== env.basicAuthPassword) { console.error('Failed to restart') ctx.status = 500 ctx.body = 'Failed to restart' } else { const { stdout, stderr } = await exec.command('pm2 reload all') ctx.body = 'Server restarted' + stdout console.error(stderr) } }) router.get( '/admin/clear-cache', auth({ name: 'bundlephobia', pass: env.basicAuthPassword }), async (ctx, next) => { try { const { stdout } = await exec.command( 'rm -rf /tmp/tmp-build/cache/_cacache /tmp/tmp-build/packages/' ) ctx.body = 'Cache cleared' + stdout } catch (err) { console.error('Failed to clear cache', err) ctx.status = 500 ctx.body = err } } ) router.get('/result', async ctx => { invariant(ctx.query.p, 'p parameter is required') const packageString = typeof ctx.query.p === 'string' ? ctx.query.p : ctx.query.p.join('/') ctx.redirect(`/package/${packageString.trim()}`) ctx.status = 301 }) router.get('(.*)', async ctx => { invariant(ctx.req.url, 'url is missing') const parsedUrl = parse(ctx.req.url, true) await handle(ctx.req, ctx.res, parsedUrl) ctx.respond = false }) server.use(async (ctx, next) => { ctx.res.statusCode = 200 await next() }) server.use(router.routes()) server.listen(port, () => { console.log(`> Ready on http://localhost:${port}`) }) }) ================================================ FILE: next.config.js ================================================ const path = require('path') module.exports = { pageExtensions: ['page.js', 'page.tsx'], sassOptions: { includePaths: [path.join(__dirname, 'stylesheets')], }, env: { RELEASE_DATE: new Date().toDateString(), }, webpack(config) { config.module.rules.push({ test: /\.svg$/i, issuer: /\.[jt]sx?$/, use: [ { loader: '@svgr/webpack', options: { svgoConfig: { plugins: [ { name: 'removeViewBox', active: false, }, ], }, }, }, ], }) return config }, } ================================================ FILE: nodemon.json ================================================ { "exec": "ts-node --project tsconfig.server.json ./index.ts", "ext": "js ts" } ================================================ FILE: package.json ================================================ { "name": "bundlephobia", "version": "1.0.1", "description": "Find the cost of adding new frontend dependencies", "main": "index.ts", "author": "Shubham Kanodia ", "license": "MIT", "private": true, "workspaces": [ "build-service", "cache-service", "scripts" ], "engines": { "node": ">= 24.0.0", "npm": ">= 5.4.x" }, "dependencies": { "@contentful/rich-text-react-renderer": "^15.17.0", "@contentful/rich-text-types": "^15.15.1", "@koa/router": "^12.0.1", "@next/font": "^13.5.6", "@types/koa__router": "^12.0.4", "animejs": "^3.2.2", "array-to-sentence": "^2.0.0", "axios": "^0.21.4", "balloon-css": "^0.4.0", "bfj": "^9.0.2", "big-json": "^3.2.0", "canvas": "^3.2.1", "classnames": "^2.5.1", "date-fns": "^2.30.0", "debounce": "^1.2.1", "debug": "^4.3.4", "dompurify": "^2.4.7", "dotenv": "^8.6.0", "dotenv-defaults": "^2.0.2", "esbuild": "^0.18.20", "esbuild-register": "^3.5.0", "execa": "^5.1.1", "fabric": "^6.0.0", "firebase": "^8.10.1", "firebase-admin": "^12.6.0", "flatten": "^1.0.3", "git-url-parse": "^13.1.1", "github": "^10.1.0", "got": "^9.6.0", "hot-shots": "^6.8.7", "ipchecker": "^0.0.2", "is-empty-object": "^1.1.1", "jsdom": "^24.0.0", "jsonstream": "^1.0.3", "koa": "^2.15.0", "koa-basic-auth": "^4.0.0", "koa-better-ratelimit": "^2.1.2", "koa-bodyparser": "^4.4.1", "koa-cache-control": "^2.0.0", "koa-cash": "3.0.4", "koa-compress": "^3.1.0", "koa-proxy": "^0.9.0", "koa-requestid": "^2.2.1", "koa-send": "^5.0.1", "koa-static": "^5.0.0", "lodash": "^4.17.21", "lodash.isequal": "^4.5.0", "lodash.sortby": "^4.7.0", "lru-cache": "^6.0.0", "memory-fs": "^0.5.0", "mkdir-promise": "^1.0.0", "natural": "^5.2.4", "next": "^13.5.6", "node-fetch": "^2.7.0", "normalize.css": "^8.0.1", "p-queue": "^3.2.0", "package-build-stats": "8.2.5", "pacote": "^11.3.5", "performance-now": "^2.1.0", "progress-stream": "^2.0.0", "promise-queue-plus": "^1.2.2", "promise.series": "^0.2.0", "query-string": "^7.1.3", "react": "18.2.0", "react-autocomplete": "^1.8.1", "react-contentful": "^2.0.31", "react-dom": "18.2.0", "react-dropzone": "^7.0.1", "react-flip-move": "^3.0.5", "react-sidebar": "^3.0.2", "remark": "^9.0.0", "sass": "^1.70.0", "semver": "^7.6.3", "stream-chain": "^3.3.2", "stream-json": "^1.8.0", "strip-markdown": "^4.2.0", "trending-github": "^1.2.0", "truncate": "^3.0.0", "ts-invariant": "^0.10.3", "uglifyjs-webpack-plugin": "^2.2.0", "unfetch": "^4.2.0", "webpack": "^4.47.0", "winston": "^3.11.0", "workerpool": "^6.5.1" }, "scripts": { "dev": "DEBUG='bp:*' NODE_ENV=development nodemon --watch ./server --watch ./utils ./index.ts", "build": "NODE_ENV=production next build", "prebuild": "node bin/generate-sitemap.js", "prestart": "yarn build", "start": "DEBUG='bp:*' NODE_ENV=production node ./index.js", "prod": "yarn run build && yarn start", "test": "jest", "test:watch": "jest --watch", "prettier": "prettier --write '**/*.{html,js,json,css,scss,jsx,flow,md,yml,yaml}'", "lint": "next lint", "deploy": "git branch -D gh-pages && git checkout -b gh-pages && yarn run build && yarn run export && cp -R out/* . " }, "husky": { "hooks": { "pre-commit": "next lint --fix && pretty-quick --staged" } }, "prettier": { "semi": false, "arrowParens": "avoid", "singleQuote": true }, "resolutions": { "canvas": "^3.2.1", "@types/react": "18.2.48" }, "devDependencies": { "@svgr/webpack": "^6.5.1", "@swc/core": "^1.3.107", "@swc/helpers": "^0.5.3", "@types/animejs": "^3.1.12", "@types/debounce": "^1.2.4", "@types/jsonstream": "^0.8.33", "@types/koa": "^2.14.0", "@types/koa-basic-auth": "^2.0.6", "@types/koa-bodyparser": "^4.3.12", "@types/koa-cache-control": "^2.0.5", "@types/koa-cash": "^4.1.3", "@types/koa-compress": "^4.0.6", "@types/koa-proxy": "^1.0.7", "@types/koa-static": "^4.0.4", "@types/node": "^24.0.0", "@types/progress-stream": "^2", "@types/react": "18.2.48", "@types/react-autocomplete": "1.8.10", "@types/react-dom": "18.2.18", "@types/react-sidebar": "^3.0.4", "@types/semver": "^7.7.1", "@types/stream-json": "^1", "autoprefixer": "^9.8.8", "eslint": "^8.56.0", "eslint-config-next": "^13.5.6", "eslint-config-prettier": "^8.10.0", "glob": "^7.2.3", "husky": "^4.3.8", "jest": "^24.9.0", "nodemon": "^2.0.22", "postcss": "^8.4.33", "postcss-easy-import": "^3.0.0", "postcss-loader": "^7.3.4", "prettier": "2.8.8", "pretty-quick": "^3.3.1", "sass-loader": "^10.5.2", "sitemap": "^7.1.1", "ts-node": "^10.9.2", "typescript": "^4.9.5" }, "packageManager": "yarn@4.0.2" } ================================================ FILE: pages/_app.page.tsx ================================================ import React from 'react' import Head from 'next/head' import { AppProps } from 'next/app' import '../stylesheets/index.scss' function App({ Component, pageProps }: AppProps) { return ( <> Bundlephobia ❘ cost of adding a npm package ) } export default App ================================================ FILE: pages/_document.page.tsx ================================================ import React from 'react' import Document, { DocumentContext, Head as DocumentHead, Html, Main, NextScript, } from 'next/document' const amplitudeScript = ` (function(e,t){var n=e.amplitude||{_q:[],_iq:{}};var r=t.createElement("script") ;r.type="text/javascript" ;r.integrity="sha384-girahbTbYZ9tT03PWWj0mEVgyxtZoyDF9KVZdL+R53PP5wCY0PiVUKq0jeRlMx9M" ;r.crossOrigin="anonymous";r.async=true ;r.src="https://cdn.amplitude.com/libs/amplitude-7.2.1-min.gz.js" ;r.onload=function(){if(!e.amplitude.runQueuedFunctions){ console.log("[Amplitude] Error: could not load SDK")}} ;var i=t.getElementsByTagName("script")[0];i.parentNode.insertBefore(r,i) ;function s(e,t){e.prototype[t]=function(){ this._q.push([t].concat(Array.prototype.slice.call(arguments,0)));return this}} var o=function(){this._q=[];return this} ;var a=["add","append","clearAll","prepend","set","setOnce","unset"] ;for(var c=0;c