├── .env.defaults ├── .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 ├── .idea ├── .gitignore └── prettier.xml ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .travis.yml ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-interactive-tools.cjs └── releases │ └── yarn-3.6.1.cjs ├── .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 ├── index.js ├── package.json └── yarn.lock ├── cache-service ├── .gitignore ├── cache.utils.js ├── index.js ├── middlewares │ ├── exports-size.middleware.js │ └── package-size.middleware.js ├── package.json └── yarn.lock ├── client ├── analytics.ts ├── api.ts ├── assets │ ├── dependency.svg │ ├── digital-ocean-logo.svg │ ├── empty-box.svg │ ├── git-logo.svg │ ├── github-logo.svg │ ├── heart.svg │ ├── info.svg │ ├── npm-logo.svg │ ├── plus.svg │ ├── public │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-256x256.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── manifest.json │ │ ├── mstile-150x150.png │ │ ├── npm-logo.svg │ │ ├── open-search-description.xml │ │ ├── robots.txt │ │ ├── safari-pinned-tab.svg │ │ └── sitemap.xml │ ├── side-effect.svg │ ├── site-logo.svg │ └── tree-shake.svg ├── components │ ├── 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-results │ ├── ScanResults.js │ ├── ScanResults.scss │ └── index.page.js └── scan │ ├── Scan.js │ ├── Scan.scss │ └── index.page.js ├── process.yml ├── 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 └── yarn.lock /.env.defaults: -------------------------------------------------------------------------------- 1 | BASIC_AUTH_PASSWORD=password -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "prettier"], 3 | "plugins": [], 4 | "root": true, 5 | "rules": {} 6 | } 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: pastelsky 4 | open_collective: bundlephobia 5 | ko_fi: # Replace with a single Ko-fi username 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-package-build-failure---inaccurate-sizes.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Package Build failure / inaccurate sizes 3 | about: Unexpected failures that are not explained by the FAQ's page - https://github.com/pastelsky/bundlephobia#faq 4 | title: ': fails to ' 5 | labels: '' 6 | assignees: pastelsky 7 | --- 8 | 9 | ## Package name 10 | 11 | ### Entire (stringified) error that I see in my browser console 12 | 13 | ``` 14 | 15 | ``` 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-similar-package-suggestion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Similar package suggestion 3 | about: Suggest a better alternative to a popular package 4 | title: 'Package suggestion: for ' 5 | labels: similar suggestion 6 | assignees: '' 7 | --- 8 | 9 | **Package name** 10 | 11 | **Alternative to** 12 | 13 | 14 | 15 | **Quality check** 16 | 17 | 18 | 19 | - [ ] Package has sufficient overlap in functionality to act as a replacement. 20 | - [ ] Package is actively maintained, and/or stable for use. 21 | - [ ] Package has at least 1000 weekly downloads on NPM or is relatively popular on GitHub. 22 | - [ ] This package is a better alternative to what is already suggested for this category (please explain why), or the category is new. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-feature-request---improvement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request / Improvement 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: Improvement 6 | assignees: '' 7 | --- 8 | 9 | **Please describe the feature/suggestion** 10 | 11 | **Describe the solution you'd like** 12 | 13 | **Describe any alternatives you've considered** 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/4-bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | **To Reproduce** 12 | 13 | **Expected behavior** 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [bundlephobia] 9 | pull_request: 10 | branches: [bundlephobia] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [16.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'yarn' 27 | - name: Install project dependencies 28 | run: yarn install 29 | - name: Run lint 30 | run: yarn lint 31 | - run: yarn build 32 | env: 33 | CI: true 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | .env 3 | build 4 | out 5 | now.json 6 | 7 | # package directories 8 | node_modules 9 | 10 | # Serverless directories 11 | .serverless 12 | 13 | # Logs 14 | yarn-error.log 15 | logs 16 | **/.yarn/cache 17 | **/.yarn/install-state.gz 18 | 19 | # JetBrains IDE 20 | .idea 21 | 22 | # TypeScript 23 | tsconfig.tsbuildinfo 24 | next-env.d.ts 25 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.yarnpkg.com 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.13.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | bin 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '9' 4 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableInlineBuilds: true 2 | 3 | nodeLinker: node-modules 4 | 5 | plugins: 6 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 7 | spec: '@yarnpkg/plugin-interactive-tools' 8 | 9 | yarnPath: .yarn/releases/yarn-3.6.1.cjs 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for looking to help 👋. Have a nice time contributing to bundlephobia. 2 | If you've any queries regarding setup or contributing, feel free to open an issue. 3 | I'll try my best to answer as soon as I can. 4 | 5 | Note: This repository only contains the frontend, and the request server. 6 | 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) 7 | 8 | ## Running locally 9 | 10 | ### Adding the necessary keys (Optional) 11 | 12 | Add a `.env` file to the root with Algolia credentials. The server should still run without this, but some features might be disabled. 13 | 14 | ```ini 15 | # App Id for NPM Registry 16 | ALGOLIA_APP_ID=OFCNCOG2CU 17 | 18 | # API Key 19 | ALGOLIA_API_KEY= 20 | ``` 21 | 22 | In addition, one can specify - 23 | 24 | ```ini 25 | BUILD_SERVICE_ENDPOINT= 26 | ``` 27 | 28 | In the absence of such an endpoint, packages will be built locally using the [`getPackageStats` function](https://github.com/pastelsky/package-build-stats) 29 | and 30 | 31 | ```ini 32 | CACHE_SERVICE_ENDPOINT= 33 | 34 | FIREBASE_API_KEY= 35 | FIREBASE_AUTH_DOMAIN= 36 | FIREBASE_DATABASE_URL= 37 | ``` 38 | 39 | for caching to work (optional). 40 | 41 | ### Canvas compile issues 42 | 43 | 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). 44 | 45 | ### Commands 46 | 47 | | script | description | 48 | | ---------------- | :--------------------------------- | 49 | | `yarn run dev` | Start a development server locally | 50 | | `yarn run build` | Build for production | 51 | | `yarn run prod` | Start a production server locally | 52 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Type 2 | 3 | 4 | 5 | ## Package name 6 | 7 | ### Entire error (stringified) I see in my browser console 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Shubham Kanodia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 | 7 | 8 | 9 | 10 |

11 |

12 | bundlephobia.com
13 |

14 |

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

17 | 18 |

19 | Bundlephobia's looking for contributors and co-maintainers 20 |

21 | 22 | ## Features 23 | 24 | - Works with ES6 packages 25 | - Can build css and scss packages as well (beta) 26 | - Reports historical trends 27 | - See package composition 28 | 29 | ## Badges 30 | 31 | - [badgen.net](https://badgen.net/#bundlephobia) - example size of react: ![react](https://badgen.net/bundlephobia/minzip/react) 32 | - [shields.io](https://shields.io/#/examples/size) - example size of react: ![react](https://img.shields.io/bundlephobia/minzip/react.svg) 33 | 34 | ## Built using bundlephobia 35 | 36 | - Size in browser - As seen on package searches at [yarnpkg.com](https://yarnpkg.com) 37 | - [bundlephobia-cli](https://github.com/AdrieanKhisbe/bundle-phobia-cli) - A Command Line client for bundlephobia 38 | - [importcost](https://atom.io/packages/importcost) - An Atom plugin to display size of imported packages 39 | - [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. 40 | - [npmcharts.com](https://npmcharts.com/compare/bundle-phobia-cli) - bundle size stats at top of page 41 | - [Rollpkg](https://github.com/rafgraph/rollpkg) - A build tool to create packages with Rollup and TypeScript 42 | 43 | ## Support 44 | 45 | Liked bundlephobia? Used it's API to build something cool? Let us know! 46 | 47 | We could use some 💛 and sponsorship on – 48 | 49 | 50 | 51 | 52 | 53 | ## FAQ 54 | 55 | #### 1. Why does search for package X throw `MissingDependencyError` ? 56 | 57 | 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. 58 | 59 | 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` 60 | 61 | #### 2. I see a `BuildError` for package X, but I'm not sure why. 62 | 63 | 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. 64 | 65 | ## Contributing 66 | 67 | See [Contributing](https://github.com/pastelsky/bundlephobia/blob/bundlephobia/CONTRIBUTING.md) 68 | 69 | ## Sponsors 70 | 71 | 72 | -------------------------------------------------------------------------------- /__tests__/errors-cache.test.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | 3 | const baseURL = 'http://127.0.0.1:5000/api/size?package=' 4 | 5 | describe('build api', () => { 6 | beforeEach(function () { 7 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000 8 | }) 9 | 10 | it('builds correct packages', async done => { 11 | const resultURL = baseURL + 'react@16.5.0' 12 | const result = await fetch(resultURL) 13 | const errorJSON = await result.json() 14 | 15 | expect(result.status).toBe(200) 16 | expect(result.headers.get('cache-control')).toBe('max-age=86400') 17 | 18 | expect(errorJSON).toEqual({ 19 | scoped: false, 20 | name: 'react', 21 | version: '16.5.0', 22 | description: 23 | 'React is a JavaScript library for building user interfaces.', 24 | repository: 'https://github.com/facebook/react', 25 | dependencyCount: 4, 26 | hasJSNext: false, 27 | hasJSModule: false, 28 | hasSideEffects: true, 29 | size: 5951, 30 | gzip: 2528, 31 | dependencySizes: [{ name: 'react', approximateSize: 5957 }], 32 | }) 33 | 34 | done() 35 | }) 36 | 37 | it('handles hash bang in the beginning of packages', async done => { 38 | const resultURL = baseURL + '@bundlephobia/test-build-error' 39 | const result = await fetch(resultURL) 40 | const resultJSON = await result.json() 41 | 42 | expect(result.status).toBe(200) 43 | expect(result.headers.get('cache-control')).toBe('max-age=86400') 44 | 45 | expect(resultJSON.size).toBe(183) 46 | expect(resultJSON.gzip).toBe(153) 47 | 48 | done() 49 | }) 50 | 51 | it('gives right error messages on when trying to build blocklisted packages', async done => { 52 | const resultURL = baseURL + 'polymer-cli' 53 | const result = await fetch(resultURL) 54 | const errorJSON = await result.json() 55 | 56 | expect(result.status).toBe(403) 57 | expect(result.headers.get('cache-control')).toBe('max-age=60') 58 | 59 | expect(errorJSON.error.code).toBe('BlocklistedPackageError') 60 | expect(errorJSON.error.message).toBe( 61 | 'The package you were looking for is blocklisted due to suspicious activity in the past' 62 | ) 63 | 64 | done() 65 | }) 66 | 67 | it('gives right error messages on when trying to build entry point error ', async done => { 68 | const resultURL = baseURL + '@bundlephobia/test-entry-point-error' 69 | const result = await fetch(resultURL) 70 | const errorJSON = await result.json() 71 | 72 | expect(result.status).toBe(500) 73 | expect(result.headers.get('cache-control')).toBe('max-age=3600') 74 | 75 | expect(errorJSON.error.code).toBe('EntryPointError') 76 | expect(errorJSON.error.message).toBe( 77 | "We could not guess a valid entry point for this package. Perhaps the author hasn't specified one in its package.json ?" 78 | ) 79 | 80 | done() 81 | }) 82 | 83 | it('ignores errors when trying to build packages with missing dependency errors', async done => { 84 | const resultURL = baseURL + '@bundlephobia/missing-dependency-error' 85 | const result = await fetch(resultURL) 86 | const resultJSON = await result.json() 87 | 88 | expect(result.status).toBe(200) 89 | expect(result.headers.get('cache-control')).toBe('max-age=86400') 90 | 91 | expect(resultJSON.size).toBe(243) 92 | expect(resultJSON.gzip).toBe(178) 93 | expect(resultJSON.ignoredMissingDependencies).toStrictEqual([ 94 | 'missing-package', 95 | ]) 96 | done() 97 | }) 98 | 99 | it("gives right error messages on when trying to build packages that don't exist", async done => { 100 | const resultURL = baseURL + '@bundlephobia/does-not-exist' 101 | const result = await fetch(resultURL) 102 | const errorJSON = await result.json() 103 | 104 | expect(result.status).toBe(404) 105 | expect(result.headers.get('cache-control')).toBe('max-age=60') 106 | 107 | expect(errorJSON.error.code).toBe('PackageNotFoundError') 108 | expect(errorJSON.error.message).toBe( 109 | "The package you were looking for doesn't exist." 110 | ) 111 | 112 | done() 113 | }) 114 | 115 | it("gives right error messages on when trying to build packages versions that don't exist", async done => { 116 | const resultURL = baseURL + '@bundlephobia/test-entry-point-error@459.0.0' 117 | const result = await fetch(resultURL) 118 | const errorJSON = await result.json() 119 | 120 | expect(result.status).toBe(404) 121 | expect(result.headers.get('cache-control')).toBe('max-age=60') 122 | expect(errorJSON.error.code).toBe('PackageVersionMismatchError') 123 | 124 | done() 125 | }) 126 | }) 127 | -------------------------------------------------------------------------------- /__tests__/stress-test.sh: -------------------------------------------------------------------------------- 1 | (curl localhost:5000/api/size?package=react && echo '\n') & 2 | sleep 3 3 | (curl localhost:5000/api/size?package=preact && echo '\n') & 4 | sleep 3 5 | (curl localhost:5000/api/size?package=d3 && echo '\n') & 6 | sleep 3 7 | (curl localhost:5000/api/size?package=c3 && echo '\n') & 8 | sleep 3 9 | (curl localhost:5000/api/size?package=inferno && echo '\n') & 10 | sleep 3 11 | (curl localhost:5000/api/size?package=react-router && echo '\n') & 12 | sleep 3 13 | (curl localhost:5000/api/size?package=localforage && echo '\n') & 14 | sleep 3 15 | (curl localhost:5000/api/size?package=request && echo '\n') & 16 | sleep 3 17 | (curl localhost:5000/api/size?package=lodash && echo '\n') & 18 | sleep 3 19 | (curl localhost:5000/api/size?package=moment && echo '\n') & 20 | sleep 3 21 | (curl localhost:5000/api/size?package=antd && echo '\n') & 22 | sleep 3 23 | (curl localhost:5000/api/size?package=vue && echo '\n') & 24 | sleep 3 25 | (curl localhost:5000/api/size?package=react-clipboard && echo '\n') & 26 | sleep 3 27 | (curl localhost:5000/api/size?package=react-ink && echo '\n') & 28 | sleep 3 29 | (curl localhost:5000/api/size?package=glamorous && echo '\n') & 30 | sleep 3 31 | (curl localhost:5000/api/size?package=preact-compat && echo '\n') & 32 | sleep 3 33 | (curl localhost:5000/api/size?package=inferno-compat && echo '\n') & 34 | sleep 3 35 | (curl localhost:5000/api/size?package=immutable && echo '\n') & 36 | sleep 3 37 | (curl localhost:5000/api/size?package=redux && echo '\n') & 38 | sleep 3 39 | (curl localhost:5000/api/size?package=mobx && echo '\n') & 40 | sleep 3 41 | (curl localhost:5000/api/size?package=mobx-react && echo '\n') & 42 | sleep 3 43 | (curl localhost:5000/api/size?package=styled-components && echo '\n') & 44 | wait 45 | #sleep 5 46 | #(curl localhost:5000/api/size?package=backbone && echo '\n') & 47 | #sleep 5 48 | #(curl localhost:5000/api/size?package=jquery && echo '\n') & 49 | #sleep 5 50 | #(curl localhost:5000/api/size?package=formik && echo '\n') & 51 | #sleep 5 52 | #(curl localhost:5000/api/size?package=animejs && echo '\n') & 53 | #sleep 5 54 | #(curl localhost:5000/api/size?package=three && echo '\n') & 55 | #sleep 5 56 | #(curl localhost:5000/api/size?package=babylonjs && echo '\n') & 57 | #sleep 5 58 | #(curl localhost:5000/api/size?package=emotion && echo '\n') & 59 | #sleep 5 60 | #(curl localhost:5000/api/size?package=react-treeview && echo '\n') & 61 | #sleep 5 62 | #(curl localhost:5000/api/size?package=react-bootstrap && echo '\n') & 63 | #sleep 5 64 | #(curl localhost:5000/api/size?package=vuex && echo '\n') & 65 | #sleep 5 66 | #(curl localhost:5000/api/size?package=mojs && echo '\n') & 67 | #sleep 5 68 | #(curl localhost:5000/api/size?normalize.css && echo '\n') & 69 | -------------------------------------------------------------------------------- /__tests__/utils.test.js: -------------------------------------------------------------------------------- 1 | import { parsePackageString } from '../utils/common.utils' 2 | 3 | describe('parsePackageString', () => { 4 | it('handles scoped packages correctly', () => { 5 | expect(parsePackageString('@babel/core@9.8.0')).toEqual({ 6 | scoped: true, 7 | name: '@babel/core', 8 | version: '9.8.0', 9 | }) 10 | }) 11 | 12 | it('handles scoped packages without versions correctly', () => { 13 | expect(parsePackageString('@babel/core')).toEqual({ 14 | scoped: true, 15 | name: '@babel/core', 16 | version: null, 17 | }) 18 | }) 19 | 20 | it('handles regular packages correctly', () => { 21 | expect(parsePackageString('react@15.6.1')).toEqual({ 22 | scoped: false, 23 | name: 'react', 24 | version: '15.6.1', 25 | }) 26 | }) 27 | 28 | it('handles regular packages without version correctly', () => { 29 | expect(parsePackageString('react')).toEqual({ 30 | scoped: false, 31 | name: 'react', 32 | version: null, 33 | }) 34 | }) 35 | 36 | it('handles special characters in name properly', () => { 37 | expect(parsePackageString('chart.js@5.6.0')).toEqual({ 38 | scoped: false, 39 | name: 'chart.js', 40 | version: '5.6.0', 41 | }) 42 | }) 43 | 44 | it('handles special characters in version properly', () => { 45 | expect(parsePackageString('chart.js@0.7.0-beta')).toEqual({ 46 | scoped: false, 47 | name: 'chart.js', 48 | version: '0.7.0-beta', 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /bin/generate-sitemap.js: -------------------------------------------------------------------------------- 1 | const { SitemapStream, streamToPromise } = require( 'sitemap' ) 2 | const { Readable } = require( 'stream' ) 3 | const { writeFileSync } = require('fs') 4 | const path = require('path') 5 | 6 | // Source: https://analytics.amplitude.com/bundlephobia/chart/3tbq2vm/edit/jmy3u6h 7 | const popularPackages = [ 8 | "react", 9 | "moment", 10 | "lodash", 11 | "react-dom", 12 | "axios", 13 | "@material-ui/core", 14 | "date-fns", 15 | "dayjs", 16 | "vue", 17 | "redux", 18 | "react-query", 19 | "swiper", 20 | "react-hook-form", 21 | "styled-components", 22 | "formik", 23 | "framer-motion", 24 | "yup", 25 | "angular", 26 | "antd", 27 | "preact", 28 | "react-spring", 29 | "rxjs", 30 | "jquery", 31 | "chart.js", 32 | "firebase", 33 | "glider-js", 34 | "chroma-js", 35 | "react-select", 36 | "@google/model-viewer", 37 | "bootstrap", 38 | "react-redux", 39 | "@apollo/client", 40 | "three", 41 | "luxon", 42 | "uuid", 43 | "tailwindcss", 44 | "swr", 45 | "mobx", 46 | "react-slick", 47 | "d3", 48 | "react-router-dom", 49 | "@angular/core", 50 | "recoil", 51 | "immer", 52 | "express", 53 | "classnames", 54 | "react-datepicker", 55 | "recharts", 56 | "svelte", 57 | "@chakra-ui/react", 58 | "react-final-form", 59 | "xstate", 60 | "zustand", 61 | "slick-carousel", 62 | "next", 63 | "@reduxjs/toolkit", 64 | "react-transition-group", 65 | "ramda", 66 | "gsap", 67 | "lodash-es", 68 | "query-string", 69 | "emotion", 70 | "react-beautiful-dnd", 71 | "react-router", 72 | "react-dnd", 73 | "react-bootstrap", 74 | "react-window", 75 | "@react-google-maps/api", 76 | "qs", 77 | "react-motion", 78 | "joi", 79 | "slate", 80 | "moment-timezone", 81 | "chartist", 82 | "react-i18next", 83 | "react-table", 84 | "react-virtualized", 85 | "lottie-web", 86 | "node-fetch", 87 | "@emotion/styled", 88 | "react-toastify", 89 | "xlsx", 90 | "i18next", 91 | "flickity", 92 | "@emotion/react", 93 | "@material-ui/styles", 94 | "animejs", 95 | "react-intl", 96 | "@hookstate/core", 97 | "dompurify", 98 | "@popperjs/core", 99 | "graphql", 100 | "highcharts", 101 | "js-cookie", 102 | "vuetify", 103 | "clsx", 104 | "@sentry/browser", 105 | "draft-js", 106 | "zod", 107 | "material-ui", 108 | "superstruct", 109 | "downshift", 110 | "redux-saga", 111 | "@headlessui/react", 112 | "redux-thunk", 113 | "nanoid", 114 | "libphonenumber-js", 115 | "redux-toolkit", 116 | "react-markdown", 117 | "react-day-picker", 118 | "react-use", 119 | "urql", 120 | "quill", 121 | "@material-ui/icons", 122 | "react-modal", 123 | "marked", 124 | "react-icons", 125 | "momentjs", 126 | "ajv", 127 | "jspdf", 128 | "react-dropzone", 129 | "react-popper", 130 | "jotai", 131 | "react-tooltip", 132 | "sanitize-html", 133 | "crypto-js", 134 | "react-move", 135 | "react-helmet", 136 | "apexcharts", 137 | "react-chartjs-2", 138 | "react-multi-carousel", 139 | "fuse.js", 140 | "alpinejs", 141 | "aws-sdk", 142 | "react-intersection-observer", 143 | "react-responsive-modal", 144 | "core-js", 145 | "tippy.js", 146 | "react-responsive-carousel", 147 | "react-dates", 148 | "popper.js", 149 | "graphql-request", 150 | "frappe-charts", 151 | "exceljs", 152 | "chartjs", 153 | "react-form", 154 | "vuex", 155 | "uplot", 156 | "date-fns-tz", 157 | "phin", 158 | "react-player", 159 | "keen-slider", 160 | "underscore", 161 | "final-form", 162 | "@angular/material", 163 | "react-number-format", 164 | "immutable", 165 | "xss", 166 | "chakra-ui", 167 | "lodash.debounce", 168 | "typescript", 169 | "echarts", 170 | "bulma", 171 | "jsonschema", 172 | "@xstate/react", 173 | "react-pdf", 174 | "got", 175 | "victory", 176 | "lazysizes", 177 | "validator", 178 | "lit-html", 179 | "effector", 180 | "numeral", 181 | "lit-element", 182 | "mobx-react", 183 | "polished" 184 | ] 185 | 186 | const otherPages = ['', '/scan'] 187 | 188 | const links = [ 189 | ...otherPages.map(page => ({ 190 | url: page, 191 | changefreq: 'weekly', 192 | priority: 1 193 | })), 194 | ...popularPackages.map(package => ({ 195 | url: `/package/${package}`, 196 | changefreq: 'weekly', 197 | priority: 0.7 198 | })), 199 | ] 200 | 201 | // Create a stream to write to 202 | const stream = new SitemapStream( { hostname: 'https://bundlephobia.com' } ) 203 | 204 | // Return a promise that resolves with your XML string 205 | const sitemapPromise = streamToPromise(Readable.from(links).pipe(stream)).then((data) => 206 | data.toString() 207 | ) 208 | 209 | sitemapPromise 210 | .then((sitemap) => { 211 | writeFileSync(path.join(__dirname, '..', 'client', 'assets', 'public', 'sitemap.xml'), sitemap, 'utf8') 212 | }) 213 | .catch((err) => { 214 | console.error(err) 215 | process.exit(1) 216 | }) 217 | -------------------------------------------------------------------------------- /bin/getResults.js: -------------------------------------------------------------------------------- 1 | const firebase = require('firebase') 2 | const { encodeFirebaseKey, decodeFirebaseKey } = require('../utils/index') 3 | const fs = require('fs') 4 | require('dotenv').config() 5 | 6 | const firebaseConfig = { 7 | apiKey: process.env.FIREBASE_API_KEY, 8 | authDomain: process.env.FIREBASE_AUTH_DOMAIN, 9 | databaseURL: process.env.FIREBASE_DATABASE_URL, 10 | } 11 | 12 | firebase.initializeApp(firebaseConfig) 13 | 14 | function getFirebaseStoreFromDisk() { 15 | try { 16 | return require('./data/firebase-modules.json') 17 | } catch (err) { 18 | console.log('not found on disk') 19 | return null 20 | } 21 | } 22 | 23 | async function getFirebaseStoreFromNetwork() { 24 | const modulesRef = firebase.database().ref('modules-v2') 25 | const lastEntry = Object.keys( 26 | await modulesRef 27 | .limitToLast(1) 28 | .once('value') 29 | .then(snapshot => snapshot.val()) 30 | )[0] 31 | 32 | const firstEntry = Object.keys( 33 | await modulesRef 34 | .limitToFirst(1) 35 | .once('value') 36 | .then(snapshot => snapshot.val()) 37 | )[0] 38 | 39 | let currentLastEntry = firstEntry 40 | let allData = {} 41 | let counter = 0 42 | 43 | console.log('fetching from ', firstEntry, ' to ', lastEntry) 44 | 45 | while (currentLastEntry !== lastEntry) { 46 | counter += 20000 47 | const snapshot = await firebase 48 | .database() 49 | .ref('modules-v2') 50 | .orderByKey() 51 | .startAt(currentLastEntry) 52 | .limitToFirst(20000) 53 | .once('value') 54 | .then(snapshot => snapshot.val()) 55 | 56 | const packageNames = Object.keys(snapshot) 57 | currentLastEntry = packageNames[packageNames.length - 1] 58 | console.log( 59 | 'Fetched records till ', 60 | counter, 61 | currentLastEntry, 62 | 'total of ', 63 | Object.keys(snapshot), 64 | ' packages.' 65 | ) 66 | allData = { ...allData, ...snapshot } 67 | } 68 | 69 | fs.mkdirSync(__dirname + '/data', { recursive: true }) 70 | 71 | fs.writeFileSync( 72 | __dirname + '/data/firebase-modules.json', 73 | JSON.stringify(allData, null, 2), 74 | 'utf8' 75 | ) 76 | return allData 77 | } 78 | 79 | async function getResults() { 80 | let firebaseStore = getFirebaseStoreFromDisk() 81 | console.log('loaded firebase store') 82 | return Object.keys(firebaseStore).flatMap(packageName => 83 | Object.keys(firebaseStore[packageName]).map( 84 | version => firebaseStore[packageName][version] 85 | ) 86 | ) 87 | } 88 | 89 | async function getPackages() { 90 | let firebaseStore = 91 | getFirebaseStoreFromDisk() || (await getFirebaseStoreFromNetwork()) 92 | const packages = Object.keys(firebaseStore).map( 93 | packageName => firebaseStore[packageName] 94 | ) 95 | console.log('fetched ', Object.keys(firebaseStore), ' packages ') 96 | return packages 97 | } 98 | 99 | module.exports = { getResults, getPackages } 100 | -------------------------------------------------------------------------------- /build-service/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv-defaults').config() 2 | const fastify = require('fastify')() 3 | const { 4 | getPackageStats, 5 | getAllPackageExports, 6 | getPackageExportSizes, 7 | eventQueue, 8 | } = require('package-build-stats') 9 | const Amplitude = require('@amplitude/node') 10 | 11 | if (process.env.AMPLITUDE_API_KEY) { 12 | const client = Amplitude.init(process.env.AMPLITUDE_API_KEY) 13 | 14 | eventQueue.on('*', (event, details) => { 15 | client.logEvent({ 16 | event_type: event, 17 | user_id: 'build-service', 18 | event_properties: { 19 | ...details, 20 | }, 21 | }) 22 | }) 23 | 24 | setInterval(() => { 25 | client.flush() 26 | }, 5000) 27 | } 28 | 29 | fastify.get('/size', async (req, res) => { 30 | const packageString = decodeURIComponent(req.query.p) 31 | try { 32 | const result = await getPackageStats(packageString, { 33 | installTimeout: 60000, 34 | }) 35 | return res.code(200).send(result) 36 | } catch (err) { 37 | console.log(err) 38 | const errorToSend = 'toJSON' in err ? err.toJSON() : err 39 | return res.code(500).send(errorToSend) 40 | } 41 | }) 42 | 43 | fastify.get('/exports-sizes', async (req, res) => { 44 | const packageString = decodeURIComponent(req.query.p) 45 | 46 | try { 47 | const result = await getPackageExportSizes(packageString, { 48 | installTimeout: 60000, 49 | }) 50 | return res.code(200).send(result) 51 | } catch (err) { 52 | console.log(err) 53 | const errorToSend = 'toJSON' in err ? err.toJSON() : err 54 | return res.code(500).send(errorToSend) 55 | } 56 | }) 57 | 58 | fastify.get('/exports', async (req, res) => { 59 | const packageString = decodeURIComponent(req.query.p) 60 | 61 | try { 62 | const result = await getAllPackageExports(packageString, { 63 | installTimeout: 60000, 64 | }) 65 | return res.code(200).send(result) 66 | } catch (err) { 67 | console.log(err) 68 | const errorToSend = 'toJSON' in err ? err.toJSON() : err 69 | return res.code(500).send(errorToSend) 70 | } 71 | }) 72 | 73 | fastify 74 | .listen({ port: 7002 }) 75 | .then(() => { 76 | console.log(`server listening on ${fastify.server.address().port}`) 77 | }) 78 | .catch(err => { 79 | console.error(err) 80 | process.exit(1) 81 | }) 82 | -------------------------------------------------------------------------------- /build-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build-service", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@amplitude/node": "^1.10.2", 8 | "dotenv-defaults": "^2.0.2", 9 | "fastify": "^4.21.0", 10 | "now-logs": "^0.0.7", 11 | "package-build-stats": "7.3.14" 12 | }, 13 | "engines": { 14 | "node": ">= 9.x.x", 15 | "npm": ">= 5.5.x" 16 | }, 17 | "scripts": { 18 | "start": "DEBUG=bp:* node index", 19 | "dev": "DEBUG=bp:* node index" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cache-service/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/bundlephobia/8e5ae109bfbd152b497c8b5f8299e33d23918303/cache-service/.gitignore -------------------------------------------------------------------------------- /cache-service/cache.utils.js: -------------------------------------------------------------------------------- 1 | function encodeFirebaseKey(key) { 2 | return key.replace(/[.]/g, ',').replace(/\//g, '__') 3 | } 4 | 5 | module.exports = { encodeFirebaseKey } 6 | -------------------------------------------------------------------------------- /cache-service/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv-defaults').config() 2 | const firebase = require('firebase') 3 | const fastify = require('fastify')() 4 | const { 5 | getPackageSizeMiddlware, 6 | postPackageSizeMiddlware, 7 | } = require('./middlewares/package-size.middleware') 8 | const { 9 | getExportsSizeMiddlware, 10 | postExportsSizeMiddleware, 11 | } = require('./middlewares/exports-size.middleware') 12 | 13 | const firebaseConfig = { 14 | apiKey: process.env.FIREBASE_API_KEY, 15 | authDomain: process.env.FIREBASE_AUTH_DOMAIN, 16 | databaseURL: process.env.FIREBASE_DATABASE_URL, 17 | } 18 | 19 | firebase.initializeApp(firebaseConfig) 20 | 21 | fastify.get('/package-cache', getPackageSizeMiddlware) 22 | fastify.post('/package-cache', postPackageSizeMiddlware) 23 | 24 | fastify.get('/exports-cache', getExportsSizeMiddlware) 25 | fastify.post('/exports-cache', postExportsSizeMiddleware) 26 | 27 | fastify 28 | .listen({ port: 7001 }) 29 | .then(() => { 30 | console.log(`server listening on ${fastify.server.address().port}`) 31 | }) 32 | .catch(err => { 33 | console.error(err) 34 | process.exit(1) 35 | }) 36 | -------------------------------------------------------------------------------- /cache-service/middlewares/exports-size.middleware.js: -------------------------------------------------------------------------------- 1 | require('dotenv-defaults').config() 2 | const LRU = require('lru-cache') 3 | const firebase = require('firebase') 4 | const debug = require('debug')('bp:cache') 5 | const { encodeFirebaseKey } = require('../cache.utils') 6 | 7 | const LRUCache = new LRU({ max: 1500 }) 8 | 9 | async function getPackageResult({ name, version }) { 10 | const ref = firebase 11 | .database() 12 | .ref() 13 | .child('exports') 14 | .child(encodeFirebaseKey(name)) 15 | .child(encodeFirebaseKey(version)) 16 | 17 | const snapshot = await ref.once('value') 18 | return snapshot.val() 19 | } 20 | 21 | async function setPackageResult({ name, version, result }) { 22 | const modules = firebase.database().ref().child('exports') 23 | return modules 24 | .child(encodeFirebaseKey(name)) 25 | .child(encodeFirebaseKey(version)) 26 | .set(result) 27 | } 28 | 29 | async function getExportsSizeMiddlware(req, res) { 30 | const name = decodeURIComponent(req.query.name) 31 | const version = decodeURIComponent(req.query.version) 32 | 33 | if (!name || !version) { 34 | return res.code(422).send() 35 | } 36 | debug('get exports %s@%s', name, version) 37 | const lruCacheEntry = LRUCache.get(`${name}@${version}`) 38 | 39 | if (lruCacheEntry) { 40 | debug('cache hit: memory') 41 | return res.code(200).send(lruCacheEntry) 42 | } else { 43 | const result = await getPackageResult({ name, version }) 44 | if (result) { 45 | debug('cache hit: firebase') 46 | LRUCache.set(`${name}@${version}`, result) 47 | return res.code(200).send(result) 48 | } 49 | } 50 | return res.code(404).send() 51 | } 52 | 53 | async function postExportsSizeMiddleware(req, res) { 54 | const { name, version, result } = req.body 55 | 56 | if (!name || !version || !result) return res.code(422).send() 57 | 58 | debug('set exports %O to %O', { name, version }, result) 59 | LRUCache.set(`${name}@${version}`, result) 60 | try { 61 | await setPackageResult({ name, version, result }) 62 | return res.code(201).send() 63 | } catch (err) { 64 | console.log(err) 65 | return res.code(500).send({ error: err }) 66 | } 67 | } 68 | 69 | module.exports = { getExportsSizeMiddlware, postExportsSizeMiddleware } 70 | -------------------------------------------------------------------------------- /cache-service/middlewares/package-size.middleware.js: -------------------------------------------------------------------------------- 1 | require('dotenv-defaults').config() 2 | const firebase = require('firebase') 3 | const LRU = require('lru-cache') 4 | const debug = require('debug')('bp:cache') 5 | const { encodeFirebaseKey } = require('../cache.utils') 6 | const LRUCache = new LRU({ max: 3000 }) 7 | 8 | async function getPackageResult({ name, version }) { 9 | const ref = firebase 10 | .database() 11 | .ref() 12 | .child('modules-v2') 13 | .child(encodeFirebaseKey(name)) 14 | .child(encodeFirebaseKey(version)) 15 | 16 | const snapshot = await ref.once('value') 17 | return snapshot.val() 18 | } 19 | 20 | async function setPackageResult({ name, version, result }) { 21 | const modules = firebase.database().ref().child('modules-v2') 22 | return modules 23 | .child(encodeFirebaseKey(name)) 24 | .child(encodeFirebaseKey(version)) 25 | .set(result) 26 | } 27 | 28 | async function getPackageSizeMiddlware(req, res) { 29 | const name = decodeURIComponent(req.query.name) 30 | const version = decodeURIComponent(req.query.version) 31 | 32 | if (!name || !version) { 33 | return res.code(422).send() 34 | } 35 | debug('get package %s@%s', name, version) 36 | const lruCacheEntry = LRUCache.get(`${name}@${version}`) 37 | 38 | if (lruCacheEntry) { 39 | debug('cache hit: memory') 40 | return res.code(200).send(lruCacheEntry) 41 | } else { 42 | const result = await getPackageResult({ name, version }) 43 | if (result) { 44 | debug('cache hit: firebase') 45 | LRUCache.set(`${name}@${version}`, result) 46 | return res.code(200).send(result) 47 | } 48 | } 49 | return res.code(404).send() 50 | } 51 | 52 | async function postPackageSizeMiddlware(req, res) { 53 | const { name, version, result } = req.body 54 | 55 | if (!name || !version || !result) return res.code(422).send() 56 | 57 | debug('set package %O to %O', { name, version }, result) 58 | LRUCache.set(`${name}@${version}`, result) 59 | try { 60 | await setPackageResult({ name, version, result }) 61 | return res.code(201).send() 62 | } catch (err) { 63 | console.log(err) 64 | return res.code(500).send({ error: err }) 65 | } 66 | } 67 | 68 | module.exports = { getPackageSizeMiddlware, postPackageSizeMiddlware } 69 | -------------------------------------------------------------------------------- /cache-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cache", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "debug": "^4.3.4", 8 | "dotenv": "^8.6.0", 9 | "dotenv-defaults": "^2.0.2", 10 | "fastify": "^4.21.0", 11 | "firebase": "^8.10.1", 12 | "lru-cache": "^5.1.1" 13 | }, 14 | "scripts": { 15 | "start": "DEBUG=bp* node index", 16 | "dev": "DEBUG=bp* node index" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/analytics.ts: -------------------------------------------------------------------------------- 1 | type HasPackageName = { 2 | packageName: string 3 | } 4 | 5 | type HasTimeTaken = { 6 | timeTaken: number 7 | } 8 | 9 | type HasIsDisabled = { 10 | isDisabled: boolean 11 | } 12 | 13 | type HasSuccessRatio = { 14 | successRatio: number 15 | } 16 | 17 | type HasPackageNameAndTimeTaken = HasPackageName & HasTimeTaken 18 | 19 | export default class Analytics { 20 | static pageView(pageType: string) { 21 | amplitude.getInstance().logEvent(`Viewed ${pageType}`, { 22 | path: window.location.pathname, 23 | }) 24 | } 25 | 26 | static performedSearch(packageName: string) { 27 | amplitude.getInstance().logEvent('Search Performed', { 28 | package: packageName, 29 | }) 30 | } 31 | 32 | static searchSuccess({ packageName, timeTaken }: HasPackageNameAndTimeTaken) { 33 | amplitude.getInstance().logEvent('Search Successful', { 34 | package: packageName, 35 | timeTaken, 36 | }) 37 | } 38 | 39 | static searchFailure({ packageName, timeTaken }: HasPackageNameAndTimeTaken) { 40 | amplitude.getInstance().logEvent('Search Failed', { 41 | package: packageName, 42 | timeTaken, 43 | }) 44 | } 45 | 46 | static graphBarClicked({ 47 | packageName, 48 | isDisabled, 49 | }: HasPackageName & HasIsDisabled) { 50 | amplitude.getInstance().logEvent('Bar Graph Clicked', { 51 | package: packageName, 52 | isDisabled, 53 | }) 54 | } 55 | 56 | static scanPackageJsonDropped(itemCount: number) { 57 | amplitude.getInstance().logEvent('Scan packageJSON dropped', { 58 | itemCount, 59 | }) 60 | } 61 | 62 | static performedScan() { 63 | amplitude.getInstance().logEvent('Scan Performed') 64 | } 65 | 66 | static scanParseError() { 67 | amplitude.getInstance().logEvent('Scan Parse Error') 68 | } 69 | 70 | static scanCompleted({ 71 | timeTaken, 72 | successRatio, 73 | }: HasTimeTaken & HasSuccessRatio) { 74 | amplitude.getInstance().logEvent('Scan Parse Completed', { 75 | successRatio, 76 | timeTaken, 77 | }) 78 | } 79 | 80 | static performedExportsAnalysis(packageName: string) { 81 | amplitude.getInstance().logEvent('Exports Analysis Performed', { 82 | package: packageName, 83 | }) 84 | } 85 | 86 | static exportsAnalysisSuccess({ 87 | packageName, 88 | timeTaken, 89 | }: HasPackageNameAndTimeTaken) { 90 | amplitude.getInstance().logEvent('Exports Analysis Successful', { 91 | package: packageName, 92 | timeTaken, 93 | }) 94 | } 95 | 96 | static exportsAnalysisFailure({ 97 | packageName, 98 | timeTaken, 99 | }: HasPackageNameAndTimeTaken) { 100 | amplitude.getInstance().logEvent('Exports Analysis Failed', { 101 | package: packageName, 102 | timeTaken, 103 | }) 104 | } 105 | 106 | static exportsSizesSuccess({ 107 | packageName, 108 | timeTaken, 109 | }: HasPackageNameAndTimeTaken) { 110 | amplitude.getInstance().logEvent('Exports Size Calculated', { 111 | package: packageName, 112 | timeTaken, 113 | }) 114 | } 115 | 116 | static exportsSizesFailure({ 117 | packageName, 118 | timeTaken, 119 | }: HasPackageNameAndTimeTaken) { 120 | amplitude.getInstance().logEvent('Exports Size Failed', { 121 | package: packageName, 122 | timeTaken, 123 | }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /client/api.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'unfetch' 2 | 3 | type PackageSuggestion = { 4 | searchScore: number 5 | score: { detail: { popularity: number } } 6 | } 7 | 8 | type RecentSearch = { 9 | [key: string]: { 10 | name: string 11 | version: string 12 | lastSearched: number 13 | count: number 14 | } 15 | } 16 | 17 | export default class API { 18 | static get(url: string, isInternal = true): Promise { 19 | const headers: Record = { 20 | Accept: 'application/json', 21 | } 22 | 23 | if (isInternal) { 24 | headers['X-Bundlephobia-User'] = 'bundlephobia website' 25 | } 26 | return fetch(url, { headers }).then(res => { 27 | if (!res.ok) { 28 | try { 29 | return res.json().then(err => Promise.reject(err)) 30 | } catch (e) { 31 | if (res.status === 503) { 32 | return Promise.reject({ 33 | error: { 34 | code: 'TimeoutError', 35 | message: 36 | 'This is taking unusually long. Check back in a couple of minutes?', 37 | }, 38 | }) 39 | } 40 | 41 | return Promise.reject({ 42 | error: { 43 | code: 'BuildError', 44 | message: 45 | "Oops, something went wrong and we don't have an appropriate error for this. Open an issue maybe?", 46 | }, 47 | }) 48 | } 49 | } 50 | return res.json() 51 | }) 52 | } 53 | 54 | static getInfo(packageString: string) { 55 | return API.get(`/api/size?package=${packageString}&record=true`) 56 | } 57 | 58 | static getExports(packageString: string) { 59 | return API.get(`/api/exports?package=${packageString}`) 60 | } 61 | 62 | static getExportsSizes(packageString: string) { 63 | return API.get(`/api/exports-sizes?package=${packageString}`) 64 | } 65 | 66 | static getHistory(packageString: string, limit: number) { 67 | return API.get( 68 | `/api/package-history?package=${packageString}&limit=${limit}` 69 | ) 70 | } 71 | 72 | static getRecentSearches(limit: number) { 73 | return API.get(`/api/recent?limit=${limit}`) 74 | } 75 | 76 | static getSimilar(packageName: string) { 77 | return API.get(`/api/similar-packages?package=${packageName}`) 78 | } 79 | 80 | static getSuggestions(query: string) { 81 | const suggestionSort = ( 82 | packageA: PackageSuggestion, 83 | packageB: PackageSuggestion 84 | ) => { 85 | // Rank closely matching packages followed 86 | // by most popular ones 87 | if ( 88 | Math.abs( 89 | Math.log(packageB.searchScore) - Math.log(packageA.searchScore) 90 | ) > 1 91 | ) { 92 | return packageB.searchScore - packageA.searchScore 93 | } else { 94 | return ( 95 | packageB.score.detail.popularity - packageA.score.detail.popularity 96 | ) 97 | } 98 | } 99 | 100 | return API.get( 101 | `https://api.npms.io/v2/search/suggestions?q=${query}`, 102 | false 103 | ).then(result => result.sort(suggestionSort)) 104 | 105 | //backup when npms.io is down 106 | 107 | //return API.get(`/-/search?text=${query}`) 108 | // .then(result => result.objects 109 | // .sort(suggestionSort) 110 | // .map(suggestion => { 111 | // const name = suggestion.package.name 112 | // const hasMatch = name.includes(query) 113 | // const startIndex = name.indexOf(query) 114 | // const endIndex = startIndex + query.length 115 | // let highlight 116 | // 117 | // if (hasMatch) { 118 | // highlight = 119 | // name.substring(0, startIndex) + 120 | // '' + name.substring(startIndex, endIndex) + '' + 121 | // name.substring(endIndex) 122 | // } else { 123 | // highlight = name 124 | // } 125 | // 126 | // return { 127 | // ...suggestion, 128 | // highlight, 129 | // } 130 | // }), 131 | // ) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /client/assets/dependency.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dependencies 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /client/assets/digital-ocean-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | -------------------------------------------------------------------------------- /client/assets/empty-box.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /client/assets/git-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/assets/github-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/assets/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /client/assets/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/assets/npm-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/assets/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /client/assets/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/bundlephobia/8e5ae109bfbd152b497c8b5f8299e33d23918303/client/assets/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /client/assets/public/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/bundlephobia/8e5ae109bfbd152b497c8b5f8299e33d23918303/client/assets/public/android-chrome-256x256.png -------------------------------------------------------------------------------- /client/assets/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/bundlephobia/8e5ae109bfbd152b497c8b5f8299e33d23918303/client/assets/public/apple-touch-icon.png -------------------------------------------------------------------------------- /client/assets/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2b5797 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/assets/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/bundlephobia/8e5ae109bfbd152b497c8b5f8299e33d23918303/client/assets/public/favicon-16x16.png -------------------------------------------------------------------------------- /client/assets/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/bundlephobia/8e5ae109bfbd152b497c8b5f8299e33d23918303/client/assets/public/favicon-32x32.png -------------------------------------------------------------------------------- /client/assets/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/bundlephobia/8e5ae109bfbd152b497c8b5f8299e33d23918303/client/assets/public/favicon.ico -------------------------------------------------------------------------------- /client/assets/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bundlephobia", 3 | "icons": [ 4 | { 5 | "src": "/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | }, 9 | { 10 | "src": "/android-chrome-256x256.png", 11 | "sizes": "256x256", 12 | "type": "image/png" 13 | } 14 | ], 15 | "theme_color": "#ffffff", 16 | "background_color": "#ffffff", 17 | "display": "standalone", 18 | "orientation": "portrait" 19 | } 20 | -------------------------------------------------------------------------------- /client/assets/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/bundlephobia/8e5ae109bfbd152b497c8b5f8299e33d23918303/client/assets/public/mstile-150x150.png -------------------------------------------------------------------------------- /client/assets/public/npm-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | npm 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/assets/public/open-search-description.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | bundlephobia 4 | Search npm packages on bundlephobia 5 | UTF-8 6 | UTF-8 7 | en-us 8 | https://bundlephobia.com/favicon-32x32.png 9 | bundlephobia 10 | 12 | https://bundlephobia.com 13 | 14 | -------------------------------------------------------------------------------- /client/assets/public/robots.txt: -------------------------------------------------------------------------------- 1 | # * 2 | User-agent: * 3 | Allow: / 4 | Disallow: /scan-results 5 | 6 | # Host 7 | Host: https://bundlephobia.com 8 | 9 | # Sitemaps 10 | Sitemap: https://bundlephobia.com/sitemap.xml 11 | -------------------------------------------------------------------------------- /client/assets/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/assets/side-effect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/assets/tree-shake.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /client/components/AutocompleteInput/AutocompleteInput.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | @import '../../../stylesheets/colors'; 4 | @import '../../../stylesheets/variables'; 5 | 6 | .autocomplete-input__container { 7 | position: relative; 8 | width: 100%; 9 | } 10 | 11 | .autocomplete-input { 12 | width: 40vw; 13 | border: none; 14 | border-radius: 50px; 15 | color: rgba(0, 0, 0, 0); 16 | } 17 | 18 | .autocomplete-input, 19 | .autocomplete-input__dummy-input { 20 | @include font-size-lg; 21 | padding: $global-spacing * 1.5 (25px + $global-spacing * 2) $global-spacing * 22 | 1.5 $global-spacing * 3; 23 | font-family: $font-family-code; 24 | font-weight: $font-weight-thin; 25 | width: 100%; 26 | box-sizing: border-box; 27 | letter-spacing: -0.7px; 28 | margin: 0; 29 | 30 | @media screen and (max-width: 40em) { 31 | padding: $global-spacing (20px + $global-spacing) $global-spacing 32 | $global-spacing; 33 | } 34 | } 35 | 36 | .autocomplete-input__dummy-input { 37 | position: absolute; 38 | left: 0; 39 | right: 0; 40 | top: 0; 41 | bottom: 0; 42 | pointer-events: none; 43 | display: flex; 44 | align-items: center; 45 | white-space: nowrap; 46 | overflow: hidden; 47 | } 48 | 49 | .dummy-input__package-name { 50 | color: #1d1d1d; 51 | font-size: inherit; 52 | font-weight: inherit; 53 | margin: 0; 54 | } 55 | 56 | .dummy-input__package-version { 57 | color: #636363; 58 | } 59 | 60 | .dummy-input__at-separator { 61 | color: $pastel-green; 62 | } 63 | 64 | .autocomplete-input__suggestions-menu { 65 | border: 1px solid $autocomplete-border-color; 66 | border-top: 0; 67 | background: rgba(255, 255, 255, 0.96); 68 | font-size: 90%; 69 | position: absolute; 70 | overflow: auto; 71 | z-index: 10; 72 | max-height: 35vh; 73 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); 74 | border-bottom-left-radius: 10px; 75 | border-bottom-right-radius: 10px; 76 | animation: unroll 0.2s cubic-bezier(0.305, 0.42, 0.205, 1.2); 77 | // align borders, negating nesting due to border on parent 78 | left: -1px; 79 | // hide rounded borders of the input 80 | margin-top: -5px; 81 | width: 100%; 82 | width: calc(100% + 2px); 83 | 84 | &:empty { 85 | border: 0; 86 | } 87 | } 88 | 89 | @keyframes unroll { 90 | from { 91 | opacity: 0; 92 | transform: translateY(-5px); 93 | } 94 | 95 | to { 96 | opacity: 1; 97 | transform: translateY(0px); 98 | } 99 | } 100 | 101 | .autocomplete-input__suggestion { 102 | padding: 10px 32px; 103 | color: #333; 104 | font-size: 15px; 105 | cursor: pointer; 106 | font-family: 'Source Code Pro', monospace; 107 | font-weight: $font-weight-light; 108 | letter-spacing: -0.5px; 109 | 110 | &:not(:last-of-type) { 111 | border-bottom: 1px solid #f5f5f5; 112 | } 113 | 114 | @media screen and (max-width: 40em) { 115 | padding: math.div($global-spacing, 1.5) $global-spacing * 1.5; 116 | } 117 | 118 | em { 119 | font-weight: $font-weight-bold; 120 | font-style: normal; 121 | color: #444; 122 | } 123 | } 124 | 125 | .autocomplete-input__suggestion--highlight { 126 | background: rgb(212, 243, 255); 127 | } 128 | 129 | .autocomplete-input__suggestion-description { 130 | @include font-size-xs; 131 | width: 100%; 132 | min-width: 260px; 133 | overflow: hidden; 134 | white-space: nowrap; 135 | text-overflow: ellipsis; 136 | font-family: $font-family-body; 137 | font-weight: $font-weight-thin; 138 | color: #666; 139 | padding-top: 5px; 140 | letter-spacing: 0; 141 | 142 | @media screen and (max-width: 40em) { 143 | font-weight: $font-weight-light; 144 | } 145 | } 146 | 147 | .autocomplete-input__form { 148 | display: flex; 149 | align-items: baseline; 150 | position: relative; 151 | } 152 | 153 | .autocomplete-input__search-icon { 154 | position: absolute; 155 | right: $global-spacing * 2.5; 156 | z-index: 1; 157 | cursor: pointer; 158 | top: 0; 159 | bottom: 0; 160 | margin: auto; 161 | width: 25px; 162 | height: 25px; 163 | border: none; 164 | background: none; 165 | padding: 0; 166 | 167 | @media screen and (max-width: 40em) { 168 | width: 16px; 169 | height: 16px; 170 | right: $global-spacing * 1.5; 171 | } 172 | 173 | svg { 174 | width: 100%; 175 | height: 100%; 176 | 177 | path { 178 | transition: all 0.2s; 179 | fill: #666; 180 | } 181 | } 182 | 183 | &:hover { 184 | path { 185 | fill: $pastel-green; 186 | stroke: $pastel-green; 187 | stroke-width: 4px; 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /client/components/AutocompleteInput/AutocompleteInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cx from 'classnames' 3 | import AutoComplete from 'react-autocomplete' 4 | 5 | import SearchIcon from '../Icons/SearchIcon' 6 | import { parsePackageString } from '../../../utils/common.utils' 7 | import { useAutocompleteInput } from './hooks/useAutocompleteInput' 8 | import { SuggestionItem } from './components/SuggestionItem' 9 | import { useFontSize } from './hooks/useFontSize' 10 | 11 | type AutocompleteInputProps = { 12 | initialValue?: string 13 | renderAsH1?: boolean 14 | className?: string 15 | containerClass?: string 16 | autoFocus?: boolean 17 | onSearchSubmit: (value: string) => void 18 | } 19 | 20 | export const AutocompleteInput = ({ 21 | initialValue = '', 22 | renderAsH1 = false, 23 | className, 24 | containerClass, 25 | autoFocus, 26 | onSearchSubmit, 27 | }: AutocompleteInputProps) => { 28 | const searchInput = React.useRef(null) 29 | const { 30 | value, 31 | isMenuVisible, 32 | suggestions, 33 | handleSubmit, 34 | handleInputChange, 35 | setIsMenuVisible, 36 | setSuggestions, 37 | } = useAutocompleteInput({ initialValue, onSubmit: onSearchSubmit }) 38 | const { searchFontSize } = useFontSize({ value }) 39 | 40 | const { name, version } = React.useMemo( 41 | () => parsePackageString(value), 42 | [value] 43 | ) 44 | 45 | return ( 46 |
50 |
56 | item.package.name} 58 | inputProps={{ 59 | placeholder: 'find package', 60 | className: 'autocomplete-input', 61 | autoCorrect: 'off', 62 | autoFocus: autoFocus, 63 | autoCapitalize: 'off', 64 | spellCheck: false, 65 | style: { fontSize: searchFontSize! }, 66 | }} 67 | onMenuVisibilityChange={isOpen => setIsMenuVisible(isOpen)} 68 | onChange={handleInputChange} 69 | ref={searchInput} 70 | value={value} 71 | items={suggestions} 72 | onSelect={(value, item) => { 73 | setSuggestions([item]) 74 | onSearchSubmit(value) 75 | }} 76 | renderMenu={(items, value, inbuiltStyles) => { 77 | return ( 78 |
82 | {items} 83 |
84 | ) 85 | }} 86 | wrapperStyle={{ 87 | display: 'inline-block', 88 | width: '100%', 89 | position: 'relative', 90 | }} 91 | renderItem={(item, isHighlighted) => ( 92 |
93 | 94 |
95 | )} 96 | /> 97 |
101 | 105 | {name} 106 | 107 | {version !== null && ( 108 | <> 109 | @ 110 | {version} 111 | 112 | )} 113 |
114 |
115 | 118 |
119 | ) 120 | } 121 | 122 | type PackageNameElementProps = React.HTMLAttributes & { 123 | isHeading?: boolean 124 | } 125 | 126 | export function PackageNameElement({ 127 | isHeading, 128 | ...props 129 | }: PackageNameElementProps) { 130 | return isHeading ?

: 131 | } 132 | -------------------------------------------------------------------------------- /client/components/AutocompleteInput/components/SuggestionItem.tsx: -------------------------------------------------------------------------------- 1 | import cx from 'classnames' 2 | 3 | interface SuggestionItemProps { 4 | item: { 5 | highlight: string | null 6 | package: { 7 | name: string 8 | description: string 9 | } 10 | } 11 | isHighlighted: boolean 12 | } 13 | 14 | export function SuggestionItem({ item, isHighlighted }: SuggestionItemProps) { 15 | return ( 16 |
22 | {item.highlight != null ? ( 23 |
24 | ) : ( 25 |
{item.package.name}
26 | )} 27 | 28 |
29 | {item.package.description} 30 |
31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /client/components/AutocompleteInput/hooks/useAutocompleteInput.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import debounce from 'debounce' 3 | 4 | import { parsePackageString } from '../../../../utils/common.utils' 5 | import API from '../../../api' 6 | 7 | interface UseAutocompleteInputArgs { 8 | initialValue: string 9 | onSubmit: (value: string) => void 10 | } 11 | 12 | export function useAutocompleteInput({ 13 | initialValue, 14 | onSubmit, 15 | }: UseAutocompleteInputArgs) { 16 | const [value, setValue] = React.useState(initialValue) 17 | const [suggestions, setSuggestions] = React.useState([]) 18 | const [isMenuVisible, setIsMenuVisible] = React.useState(false) 19 | 20 | const getSuggestions = React.useMemo( 21 | () => 22 | debounce((value: string) => { 23 | API.getSuggestions(value).then(result => { 24 | setSuggestions(result) 25 | }) 26 | }, 150), 27 | [] 28 | ) 29 | 30 | const handleSubmit = (e: React.FormEvent) => { 31 | e.preventDefault() 32 | onSubmit(value) 33 | } 34 | 35 | const handleInputChange = ( 36 | e: React.ChangeEvent, 37 | value: string 38 | ) => { 39 | setValue(e.target.value) 40 | const trimmedValue = e.target.value.trim() 41 | const { name } = parsePackageString(trimmedValue) 42 | 43 | if (trimmedValue.length > 1) { 44 | getSuggestions(name) 45 | } 46 | 47 | if (!trimmedValue) { 48 | setSuggestions([]) 49 | } 50 | } 51 | 52 | return { 53 | value, 54 | suggestions, 55 | isMenuVisible, 56 | handleSubmit, 57 | handleInputChange, 58 | setIsMenuVisible, 59 | setSuggestions, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /client/components/AutocompleteInput/hooks/useFontSize.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export function useFontSize({ value }: { value: string }) { 4 | const searchFontSize = React.useMemo(() => { 5 | const baseFontSize = 6 | typeof window !== 'undefined' && window.innerWidth < 640 ? 22 : 35 7 | const maxFullSizeChars = 8 | typeof window !== 'undefined' && window.innerWidth < 640 ? 15 : 20 9 | const searchFontSize = 10 | value.length < maxFullSizeChars 11 | ? null 12 | : `${baseFontSize - (value.length - maxFullSizeChars) * 0.8}px` 13 | 14 | return searchFontSize 15 | }, [value]) 16 | 17 | return { searchFontSize } 18 | } 19 | -------------------------------------------------------------------------------- /client/components/AutocompleteInput/index.ts: -------------------------------------------------------------------------------- 1 | export { AutocompleteInput } from './AutocompleteInput' 2 | -------------------------------------------------------------------------------- /client/components/AutocompleteInputBox/AutocompleteInputBox.scss: -------------------------------------------------------------------------------- 1 | @import '../../../stylesheets/colors'; 2 | @import '../../../stylesheets/variables'; 3 | 4 | .autocomplete-input-box { 5 | border: 1px solid $autocomplete-border-color; 6 | border-radius: 10px; 7 | background: transparent; 8 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); 9 | max-width: 700px; 10 | min-width: 550px; 11 | 12 | @media screen and (max-width: 48em) { 13 | width: 85vw; 14 | max-width: 550px; 15 | min-width: auto; 16 | } 17 | 18 | @media screen and (max-width: 40em) { 19 | width: 85vw; 20 | min-width: auto; 21 | } 22 | } 23 | 24 | .autocomplete-input-box__footer { 25 | position: relative; 26 | 27 | &::after { 28 | content: ''; 29 | position: absolute; 30 | width: 80%; 31 | margin: auto; 32 | height: 1px; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /client/components/AutocompleteInputBox/AutocompleteInputBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import cx from 'classnames' 3 | 4 | import { WithClassName } from '../../../types' 5 | 6 | type AutocompleteInputBoxProps = React.PropsWithChildren & 7 | WithClassName & { 8 | footer?: React.ReactNode 9 | } 10 | 11 | class AutocompleteInputBox extends Component { 12 | render() { 13 | const { children, footer, className } = this.props 14 | return ( 15 |
16 | {children} 17 | {footer && ( 18 |
{footer}
19 | )} 20 |
21 | ) 22 | } 23 | } 24 | 25 | export default AutocompleteInputBox 26 | -------------------------------------------------------------------------------- /client/components/AutocompleteInputBox/index.ts: -------------------------------------------------------------------------------- 1 | import AutocompleteInputBox from './AutocompleteInputBox' 2 | 3 | export default AutocompleteInputBox 4 | -------------------------------------------------------------------------------- /client/components/BarGraph/BarGraph.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | @import '../../../stylesheets/colors'; 4 | @import '../../../stylesheets/variables'; 5 | 6 | $bar-width: 1.6vw; 7 | $min-bar-width: 20px; 8 | $bar-grow-duration: 0.4s; 9 | 10 | .bar-graph-container { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | width: 100%; 15 | height: 48vh; 16 | } 17 | 18 | .bar-graph { 19 | height: 40vh; 20 | padding-bottom: 6vh; 21 | display: flex; 22 | margin: 0; 23 | justify-content: center; 24 | } 25 | 26 | .bar-graph__bar-group { 27 | position: relative; 28 | height: 100%; 29 | margin: 0 3px; 30 | display: flex; 31 | width: $bar-width; 32 | min-width: $min-bar-width; 33 | justify-content: flex-end; 34 | flex-direction: column; 35 | animation: grow $bar-grow-duration cubic-bezier(0.305, 0.42, 0.205, 1.2); 36 | transform-origin: 100% 100%; 37 | } 38 | 39 | .bar-graph__bar-symbols { 40 | display: flex; 41 | flex-direction: column; 42 | margin-top: -500%; // don't know why this works :/ 43 | } 44 | 45 | .bar-graph__bar-symbol { 46 | text-align: center; 47 | 48 | svg { 49 | height: $global-spacing * 1.8; 50 | width: auto; 51 | } 52 | 53 | & + & { 54 | margin-top: math.div($global-spacing, 3); 55 | } 56 | } 57 | 58 | .bar-graph__bar, 59 | .bar-graph__bar2, 60 | .bar-graph__bar[data-balloon], 61 | .bar-graph__bar2[data-balloon] { 62 | width: 100%; 63 | left: 0; 64 | bottom: 0; 65 | transition: background 0.2s; 66 | cursor: pointer; 67 | } 68 | 69 | .bar-graph__bar, 70 | .bar-graph__bar[data-balloon] { 71 | background: $maya-blue; 72 | border-radius: 5px 5px 0 0; 73 | 74 | .bar-graph__bar-group:not(.bar-graph__bar-group--disabled):hover & { 75 | background: darken($maya-blue, 5%); 76 | } 77 | 78 | .bar-graph__bar-group--disabled & { 79 | background: lighten($raven, 45%); 80 | border-radius: 5px; 81 | 82 | &:hover { 83 | background: lighten($raven, 35%); 84 | } 85 | } 86 | } 87 | 88 | .bar-graph__bar2 { 89 | background: $cornflower-blue; 90 | z-index: 1; 91 | pointer-events: none; 92 | border-radius: 0 0 5px 5px; 93 | 94 | .bar-graph__bar-group:hover & { 95 | background: darken($cornflower-blue, 5%); 96 | } 97 | } 98 | 99 | .bar-graph__bar-version, 100 | .bar-graph__bar-symbols, 101 | .bar-graph__legend { 102 | animation: fade-in 0.5s $bar-grow-duration * 0.9 both 103 | cubic-bezier(0.305, 0.42, 0.205, 1.2); 104 | } 105 | 106 | .bar-graph__legend { 107 | @include font-size-xs; 108 | padding-top: $global-spacing; 109 | display: flex; 110 | text-transform: uppercase; 111 | justify-content: center; 112 | color: lighten($raven, 20%); 113 | } 114 | 115 | .bar-graph__legend__colorbox { 116 | width: $global-spacing * 1.5; 117 | height: $global-spacing * 1.5; 118 | margin-right: $global-spacing; 119 | border-radius: 3px; 120 | 121 | .bar-graph__legend__bar1 & { 122 | background: $maya-blue; 123 | } 124 | 125 | .bar-graph__legend__bar2 & { 126 | background: $cornflower-blue; 127 | } 128 | } 129 | 130 | .bar-graph__legend__bar1, 131 | .bar-graph__legend__bar2 { 132 | display: flex; 133 | align-items: center; 134 | } 135 | 136 | .bar-graph__legend__bar1 { 137 | margin-right: $global-spacing * 4; 138 | } 139 | 140 | @keyframes grow { 141 | from { 142 | transform: scaleY(0); 143 | } 144 | 145 | to { 146 | transform: scaleY(1); 147 | } 148 | } 149 | 150 | @keyframes fade-in { 151 | from { 152 | opacity: 0; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /client/components/BarGraph/index.ts: -------------------------------------------------------------------------------- 1 | import BarGraph from './BarGraph' 2 | 3 | export default BarGraph 4 | -------------------------------------------------------------------------------- /client/components/BarVersion/BarVersion.scss: -------------------------------------------------------------------------------- 1 | @import '../../../stylesheets/colors'; 2 | @import '../../../stylesheets/variables'; 3 | 4 | .bar-graph__bar-version { 5 | @include font-size-xs; 6 | z-index: 33; 7 | font-weight: $font-weight-light; 8 | transform: rotate(-90deg) translateX(#{-$global-spacing * 1.5}); 9 | font-variant-numeric: tabular-nums; 10 | color: lighten($raven, 15%); 11 | transition: opacity 0.2s, color 0.2s; 12 | font-family: $font-family-code; 13 | letter-spacing: -1px; 14 | line-height: 1; 15 | cursor: pointer; 16 | height: $bar-width; 17 | text-align: end; 18 | display: flex; 19 | justify-content: flex-end; 20 | align-items: center; 21 | 22 | .bar-graph-container:hover & { 23 | color: $raven; 24 | } 25 | 26 | .bar-graph__bar-group:hover & { 27 | color: darken($raven, 20%); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/components/BarVersion/BarVersion.tsx: -------------------------------------------------------------------------------- 1 | import truncate from 'truncate' 2 | 3 | interface Props { 4 | version: string 5 | } 6 | 7 | export function BarVersion({ version }: Props) { 8 | return ( 9 |
10 |
11 | {truncate(version, 7)} 12 |
13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /client/components/BlogLayout/BlogLayout.scss: -------------------------------------------------------------------------------- 1 | @import '../../../stylesheets/colors'; 2 | @import '../../../stylesheets/variables'; 3 | 4 | .page-container.blog { 5 | .page-content { 6 | justify-content: normal; 7 | } 8 | } 9 | 10 | .blog-layout__container { 11 | max-width: 80ch; 12 | padding: 0 2rem; 13 | width: 100%; 14 | 15 | h1 { 16 | margin-bottom: 0; 17 | } 18 | 19 | img, 20 | iframe { 21 | max-width: 100%; 22 | object-fit: contain; 23 | } 24 | } 25 | 26 | .blog-post__preview-read-more { 27 | @include font-size-xs(); 28 | letter-spacing: 1px; 29 | color: $gulf-blue; 30 | font-weight: $font-weight-bold; 31 | text-transform: uppercase; 32 | 33 | &:hover { 34 | color: lighten($gulf-blue, 20%); 35 | } 36 | } 37 | 38 | .blog-post__preview { 39 | h2 { 40 | font-weight: $dark-gulf-blue; 41 | color: #5c5c66; 42 | margin: 0; 43 | 44 | &:hover { 45 | color: lighten($gulf-blue, 10%); 46 | } 47 | } 48 | 49 | & + & { 50 | margin-top: 4rem; 51 | } 52 | } 53 | 54 | .blog-post__preview-content { 55 | color: darken($dark-raven, 10%); 56 | line-height: 1.6; 57 | @include font-size-reg(); 58 | 59 | a { 60 | color: darken($maya-blue, 20%); 61 | border-bottom: 1px solid $cornflower-blue; 62 | transition: all 0.2s; 63 | 64 | &:hover { 65 | color: darken($maya-blue, 40%); 66 | border-bottom: 1px dashed $cornflower-blue; 67 | } 68 | } 69 | } 70 | 71 | .blog-post__preview-date { 72 | @include font-size-sm; 73 | margin: 0.7rem 0 0; 74 | color: $raven; 75 | font-weight: $font-weight-light; 76 | } 77 | -------------------------------------------------------------------------------- /client/components/BlogLayout/BlogLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { WithClassName } from '../../../types' 4 | import ResultLayout from '../ResultLayout' 5 | 6 | type BlogLayoutProps = React.PropsWithChildren & WithClassName 7 | 8 | const BlogLayout = ({ className, children }: BlogLayoutProps) => ( 9 | 10 |
{children}
11 |
12 | ) 13 | 14 | export default BlogLayout 15 | -------------------------------------------------------------------------------- /client/components/BlogLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './BlogLayout' 2 | -------------------------------------------------------------------------------- /client/components/BuildProgressIndicator/BuildProgressIndicator.scss: -------------------------------------------------------------------------------- 1 | @import '../../../stylesheets/colors'; 2 | @import '../../../stylesheets/variables'; 3 | 4 | .build-progress-indicator { 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | flex-direction: column; 9 | flex-grow: 1; 10 | padding: 0 $global-spacing * 2; 11 | text-align: center; 12 | } 13 | 14 | .build-progress-indicator__text { 15 | font-size: 0.7rem; 16 | margin-top: $global-spacing * 3; 17 | color: lighten($raven, 15%); 18 | text-transform: uppercase; 19 | font-weight: $font-weight-bold; 20 | letter-spacing: 2px; 21 | line-height: 1.5; 22 | } 23 | -------------------------------------------------------------------------------- /client/components/BuildProgressIndicator/BuildProgressIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import ProgressHex from '../ProgressHex' 4 | 5 | const OptimisticLoadTimeout = 700 6 | 7 | type BuildProgressIndicatorProps = { 8 | isDone: boolean 9 | onDone: () => void 10 | } 11 | 12 | type BuildProgressIndicatorState = { 13 | started: boolean 14 | progressText?: string 15 | } 16 | 17 | const order = ['resolving', 'building', 'minifying', 'calculating'] as const 18 | 19 | export default class BuildProgressIndicator extends Component< 20 | BuildProgressIndicatorProps, 21 | BuildProgressIndicatorState 22 | > { 23 | stage: number 24 | timeoutId?: ReturnType 25 | 26 | constructor(props: BuildProgressIndicatorProps) { 27 | super(props) 28 | this.stage = 0 29 | this.state = { 30 | started: false, 31 | } 32 | } 33 | 34 | componentDidMount() { 35 | setTimeout(() => { 36 | if (!this.props.isDone) { 37 | this.setState({ started: true }) 38 | this.setMessage() 39 | } 40 | }, OptimisticLoadTimeout) 41 | } 42 | 43 | componentWillReceiveProps(nextProps: BuildProgressIndicatorProps) { 44 | if (nextProps.isDone) { 45 | this.stage = 3 46 | this.props.onDone() 47 | } 48 | } 49 | 50 | shouldComponentUpdate( 51 | props: BuildProgressIndicatorProps, 52 | nextState: BuildProgressIndicatorState 53 | ) { 54 | return this.state.progressText !== nextState.progressText 55 | } 56 | 57 | componentWillUnmount() { 58 | clearTimeout(this.timeoutId) 59 | } 60 | 61 | getProgressText = (stage: typeof order[number]) => { 62 | const progressText = { 63 | resolving: 'Resolving version and dependencies', 64 | building: 'Bundling package', 65 | minifying: 'Minifying, GZipping', 66 | calculating: 'Calculating file sizes', 67 | } 68 | return progressText[stage] 69 | } 70 | 71 | setMessage = (stage = 0) => { 72 | const timings = { 73 | resolving: 3 + Math.random() * 2, 74 | building: 5 + Math.random() * 3, 75 | minifying: 3 + Math.random() * 2, 76 | calculating: 20, 77 | } 78 | 79 | if (this.stage === order.length) { 80 | //this.props.onDone() 81 | return 82 | } 83 | 84 | this.setState({ 85 | progressText: this.getProgressText(order[this.stage]), 86 | }) 87 | 88 | this.timeoutId = setTimeout(() => { 89 | if (this.stage < order.length) { 90 | this.stage += 1 91 | } 92 | 93 | this.setMessage(this.stage) 94 | }, timings[order[stage]] * 1000) 95 | } 96 | 97 | render() { 98 | const { progressText, started } = this.state 99 | if (!started) { 100 | return null 101 | } 102 | 103 | return ( 104 |
105 | 106 |

{progressText}

107 |
108 | ) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /client/components/BuildProgressIndicator/index.ts: -------------------------------------------------------------------------------- 1 | import BuildProgressIndicator from './BuildProgressIndicator' 2 | 3 | export default BuildProgressIndicator 4 | -------------------------------------------------------------------------------- /client/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Sidebar from 'react-sidebar' 3 | import Link from 'next/link' 4 | 5 | import { WithClassName } from '../../../types' 6 | import GithubLogo from '../../assets/github-logo.svg' 7 | 8 | type HeaderProps = WithClassName 9 | 10 | type HeaderState = { 11 | sidebarDocked: boolean 12 | sidebarOpen: boolean 13 | } 14 | 15 | export default class Header extends Component { 16 | mql!: MediaQueryList 17 | 18 | constructor(props: HeaderProps) { 19 | super(props) 20 | this.state = { 21 | sidebarDocked: false, 22 | sidebarOpen: false, 23 | } 24 | } 25 | 26 | componentDidMount() { 27 | this.mql = window.matchMedia(`(min-width: 800px)`) 28 | this.setState({ sidebarDocked: this.mql.matches }) 29 | this.mql.addListener(this.mediaQueryChanged) 30 | } 31 | 32 | componentWillUnmount() { 33 | this.mql.removeListener(this.mediaQueryChanged) 34 | } 35 | 36 | onSetSidebarOpen(open: boolean) { 37 | this.setState({ sidebarOpen: open }) 38 | } 39 | 40 | mediaQueryChanged() { 41 | this.setState({ sidebarDocked: this.mql.matches, sidebarOpen: false }) 42 | } 43 | 44 | render() { 45 | return ( 46 | Sidebar content} 48 | open={this.state.sidebarOpen} 49 | docked={this.state.sidebarDocked} 50 | onSetOpen={this.onSetSidebarOpen} 51 | > 52 |
53 |
54 | 55 |
56 | Bundle 57 | Phobia 58 |
59 | 60 |
61 |
62 | 87 | 88 | 89 | 90 |
91 |
92 | Main content 93 |
94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /client/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | import Header from './Header' 2 | 3 | export default Header 4 | -------------------------------------------------------------------------------- /client/components/Icons/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { WithClassName } from '../../../types' 4 | 5 | export default function SearchIcon({ className }: WithClassName) { 6 | return ( 7 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /client/components/Icons/SideEffectIcon.scss: -------------------------------------------------------------------------------- 1 | svg.sideeffect-icon-animated { 2 | overflow: hidden; 3 | 4 | .side-effect-icon-svg__circle, 5 | .side-effect-icon-svg__arrows { 6 | transform-origin: 50% 50%; 7 | transition: all 0.2s; 8 | } 9 | 10 | &:hover { 11 | .side-effect-icon-svg__arrows { 12 | transform: scale(1.3); 13 | stroke-width: 0.3px; 14 | } 15 | 16 | .side-effect-icon-svg__circle { 17 | transform: scale(1.2); 18 | stroke-width: 0.6px; 19 | } 20 | } 21 | } 22 | 23 | @keyframes shrink-arrows { 24 | from { 25 | transform: scale(2); 26 | } 27 | 28 | to { 29 | transform: scale(1); 30 | } 31 | } 32 | 33 | @keyframes grow-circle { 34 | from { 35 | transform: scale(1.5); 36 | } 37 | 38 | to { 39 | transform: scale(1); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/components/Icons/SideEffectIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cx from 'classnames' 3 | 4 | import { WithClassName } from '../../../types' 5 | import SideEffectIconSVG from '../../assets/side-effect.svg' 6 | 7 | export default function TreeShakeIcon({ className }: WithClassName) { 8 | return ( 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /client/components/Icons/TreeShakeIcon.scss: -------------------------------------------------------------------------------- 1 | svg.treeshake-icon-animated { 2 | .tree-shake-icon-svg__bush { 3 | transition: transform 0.3s; 4 | transform-origin: 50% 100%; 5 | } 6 | 7 | &:hover { 8 | .tree-shake-icon-svg__shake { 9 | transform-origin: 50% 50%; 10 | animation: move-to-sides 0.3s, shake 0.3s 0.15s; 11 | } 12 | 13 | .tree-shake-icon-svg__bush { 14 | transform: scaleY(1.2); 15 | } 16 | } 17 | } 18 | 19 | @keyframes move-to-sides { 20 | from { 21 | transform: scale(0); 22 | } 23 | 24 | to { 25 | transform: scale(1); 26 | } 27 | } 28 | 29 | @keyframes shake { 30 | 10%, 31 | 100% { 32 | transform: translate3d(-0.5px, 0, 0); 33 | } 34 | 35 | 80% { 36 | transform: translate3d(1px, 0, 0); 37 | } 38 | 39 | 30%, 40 | 70% { 41 | transform: translate3d(-1px, 0, 0); 42 | } 43 | 44 | 60% { 45 | transform: translate3d(1px, 0, 0); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/components/Icons/TreeShakeIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cx from 'classnames' 3 | 4 | import { WithClassName } from '../../../types' 5 | import TreeShakeIconSVG from '../../assets/tree-shake.svg' 6 | 7 | export default function TreeShakeIcon({ className }: WithClassName) { 8 | return ( 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /client/components/JumpingDots/JumpingDots.scss: -------------------------------------------------------------------------------- 1 | @import '../../../stylesheets/variables'; 2 | 3 | .jumping-dots { 4 | position: relative; 5 | text-align: center; 6 | padding: 0 $global-spacing * 0.5; 7 | } 8 | 9 | .jumping-dots__dot { 10 | display: inline-block; 11 | width: 2px; 12 | height: 2px; 13 | border-radius: 50%; 14 | margin-right: 3px; 15 | background: #303131; 16 | animation: dots-wave 1s linear infinite; 17 | 18 | &:nth-child(2) { 19 | animation-delay: -0.9s; 20 | } 21 | 22 | &:nth-child(3) { 23 | animation-delay: -0.8s; 24 | } 25 | } 26 | 27 | @keyframes dots-wave { 28 | 0%, 29 | 60%, 30 | 100% { 31 | transform: initial; 32 | } 33 | 34 | 30% { 35 | transform: translateY(-8px); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/components/JumpingDots/JumpingDots.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function JumpingDots() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /client/components/JumpingDots/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './JumpingDots' 2 | -------------------------------------------------------------------------------- /client/components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Link from 'next/link' 3 | 4 | import API from '../../api' 5 | import Heart from '../../assets/heart.svg' 6 | import DigitalOceanLogo from '../../assets/digital-ocean-logo.svg' 7 | import { WithClassName } from '../../../types' 8 | 9 | type LayoutProps = React.PropsWithChildren & WithClassName 10 | 11 | type LayoutState = { 12 | recentSearches: string[] 13 | } 14 | 15 | export default class Layout extends Component { 16 | state = { 17 | recentSearches: [], 18 | } 19 | 20 | componentDidMount() { 21 | API.getRecentSearches(5).then(searches => { 22 | this.setState({ 23 | recentSearches: Object.keys(searches), 24 | }) 25 | }) 26 | } 27 | 28 | render() { 29 | const { children, className } = this.props 30 | const { recentSearches } = this.state 31 | 32 | return ( 33 |
34 |
{children}
35 | 36 |
37 |
38 |
39 |

Recent searches

40 |
    41 | {recentSearches.map(search => ( 42 |
  • 43 | {search} 44 |
  • 45 | ))} 46 |
47 |
48 |
49 |
50 |
51 |

What does Bundlephobia do?

52 |

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

59 |

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

65 |

66 | Credits to{' '} 67 | 68 | {' '} 69 | @thekitze{' '} 70 | 71 | for the name. 72 |

73 |
74 | Hosted on 75 | 76 | 77 | 78 |
79 |
80 | 98 |
99 |
100 |
101 | ) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /client/components/Layout/index.ts: -------------------------------------------------------------------------------- 1 | import Layout from './Layout' 2 | 3 | export default Layout 4 | -------------------------------------------------------------------------------- /client/components/MetaTags.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | 4 | export const DEFAULT_DESCRIPTION_START = 5 | 'Bundlephobia helps you find the performance impact of npm packages.' 6 | 7 | type MetaTagsProps = { 8 | title: string 9 | canonicalPath: string 10 | description?: string 11 | twitterDescription?: string 12 | image?: string 13 | isLargeImage?: boolean 14 | } 15 | 16 | export default function MetaTags({ 17 | description, 18 | twitterDescription, 19 | title, 20 | canonicalPath, 21 | image, 22 | isLargeImage, 23 | }: MetaTagsProps) { 24 | const defaultDescription = `${DEFAULT_DESCRIPTION_START} Find the size of any javascript package and its effect on your frontend bundle.` 25 | const defaultImage = 'https://bundlephobia.com/android-chrome-256x256.png' 26 | const origin = 27 | typeof window === 'undefined' 28 | ? 'https://bundlephobia.com' 29 | : window.location.origin 30 | 31 | return ( 32 | 33 | {title} 34 | 35 | 36 | 41 | 42 | 43 | 48 | 53 | {twitterDescription && ( 54 | 59 | )} 60 | {isLargeImage ? ( 61 | 66 | ) : ( 67 | 68 | )} 69 | 70 | 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /client/components/PageNav/PageNav.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | import GithubLogo from '../../assets/github-logo.svg' 4 | 5 | type PageNavProps = { 6 | minimal?: boolean 7 | } 8 | 9 | const PageNav = ({ minimal }: PageNavProps) => ( 10 |
11 | {!minimal && ( 12 |
13 | 14 |
15 | Bundle 16 | Phobia 17 |
18 | 19 |
20 | )} 21 |
22 | 50 | 51 | 52 | 53 |
54 |
55 | ) 56 | 57 | export default PageNav 58 | -------------------------------------------------------------------------------- /client/components/PageNav/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './PageNav' 2 | -------------------------------------------------------------------------------- /client/components/ProgressHex/ProgressHex.scss: -------------------------------------------------------------------------------- 1 | .progress-hex { 2 | width: 8rem; 3 | height: 8rem; 4 | contain: strict; 5 | will-change: transform; 6 | 7 | circle { 8 | fill: #212121; 9 | transform-box: view-box; 10 | transform-origin: 50% 50%; 11 | } 12 | } 13 | 14 | .progress-hex__trail { 15 | stroke-width: 1px; 16 | } 17 | -------------------------------------------------------------------------------- /client/components/ProgressHex/ProgressHex.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ProgressHexAnimator from './progress-hex-timeline' 3 | 4 | type ProgressHexProps = { 5 | compact?: boolean 6 | } 7 | 8 | class ProgressHex extends Component { 9 | svgRef: React.RefObject 10 | animator?: ProgressHexAnimator 11 | timeline?: ReturnType 12 | 13 | constructor(props: ProgressHexProps) { 14 | super(props) 15 | this.svgRef = React.createRef() 16 | } 17 | 18 | componentDidMount() { 19 | this.animator = new ProgressHexAnimator({ svg: this.svgRef.current! }) 20 | this.timeline = this.animator.createTimeline() 21 | this.timeline.play() 22 | } 23 | 24 | componentWillUnmount() { 25 | this.timeline?.pause() 26 | } 27 | 28 | render() { 29 | const { compact } = this.props 30 | return ( 31 | 39 | {!compact && ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | )} 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | ) 114 | } 115 | } 116 | 117 | export default ProgressHex 118 | -------------------------------------------------------------------------------- /client/components/ProgressHex/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ProgressHex' 2 | -------------------------------------------------------------------------------- /client/components/QuickStatsBar/QuickStatsBar.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | @import '../../../stylesheets/colors'; 4 | @import '../../../stylesheets/variables'; 5 | 6 | .quick-stats-bar { 7 | display: flex; 8 | align-content: center; 9 | @include font-size-xs; 10 | color: lighten($raven, 15%); 11 | background: lighten($raven, 55%); 12 | border-radius: 0 0 10px 10px; 13 | overflow: hidden; 14 | } 15 | 16 | .quick-stats-bar__stat { 17 | padding: math.div($global-spacing, 1.5) $global-spacing * 1.5; 18 | display: flex; 19 | align-content: center; 20 | position: relative; 21 | justify-content: center; 22 | flex: 1 1 auto; 23 | white-space: nowrap; 24 | margin: auto 0; 25 | 26 | & > * { 27 | margin: auto 0; 28 | } 29 | 30 | &:not(:first-of-type)::before { 31 | content: ''; 32 | width: 1px; 33 | height: 60%; 34 | position: absolute; 35 | background: transparentize(black, 0.95); 36 | top: 0; 37 | bottom: 0; 38 | margin: auto; 39 | left: 0; 40 | } 41 | 42 | &:first-of-type, 43 | &:last-of-type { 44 | //padding-left: $global-spacing; 45 | } 46 | } 47 | 48 | .quick-stats-bar__stat--optional { 49 | @media screen and (max-width: 48em) { 50 | display: none; 51 | } 52 | 53 | @media screen and (max-width: 40em) { 54 | display: none; 55 | } 56 | } 57 | 58 | .quick-stats-bar__stat--description { 59 | overflow: hidden; 60 | text-overflow: ellipsis; 61 | display: block; 62 | flex-grow: 1; 63 | 64 | @media screen and (max-width: 40em) { 65 | display: none; 66 | } 67 | } 68 | 69 | .quick-stats-bar__stat--description-content { 70 | margin-left: $global-spacing; 71 | } 72 | 73 | .quick-stats-bar__stat-icon { 74 | margin-right: $global-spacing; 75 | } 76 | 77 | .quick-stats-bar__logo-icon { 78 | vertical-align: middle; 79 | 80 | &.quick-stats-bar__logo-icon--npm { 81 | width: 36px; 82 | } 83 | 84 | &.quick-stats-bar__logo-icon--github { 85 | height: 18px; 86 | width: 18px; 87 | } 88 | 89 | path { 90 | transition: fill 0.2s; 91 | fill: lighten($raven, 15%); 92 | } 93 | } 94 | 95 | .quick-stats-bar__link { 96 | margin: auto $global-spacing * 0.5; 97 | 98 | &:hover { 99 | .quick-stats-bar__logo-icon--github { 100 | path { 101 | fill: #333; 102 | } 103 | } 104 | .quick-stats-bar__logo-icon--npm { 105 | path { 106 | fill: #cb3837; 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /client/components/QuickStatsBar/QuickStatsBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import { sanitizeHTML } from '../../../utils/common.utils' 4 | import TreeShakeIcon from '../../assets/tree-shake.svg' 5 | import SideEffectIcon from '../../assets/side-effect.svg' 6 | import DependencyIcon from '../../assets/dependency.svg' 7 | import GithubIcon from '../../assets/github-logo.svg' 8 | import NPMIcon from '../../assets/npm-logo.svg' 9 | import InfoIcon from '../../assets/info.svg' 10 | import { PackageInfo } from '../../../types' 11 | 12 | type QuickStatsBarProps = Pick< 13 | PackageInfo, 14 | | 'name' 15 | | 'description' 16 | | 'repository' 17 | | 'dependencyCount' 18 | | 'isTreeShakeable' 19 | | 'hasSideEffects' 20 | > 21 | 22 | class QuickStatsBar extends Component { 23 | static defaultProps = { 24 | description: '', 25 | } 26 | 27 | getStatItemCount = () => { 28 | const { isTreeShakeable, hasSideEffects } = this.props 29 | let statItemCount = 0 30 | 31 | if (isTreeShakeable) statItemCount += 1 32 | if (hasSideEffects !== true) statItemCount += 1 33 | return statItemCount 34 | } 35 | 36 | getTrimmedDescription = () => { 37 | let trimmed 38 | const { description } = this.props 39 | if (description.trim().endsWith('.')) { 40 | trimmed = description.substring(0, description.length - 1) 41 | } else { 42 | trimmed = description.trim() 43 | } 44 | 45 | return sanitizeHTML(trimmed) 46 | } 47 | 48 | render() { 49 | const { 50 | isTreeShakeable, 51 | hasSideEffects, 52 | dependencyCount, 53 | name, 54 | repository, 55 | } = this.props 56 | const statItemCount = this.getStatItemCount() 57 | 58 | return ( 59 |
60 |
64 | 65 | {statItemCount < 2 && ( 66 | 73 | )} 74 |
75 | 76 | {isTreeShakeable && ( 77 |
78 | {' '} 79 | tree-shakeable 80 |
81 | )} 82 | 83 | {!(hasSideEffects === true) && ( 84 |
85 | {' '} 86 | 87 | {!(hasSideEffects === false) && hasSideEffects.length 88 | ? 'some side-effects' 89 | : 'side-effect free'} 90 | 91 |
92 | )} 93 |
94 | 95 | 96 | {dependencyCount === 0 ? ( 97 | 'no dependencies' 98 | ) : ( 99 | 100 | {dependencyCount}{' '} 101 | {dependencyCount > 1 ? 'dependencies' : 'dependency'} 102 | 103 | )} 104 | 105 |
106 |
107 | 113 | 114 | 115 | {repository && ( 116 | 122 | 123 | 124 | )} 125 |
126 |
127 | ) 128 | } 129 | } 130 | 131 | export default QuickStatsBar 132 | -------------------------------------------------------------------------------- /client/components/QuickStatsBar/index.ts: -------------------------------------------------------------------------------- 1 | import QuickStatsBar from './QuickStatsBar' 2 | 3 | export default QuickStatsBar 4 | -------------------------------------------------------------------------------- /client/components/ResultLayout/ResultLayout.scss: -------------------------------------------------------------------------------- 1 | @import '../../../stylesheets/variables'; 2 | @import '../../../stylesheets/colors'; 3 | 4 | .page-header { 5 | padding: $global-spacing * 3; 6 | padding-bottom: $global-spacing * 2; 7 | display: flex; 8 | align-items: center; 9 | 10 | @media screen and (max-width: 40em) { 11 | padding: $global-spacing * 2; 12 | } 13 | } 14 | 15 | .page-header--right-section { 16 | margin-left: auto; 17 | display: flex; 18 | align-items: center; 19 | } 20 | 21 | .github-logo { 22 | width: 30px; 23 | height: 30px; 24 | 25 | @media screen and (max-width: 40em) { 26 | width: 20px; 27 | height: 20px; 28 | } 29 | 30 | path { 31 | fill: #666; 32 | transition: fill 0.2s; 33 | } 34 | 35 | &:hover { 36 | path { 37 | fill: black; 38 | } 39 | } 40 | } 41 | 42 | .logo-small { 43 | @include font-size-reg; 44 | text-transform: uppercase; 45 | font-weight: $font-weight-very-bold; 46 | letter-spacing: 3px; 47 | user-select: none; 48 | cursor: pointer; 49 | color: #212121; 50 | } 51 | 52 | .logo-small__alt { 53 | color: #888; 54 | } 55 | 56 | .page-container { 57 | display: flex; 58 | flex-direction: column; 59 | min-height: 100vh; 60 | min-height: calc(100vh - 6px); 61 | flex-gorw: 1; 62 | } 63 | 64 | .page-content { 65 | display: flex; 66 | align-items: center; 67 | justify-content: center; 68 | flex-direction: column; 69 | flex-grow: 1; 70 | 71 | @media screen and (max-width: 40em) { 72 | padding: 0 $global-spacing * 2; 73 | } 74 | } 75 | 76 | .page-header__quicklinks { 77 | list-style: none; 78 | margin: 0 2rem 0 0; 79 | font-weight: $font-weight-light; 80 | display: flex; 81 | 82 | a { 83 | @include font-size-xs; 84 | text-transform: uppercase; 85 | letter-spacing: 0.3px; 86 | font-weight: $font-weight-bold; 87 | opacity: 0.55; 88 | color: $raven; 89 | transition: opacity 0.2s; 90 | 91 | &:hover { 92 | opacity: 1; 93 | } 94 | } 95 | 96 | li + li { 97 | margin-left: $global-spacing * 2; 98 | } 99 | 100 | @media screen and (max-width: 40em) { 101 | max-width: 40vw; 102 | overflow: scroll; 103 | align-items: center; 104 | justify-content: flex-end; 105 | overflow: scroll; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /client/components/ResultLayout/ResultLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import cx from 'classnames' 3 | 4 | import Layout from '../../components/Layout' 5 | import PageNav from '../PageNav' 6 | import { WithClassName } from '../../../types' 7 | 8 | export default class ResultLayout extends Component< 9 | React.PropsWithChildren & WithClassName 10 | > { 11 | render() { 12 | const { children, className } = this.props 13 | return ( 14 | 15 |
16 | 17 |
{children}
18 |
19 |
20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/components/ResultLayout/index.ts: -------------------------------------------------------------------------------- 1 | import ResultLayout from './ResultLayout' 2 | 3 | export default ResultLayout 4 | -------------------------------------------------------------------------------- /client/components/SimilarPackageCard/SimilarPackageCard.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | @import '../../../stylesheets/colors'; 4 | @import '../../../stylesheets/variables'; 5 | 6 | .similar-package-card { 7 | border: 1px solid $autocomplete-border-color; 8 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); 9 | border-radius: 10px; 10 | width: calc(25% - #{$global-spacing * 2}); 11 | display: flex; 12 | flex-direction: column; 13 | margin: $global-spacing; 14 | color: inherit; 15 | transition: all 0.2s; 16 | 17 | &:hover { 18 | transform: scale(1.01); 19 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.06); 20 | border: 1px solid fade-in($autocomplete-border-color, 0.02); 21 | } 22 | 23 | &.similar-package-card--empty { 24 | border-style: dashed; 25 | background: transparentize($raven, 0.97); 26 | } 27 | 28 | @media screen and (max-width: 64em) { 29 | margin: math.div($global-spacing, 1.5); 30 | width: calc(33.3% - #{$global-spacing * 2}); 31 | } 32 | 33 | @media screen and (max-width: 48em) { 34 | margin: math.div($global-spacing, 1.5); 35 | width: calc(50% - #{$global-spacing * 1.5}); 36 | } 37 | 38 | @media screen and (max-width: 40em) { 39 | width: 100%; 40 | } 41 | } 42 | 43 | .similar-package-card--empty { 44 | color: transparentize($raven, 0.5); 45 | align-items: center; 46 | } 47 | 48 | .similar-package-card__wrap { 49 | padding: $global-spacing * 2; 50 | flex-grow: 1; 51 | 52 | .similar-package-card--empty & { 53 | padding: $global-spacing * 5 $global-spacing * 2; 54 | align-items: center; 55 | text-align: center; 56 | } 57 | } 58 | 59 | .similar-package-card__header { 60 | display: flex; 61 | } 62 | 63 | .similar-package-card__name { 64 | margin: 0; 65 | font-family: $font-family-code; 66 | font-weight: $font-weight-light; 67 | flex-grow: 1; 68 | word-break: break-word; 69 | } 70 | 71 | .similar-package-card__description { 72 | @include font-size-sm; 73 | color: $dark-raven; 74 | line-height: 1.5; 75 | margin: $global-spacing 0 0 0; 76 | word-break: break-word; 77 | img { 78 | height: auto; 79 | max-width: 100%; 80 | } 81 | 82 | .similar-package-card--empty & { 83 | text-transform: uppercase; 84 | letter-spacing: 1px; 85 | font-weight: $font-weight-bold; 86 | color: transparentize($raven, 0.4); 87 | } 88 | } 89 | 90 | .similar-package-card__footer { 91 | background: lighten($raven, 54%); 92 | display: flex; 93 | padding: $global-spacing $global-spacing * 2; 94 | align-items: center; 95 | border-radius: 0 0 10px 10px; 96 | } 97 | 98 | .similar-package-card__stat { 99 | & + & { 100 | margin-left: $global-spacing * 2; 101 | } 102 | } 103 | 104 | .similar-package-card__number { 105 | @include font-size-md; 106 | font-weight: $font-weight-very-bold; 107 | } 108 | 109 | .similar-package-card__comparison--positive { 110 | color: $pastel-green; 111 | } 112 | 113 | .similar-package-card__comparison--negative { 114 | color: $carrot-orange; 115 | } 116 | 117 | .similar-package-card__comparison--similar { 118 | color: $raven; 119 | } 120 | 121 | .similar-package-card__label { 122 | @include font-size-xs; 123 | text-transform: uppercase; 124 | letter-spacing: 1px; 125 | line-height: 1.5; 126 | 127 | .similar-package-card__size & { 128 | color: $raven; 129 | } 130 | } 131 | 132 | .similar-package-card__treeshake { 133 | height: $global-spacing * 2.5; 134 | width: auto; 135 | margin-left: auto; 136 | } 137 | 138 | .similar-package-card__shrink { 139 | font-size: 75%; 140 | .similar-package-card__size & { 141 | color: $raven; 142 | } 143 | } 144 | 145 | .similar-package-card__github-icon { 146 | height: $global-spacing * 2; 147 | width: auto; 148 | opacity: 0.5; 149 | transition: all 0.2s; 150 | vertical-align: middle; 151 | 152 | &:hover { 153 | opacity: 1; 154 | } 155 | } 156 | 157 | .similar-package-card__plus { 158 | width: 35%; 159 | height: auto; 160 | margin-bottom: $global-spacing * 1.5; 161 | path { 162 | fill: transparentize($raven, 0.7); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /client/components/SimilarPackageCard/index.ts: -------------------------------------------------------------------------------- 1 | import SimilarPackageCard from './SimilarPackageCard' 2 | 3 | export default SimilarPackageCard 4 | -------------------------------------------------------------------------------- /client/components/Stat/Stat.scss: -------------------------------------------------------------------------------- 1 | @import '../../../stylesheets/variables'; 2 | @import '../../../stylesheets/colors'; 3 | 4 | .stat-container { 5 | margin: 0 24px; 6 | 7 | @media screen and (max-width: 40em) { 8 | margin: 0 $global-spacing; 9 | } 10 | } 11 | 12 | .stat-container--compact { 13 | margin: 0; 14 | } 15 | 16 | .stat-container__value-container { 17 | display: flex; 18 | align-items: baseline; 19 | justify-content: center; 20 | padding: 5px 15px; 21 | 22 | .stat-container--compact & { 23 | padding-top: 0; 24 | } 25 | } 26 | 27 | .stat-container__value { 28 | @include font-size-xxl; 29 | font-weight: bold; 30 | color: #212121; 31 | background: inherit; 32 | position: relative; 33 | 34 | .stat-container--compact & { 35 | @include font-size-lg; 36 | font-weight: $font-weight-light; 37 | } 38 | } 39 | 40 | .stat-container__value.time::before { 41 | content: attr(data-value); 42 | position: absolute; 43 | z-index: 2; 44 | overflow: hidden; 45 | color: $pastel-green; 46 | white-space: nowrap; 47 | width: 0%; 48 | transition: width 0.3s; 49 | } 50 | 51 | .stat-container__value.time:hover::before { 52 | width: 100%; 53 | transition-duration: inherit; 54 | } 55 | 56 | .stat-container__unit { 57 | @include font-size-xl; 58 | color: $raven; 59 | font-weight: bold; 60 | margin-left: 4px; 61 | 62 | .stat-container--compact & { 63 | @include font-size-sm; 64 | font-weight: $font-weight-thin; 65 | } 66 | } 67 | 68 | .stat-container__footer { 69 | display: flex; 70 | justify-content: center; 71 | align-items: center; 72 | margin-top: 10px; 73 | 74 | .stat-container--compact & { 75 | margin-top: 0; 76 | } 77 | } 78 | 79 | .stat-container__label { 80 | @include font-size-reg; 81 | color: $raven; 82 | text-transform: uppercase; 83 | letter-spacing: 2px; 84 | text-align: center; 85 | 86 | .stat-container--compact & { 87 | @include font-size-sm; 88 | letter-spacing: 1px; 89 | } 90 | } 91 | 92 | .stat-container__info-text { 93 | margin-left: $global-spacing * 0.5; 94 | border: 1px solid rgba(40, 40, 40, 0.5); 95 | color: rgba(40, 40, 40, 0.5); 96 | width: 12px; 97 | height: 13px; 98 | font-family: $font-family-code; 99 | font-size: 12px; 100 | display: flex; 101 | align-items: center; 102 | justify-content: center; 103 | border-radius: 2px; 104 | transition: background 0.2s; 105 | cursor: help; 106 | 107 | &:hover { 108 | background: rgba(40, 40, 40, 0.6); 109 | color: white; 110 | } 111 | 112 | &::after, 113 | &::before { 114 | font-family: $font-family-body; 115 | } 116 | 117 | @media screen and (max-width: 40em) { 118 | display: none; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /client/components/Stat/Stat.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cx from 'classnames' 3 | 4 | import { formatSize, formatTime } from '../../../utils' 5 | import { WithClassName } from '../../../types' 6 | 7 | const Type = { 8 | SIZE: 'size', 9 | TIME: 'time', 10 | } as const 11 | 12 | type StatProps = WithClassName & { 13 | value: number 14 | type: 'size' | 'time' 15 | label: string 16 | infoText?: string 17 | compact?: boolean 18 | } 19 | 20 | export default function Stat({ 21 | value, 22 | label, 23 | type, 24 | infoText, 25 | compact, 26 | className, 27 | }: StatProps) { 28 | const roundedValue = 29 | type === Type.SIZE 30 | ? parseFloat(formatSize(value).size.toFixed(1)) 31 | : parseFloat(formatTime(value).size.toFixed(2)) 32 | 33 | return ( 34 |
39 |
40 |
41 |
46 | {roundedValue} 47 |
48 |
49 |
50 | {type === Type.SIZE ? formatSize(value).unit : formatTime(value).unit}{' '} 51 |
52 |
53 |
54 |
55 |
{label}
56 | {infoText && ( 57 |
62 | i 63 |
64 | )} 65 |
66 |
67 | ) 68 | } 69 | 70 | Stat.type = Type 71 | -------------------------------------------------------------------------------- /client/components/Stat/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Stat' 2 | -------------------------------------------------------------------------------- /client/components/Treemap/Treemap.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import squarify from './squarify' 4 | 5 | type TreeMapProps = { 6 | width: number 7 | height: number 8 | } & React.PropsWithChildren & 9 | React.HTMLAttributes 10 | 11 | class TreeMap extends Component { 12 | render() { 13 | const { width, height, children, ...others } = this.props 14 | 15 | const values = React.Children.map(children, square => 16 | React.isValidElement(square) ? square.props.value : square 17 | ) 18 | 19 | const squared = squarify(values, width, height, 0, 0) 20 | const getBorderRadius = (index: number) => { 21 | const topLeftRadius = 22 | squared[index][0] || squared[index][1] ? '0px' : '10px' 23 | const topRightRadius = 24 | squared[index][1] === 0 && squared[index][2] === width ? '10px' : '0px' 25 | const bottomLeftRadius = 26 | squared[index][3] === height && squared[index][0] === 0 ? '10px' : '0px' 27 | const bottomRightRadius = 28 | Math.round(squared[index][3]) === height && 29 | Math.round(squared[index][2]) === width 30 | ? '10px' 31 | : '0px' 32 | 33 | return `${topLeftRadius} ${topRightRadius} ${bottomRightRadius} ${bottomLeftRadius}` 34 | } 35 | 36 | return ( 37 |
38 | {React.Children.map(children, (child, index) => { 39 | if (!React.isValidElement(child)) { 40 | return child 41 | } 42 | 43 | const childProps = { 44 | left: `${(squared[index][0] / width) * 100}%`, 45 | top: `${(squared[index][1] / height) * 100}%`, 46 | width: `${ 47 | ((squared[index][2] - squared[index][0]) / width) * 100 48 | }%`, 49 | height: `${ 50 | ((squared[index][3] - squared[index][1]) / height) * 100 51 | }%`, 52 | borderRadius: getBorderRadius(index), 53 | data: squared[index], 54 | } 55 | 56 | return React.cloneElement(child, childProps) 57 | })} 58 |
59 | ) 60 | } 61 | } 62 | 63 | export default TreeMap 64 | -------------------------------------------------------------------------------- /client/components/Treemap/TreemapSquare.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type TreemapSquareProps = { 4 | style: React.CSSProperties 5 | data?: any 6 | } & React.PropsWithChildren & 7 | Pick< 8 | React.CSSProperties, 9 | 'left' | 'top' | 'width' | 'height' | 'borderRadius' 10 | > 11 | 12 | function TreemapSquare({ 13 | children, 14 | left, 15 | top, 16 | width, 17 | height, 18 | borderRadius, 19 | data, 20 | style, 21 | ...other 22 | }: TreemapSquareProps) { 23 | return ( 24 |
43 | {children} 44 |
45 | ) 46 | } 47 | 48 | export default TreemapSquare 49 | -------------------------------------------------------------------------------- /client/components/Treemap/index.ts: -------------------------------------------------------------------------------- 1 | import Treemap from './Treemap' 2 | import TreemapSquare from './TreemapSquare' 3 | 4 | export { Treemap, TreemapSquare } 5 | -------------------------------------------------------------------------------- /client/components/Warning/Warning.scss: -------------------------------------------------------------------------------- 1 | @import '../../../stylesheets/colors'; 2 | @import '../../../stylesheets/variables'; 3 | 4 | .warning-bar { 5 | @include font-size-xs; 6 | background: lighten($dandelion, 5%); 7 | padding: $global-spacing * 0.5 $global-spacing; 8 | border-radius: 4px; 9 | margin-top: 2vh; 10 | color: darken(desaturate($dandelion, 35%), 35%); 11 | 12 | a { 13 | @include font-size-xxs; 14 | color: inherit; 15 | font-weight: $font-weight-bold; 16 | opacity: 0.8; 17 | padding-left: $global-spacing; 18 | text-transform: uppercase; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/components/Warning/Warning.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class Warning extends Component { 4 | render() { 5 | return
{this.props.children}
6 | } 7 | } 8 | 9 | export default Warning 10 | -------------------------------------------------------------------------------- /client/components/Warning/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Warning' 2 | -------------------------------------------------------------------------------- /client/config/colors.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | '#718af0', 3 | '#6e98e6', 4 | '#79c0f2', 5 | '#7dd6fa', 6 | '#6ed0db', 7 | '#59b3aa', 8 | '#7ebf80', 9 | '#9bc26b', 10 | '#dee675', 11 | '#fff080', 12 | '#ffd966', 13 | '#ffbf66', 14 | '#ff8a66', 15 | '#ed7872', 16 | '#db6b8f', 17 | '#bd66cc', 18 | '#cae0eb', 19 | ] as const 20 | -------------------------------------------------------------------------------- /client/config/scanBlacklist.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Packages that are unlikely to be useful in 3 | * determining size of frontend bundles. 4 | */ 5 | export default [ 6 | /*** Config ****/ 7 | /dotenv/, 8 | 9 | /*** CLI Tools ***/ 10 | /gulp/, 11 | /cli/, 12 | 13 | /*** Build Tools ****/ 14 | /webpack/, 15 | /react-native/, 16 | /babel/, 17 | /rollup/, 18 | /autoprefixer/, 19 | /css-nano/, 20 | /node-sass/, 21 | /next/, 22 | /create-react-app/, 23 | /react-scripts/, 24 | /-loader/, 25 | /extract-plugin/, 26 | 27 | /**** Testing ****/ 28 | /jest/, 29 | /enzyme/, 30 | /mocha/, 31 | /ava/, 32 | /nightwatch/, 33 | 34 | /**** Server libraries ****/ 35 | /koa/, 36 | /express/, 37 | /pm2/, 38 | /nodemon/, 39 | /supervisor/, 40 | 41 | /**** Common dev dependencies ****/ 42 | /prop-types/, 43 | /devtools/, 44 | ] as const 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { register } = require('esbuild-register/dist/node') 2 | const tsconfig = require('./tsconfig.server.json') 3 | register({ 4 | tsconfigRaw: tsconfig, 5 | target: tsconfig.compilerOptions.target, 6 | }) 7 | require('./index.ts') 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | pageExtensions: ['page.js', 'page.tsx'], 5 | sassOptions: { 6 | includePaths: [path.join(__dirname, 'stylesheets')], 7 | }, 8 | env: { 9 | RELEASE_DATE: new Date().toDateString(), 10 | }, 11 | webpack(config) { 12 | config.module.rules.push({ 13 | test: /\.svg$/i, 14 | issuer: /\.[jt]sx?$/, 15 | use: [ 16 | { 17 | loader: '@svgr/webpack', 18 | options: { 19 | svgoConfig: { 20 | plugins: [ 21 | { 22 | name: 'removeViewBox', 23 | active: false, 24 | }, 25 | ], 26 | }, 27 | }, 28 | }, 29 | ], 30 | }) 31 | 32 | return config 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "exec": "ts-node --project tsconfig.server.json ./index.ts", 3 | "ext": "js ts" 4 | } 5 | -------------------------------------------------------------------------------- /pages/_app.page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | import { AppProps } from 'next/app' 4 | import '../stylesheets/index.scss' 5 | 6 | function App({ Component, pageProps }: AppProps) { 7 | return ( 8 | <> 9 | 10 | 14 | Bundlephobia ❘ cost of adding a npm package 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | export default App 22 | -------------------------------------------------------------------------------- /pages/blog/components/Article.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | import React from 'react' 3 | import BlogLayout from '../../../client/components/BlogLayout' 4 | import { useContentful } from 'react-contentful' 5 | import BlogPost from './Post' 6 | import ContentfulProvider from './ContentfulProvider' 7 | 8 | const ArticleWithContent = () => { 9 | return ( 10 | 11 | 12 |
13 | 14 | 15 | ) 16 | } 17 | 18 | const Article = () => { 19 | const router = useRouter() 20 | 21 | const { data, error, loading } = useContentful({ 22 | contentType: 'blogPost', 23 | }) 24 | 25 | if (loading) { 26 | return <>Loading... 27 | } else if (error) { 28 | return ( 29 |
30 |         {error}
31 |       
32 | ) 33 | } else if (data) { 34 | return ( 35 | <> 36 | {data.items.map(item => ( 37 | 44 | ))}{' '} 45 | 46 | ) 47 | } 48 | 49 | return <>Loading... 50 | } 51 | 52 | export default ArticleWithContent 53 | -------------------------------------------------------------------------------- /pages/blog/components/ContentfulProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | ContentfulClient, 4 | ContentfulClientInterface, 5 | ContentfulProvider as ContentfulProviderOriginal, 6 | } from 'react-contentful' 7 | 8 | const contentfulClient: ContentfulClientInterface = 9 | new (ContentfulClient as any)({ 10 | accessToken: 11 | process.env.NODE_ENV === 'production' 12 | ? 'mTlrTPvam3pHJYiOQyFAjKp2rxIBRo1qelSJPrSQUPE' 13 | : 'MvUlRvc3rMnq7CXCBPxiXSUstNscsq9XH8kMANcxoY8', 14 | host: 15 | process.env.NODE_ENV === 'production' 16 | ? 'cdn.contentful.com' 17 | : 'preview.contentful.com', 18 | space: '9cnlte662r2w', 19 | }) 20 | 21 | const ContentfulProvider = ({ children }: React.PropsWithChildren) => ( 22 | 23 | {children} 24 | 25 | ) 26 | 27 | export default ContentfulProvider 28 | -------------------------------------------------------------------------------- /pages/blog/components/Post.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | documentToReactComponents, 4 | NodeRenderer, 5 | } from '@contentful/rich-text-react-renderer' 6 | import { BLOCKS, Document, TopLevelBlock } from '@contentful/rich-text-types' 7 | import Link from 'next/link' 8 | 9 | const getWordCount = (node: TopLevelBlock) => { 10 | let count = 0 11 | if (node.nodeType === 'paragraph') { 12 | node.content.forEach(content => { 13 | switch (content.nodeType) { 14 | case 'text': 15 | count += content.value.split(' ').length 16 | } 17 | }) 18 | } else if (node.nodeType === 'embedded-asset-block') { 19 | count += 80 20 | } 21 | 22 | return count 23 | } 24 | 25 | const makeContentPreview = (content: Document) => { 26 | const wordLimit = 50 27 | let wordCount = 0 28 | const previewContent = [] 29 | let counter = 0 30 | 31 | const { content: innerContent, ...others } = content 32 | 33 | while (wordCount < wordLimit && counter < innerContent.length) { 34 | previewContent.push(innerContent[counter]) 35 | wordCount += getWordCount(innerContent[counter]) 36 | counter++ 37 | } 38 | 39 | return { ...others, content: previewContent } 40 | } 41 | 42 | type PostProps = { 43 | title: React.ReactNode 44 | content: Document 45 | slug: string 46 | preview?: boolean 47 | createdAt: string | number | Date 48 | } 49 | 50 | const Post = ({ title, content, slug, preview, createdAt }: PostProps) => { 51 | const options = { 52 | renderNode: { 53 | [BLOCKS.EMBEDDED_ASSET]: (node: Parameters[0]) => { 54 | // render the EMBEDDED_ASSET as you need 55 | return ( 56 | {node.data.target.fields.description} 62 | ) 63 | }, 64 | }, 65 | } 66 | 67 | return ( 68 |
69 | {preview ? ( 70 | 71 |

{title}

72 | 73 | ) : ( 74 |

{title}

75 | )} 76 |

77 | {new Intl.DateTimeFormat('en-GB', { 78 | dateStyle: 'long', 79 | }).format(new Date(createdAt))} 80 |

81 |
82 | {documentToReactComponents( 83 | preview ? makeContentPreview(content) : content, 84 | options 85 | )} 86 |
87 | {preview && ( 88 | 89 | Read more... 90 | 91 | )} 92 |
93 | ) 94 | } 95 | 96 | export default Post 97 | -------------------------------------------------------------------------------- /pages/blog/digital-ocean-partnership.page.tsx: -------------------------------------------------------------------------------- 1 | import Article from './components/Article' 2 | 3 | const ArticleIs = () =>
4 | 5 | export default ArticleIs 6 | -------------------------------------------------------------------------------- /pages/blog/index.page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BlogLayout from '../../client/components/BlogLayout' 3 | import { useContentful } from 'react-contentful' 4 | import Separator from '../../client/components/Separator' 5 | import Post from './components/Post' 6 | import ContentfulProvider from './components/ContentfulProvider' 7 | 8 | const BlogWithContent = () => { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | } 17 | 18 | const BlogHome = () => { 19 | const { data, error, loading } = useContentful({ 20 | contentType: 'blogPost', 21 | }) 22 | 23 | let content = null 24 | 25 | if (loading) { 26 | content = 'Loading...' 27 | } else if (error) { 28 | content = ( 29 |
30 |         {error}
31 |       
32 | ) 33 | } else if (data) { 34 | content = ( 35 | <> 36 | {data.items.map(item => ( 37 | 45 | ))}{' '} 46 | 47 | ) 48 | } 49 | 50 | return ( 51 | <> 52 |

Blogosphere

53 | 59 | {content} 60 | 61 | ) 62 | } 63 | 64 | export default BlogWithContent 65 | -------------------------------------------------------------------------------- /pages/compare/ComparePage.scss: -------------------------------------------------------------------------------- 1 | @import '../../stylesheets/base'; 2 | @import '../../stylesheets/variables'; 3 | 4 | .result-header { 5 | padding: $global-spacing * 3; 6 | padding-bottom: $global-spacing; 7 | display: flex; 8 | align-items: center; 9 | 10 | @media screen and (max-width: 40em) { 11 | padding: $global-spacing * 2; 12 | } 13 | } 14 | 15 | .page-container { 16 | min-height: 100vh; 17 | display: flex; 18 | flex-direction: column; 19 | min-height: calc(100vh - 6px); 20 | //border-top: 3px dashed #333; 21 | //border-bottom: 3px dashed #333; 22 | //border-width: 3px 0; 23 | } 24 | 25 | .result-header--right-section { 26 | margin-left: auto; 27 | } 28 | 29 | .github-logo { 30 | width: 30px; 31 | height: 30px; 32 | 33 | @media screen and (max-width: 40em) { 34 | width: 20px; 35 | height: 20px; 36 | } 37 | 38 | path { 39 | fill: #666; 40 | transition: fill 0.2s; 41 | } 42 | 43 | &:hover { 44 | path { 45 | fill: black; 46 | } 47 | } 48 | } 49 | 50 | .logo-small { 51 | @include font-size-reg; 52 | text-transform: uppercase; 53 | font-weight: $font-weight-very-bold; 54 | letter-spacing: 3px; 55 | user-select: none; 56 | cursor: pointer; 57 | color: #212121; 58 | } 59 | 60 | .logo-small__alt { 61 | color: #888; 62 | } 63 | 64 | .compare__search-container { 65 | display: flex; 66 | align-items: center; 67 | justify-content: center; 68 | flex-grow: 1; 69 | 70 | @media screen and (max-width: 40em) { 71 | padding: 0 $global-spacing * 2; 72 | } 73 | } 74 | 75 | .compare__search-inputs { 76 | display: flex; 77 | align-items: center; 78 | margin-top: -15vh; 79 | max-width: $global-spacing * 78; 80 | } 81 | 82 | .compare__vs { 83 | @include font-size-xl; 84 | //color: $raven; 85 | font-weight: $font-weight-thin; 86 | margin: 0 $global-spacing; 87 | } 88 | -------------------------------------------------------------------------------- /pages/compare/index.js: -------------------------------------------------------------------------------- 1 | import ComparePage from './ComparePage' 2 | 3 | export default ComparePage 4 | -------------------------------------------------------------------------------- /pages/index.scss: -------------------------------------------------------------------------------- 1 | @import '../stylesheets/base'; 2 | @import '../stylesheets/variables'; 3 | 4 | .homepage { 5 | height: 100vh; 6 | height: calc(100vh - 4px); 7 | padding: 0 $global-spacing * 2; 8 | } 9 | 10 | .homepage__content { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | max-width: 100%; 16 | height: calc(100vh - 90px); 17 | } 18 | 19 | .homepage__tagline { 20 | @include font-size-md; 21 | font-family: $font-family-body; 22 | font-weight: $font-weight-hairline; 23 | color: #777; 24 | margin-top: $global-spacing; 25 | text-align: center; 26 | line-height: 1.4; 27 | letter-spacing: 1px; 28 | } 29 | 30 | .logo { 31 | text-transform: uppercase; 32 | font-weight: $font-weight-bold; 33 | letter-spacing: 4px; 34 | //@include font-size-md-2; // Enable after removing chirstmas 35 | @include font-size-md; 36 | user-select: none; 37 | margin-top: $global-spacing * 2; 38 | } 39 | 40 | .logo__alt { 41 | color: #888; 42 | } 43 | 44 | .logo__skeleton { 45 | animation: move 2s alternate infinite; 46 | 47 | .logo-graphic:hover & { 48 | stroke: desaturate($pastel-green, 30%); 49 | } 50 | } 51 | 52 | .logo-graphic { 53 | width: 137px * 0.9; 54 | height: 157px * 0.9; 55 | 56 | @media screen and (max-width: 40em) { 57 | width: 137px * 0.8; 58 | height: 157px * 0.8; 59 | } 60 | 61 | &:hover { 62 | .logo__skeleton-group { 63 | opacity: 0.4; 64 | } 65 | } 66 | } 67 | 68 | .homepage__search-input-container { 69 | width: 100%; 70 | } 71 | 72 | .homepage__search-input { 73 | margin-top: $global-spacing; 74 | width: 100%; 75 | 76 | @media screen and (max-width: 40em) { 77 | margin-top: 5vh; 78 | margin-bottom: 5vh; 79 | } 80 | 81 | .autocomplete-input { 82 | text-align: center; 83 | } 84 | .autocomplete-input__dummy-input { 85 | display: flex; 86 | justify-content: center; 87 | } 88 | } 89 | 90 | .homepage__or-divider { 91 | font-weight: $font-weight-bold; 92 | text-transform: uppercase; 93 | color: $raven; 94 | margin-top: 4vh; 95 | letter-spacing: 3px; 96 | } 97 | 98 | .homepage__scan-link { 99 | margin-top: 4vh; 100 | margin-bottom: 14vh; 101 | letter-spacing: 0.5px; 102 | 103 | a { 104 | color: inherit; 105 | 106 | span { 107 | border-bottom: 1px dashed $raven; 108 | } 109 | 110 | sup { 111 | color: $cornflower-blue; 112 | } 113 | } 114 | 115 | @media screen and (max-width: 40em) { 116 | margin-bottom: 20vh; 117 | } 118 | } 119 | 120 | @for $i from 1 through 3 { 121 | .logo__skeleton:nth-of-type(#{$i}) { 122 | animation-delay: 0.2 * $i * 1s; 123 | } 124 | } 125 | 126 | @keyframes move { 127 | 0% { 128 | transform: translate(1px, 0.5px); 129 | } 130 | 131 | 20% { 132 | transform: translate(0px, -1px); 133 | } 134 | 135 | 75% { 136 | transform: translate(-1px, 1px); 137 | } 138 | 139 | 100% { 140 | transform: translate(0.55px, -1px); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /pages/package/[...packageString]/components/ExportAnalysisSection/index.js: -------------------------------------------------------------------------------- 1 | import ExportAnalysisSection from './ExportAnalysisSection' 2 | 3 | export default ExportAnalysisSection 4 | -------------------------------------------------------------------------------- /pages/package/[...packageString]/components/InterLinksSection/InterLinksSection.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { 3 | parsePackageString, 4 | daysFromToday, 5 | } from '../../../../../utils/common.utils' 6 | import API from '../../../../../client/api' 7 | import InterLinksSectionCard from './InterLinksSectionCard' 8 | 9 | function usePackagesFromSameScope(packageName) { 10 | const { scope } = parsePackageString(packageName) 11 | 12 | const [morePackages, setMorePackages] = useState([]) 13 | const getAgeScore = result => 14 | Math.min(1 / Math.log(daysFromToday(result.package.date)), 1) 15 | 16 | useEffect(() => { 17 | API.getSuggestions(`@${scope}`).then(results => { 18 | const sorted = results 19 | .filter(result => result.package.scope === scope) 20 | .filter(result => result.package.name !== packageName) 21 | .sort( 22 | (rA, rB) => 23 | rB.score.detail.popularity * getAgeScore(rB) - 24 | rA.score.detail.popularity * getAgeScore(rA) 25 | ) 26 | setMorePackages(sorted) 27 | }) 28 | }, [packageName]) 29 | return morePackages 30 | } 31 | 32 | const InterLinksSection = props => { 33 | const { scope } = parsePackageString(props.packageName) 34 | const morePackages = usePackagesFromSameScope(props.packageName) 35 | 36 | if (!morePackages.length) { 37 | return null 38 | } 39 | 40 | return ( 41 |
42 |
43 |

44 | More  {scope}  packages 45 |

46 |
47 | {morePackages.map(pack => ( 48 | 54 | ))} 55 |
56 |
57 |
58 | ) 59 | } 60 | 61 | export default InterLinksSection 62 | -------------------------------------------------------------------------------- /pages/package/[...packageString]/components/InterLinksSection/InterLinksSection.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../stylesheets/variables'; 2 | @import '../../../../../stylesheets/mixins'; 3 | 4 | .interlinks-section { 5 | width: 100%; 6 | } 7 | 8 | .interlinks-section__list { 9 | @include horizontal-scroll-overflow-indicators; 10 | @include hide-scrollbars; 11 | 12 | display: flex; 13 | margin-left: -$global-spacing; 14 | margin-right: -$global-spacing; 15 | white-space: nowrap; 16 | overflow-x: scroll; 17 | } 18 | -------------------------------------------------------------------------------- /pages/package/[...packageString]/components/InterLinksSection/InterLinksSectionCard/InterLinksSectionCard.js: -------------------------------------------------------------------------------- 1 | import { sanitizeHTML } from '../../../../../../utils/common.utils' 2 | import Link from 'next/link' 3 | import { formatDistanceToNow } from 'date-fns' 4 | import React from 'react' 5 | 6 | export default function InterLinksSectionCard(props) { 7 | const { description, name, date } = props 8 | 9 | return ( 10 | 11 |
12 |
13 |

{name}

14 |
15 |

21 |

22 | published {formatDistanceToNow(new Date(date), { addSuffix: true })} 23 |
24 |
25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /pages/package/[...packageString]/components/InterLinksSection/InterLinksSectionCard/InterLinksSectionCard.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | @import '../../../../../../stylesheets/colors'; 4 | @import '../../../../../../stylesheets/variables'; 5 | 6 | .interlinks-card { 7 | border: 1px solid $autocomplete-border-color; 8 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); 9 | border-radius: 10px; 10 | width: calc(22% - #{$global-spacing * 2}); 11 | margin: $global-spacing; 12 | color: inherit; 13 | transition: all 0.2s; 14 | flex: 1 0 auto; 15 | white-space: normal; 16 | 17 | &:hover { 18 | transform: scale(1.03); 19 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.06); 20 | border: 1px solid fade-in($autocomplete-border-color, 0.02); 21 | background: rgba(200, 200, 200, 0.07); 22 | } 23 | 24 | @media screen and (max-width: 64em) { 25 | margin: math.div($global-spacing, 1.5); 26 | width: calc(30% - #{$global-spacing * 2}); 27 | } 28 | 29 | @media screen and (max-width: 48em) { 30 | margin: math.div($global-spacing, 1.5); 31 | width: calc(45% - #{$global-spacing * 1.5}); 32 | } 33 | 34 | @media screen and (max-width: 40em) { 35 | width: 85%; 36 | } 37 | } 38 | 39 | .interlinks-card__wrap { 40 | padding: $global-spacing * 1.5; 41 | padding-bottom: math.div($global-spacing, 1.5); 42 | flex-grow: 1; 43 | display: flex; 44 | flex-direction: column; 45 | height: 100%; 46 | } 47 | 48 | .interlinks-card__header { 49 | display: flex; 50 | } 51 | 52 | .interlinks-card__name { 53 | margin: 0; 54 | font-family: $font-family-code; 55 | font-weight: $font-weight-light; 56 | flex-grow: 1; 57 | word-break: break-word; 58 | } 59 | 60 | .interlinks-card__description { 61 | @include font-size-sm; 62 | color: $dark-raven; 63 | line-height: 1.5; 64 | margin: $global-spacing 0 0 0; 65 | word-break: break-word; 66 | flex-grow: 1; 67 | 68 | img { 69 | height: auto; 70 | max-width: 100%; 71 | } 72 | } 73 | 74 | .interlinks-card__publish-date { 75 | @include font-size-xs; 76 | color: $raven; 77 | border-top: 1px dashed lighten($raven, 50%); 78 | padding-top: $global-spacing * 0.5; 79 | margin-top: $global-spacing * 0.5; 80 | } 81 | -------------------------------------------------------------------------------- /pages/package/[...packageString]/components/InterLinksSection/InterLinksSectionCard/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './InterLinksSectionCard' 2 | -------------------------------------------------------------------------------- /pages/package/[...packageString]/components/InterLinksSection/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './InterLinksSection' 2 | -------------------------------------------------------------------------------- /pages/package/[...packageString]/components/SimilarPackagesSection/SimilarPackagesSection.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import SimilarPackageCard from '../../../../../client/components/SimilarPackageCard/SimilarPackageCard' 3 | 4 | class SimilarPackagesSection extends Component { 5 | render() { 6 | const { packs, category, comparisonGzip } = this.props 7 | return ( 8 |
9 |

10 | {' '} 11 | Similar Packages{' '} 12 |

13 |
{category}
14 | 15 |
16 | {packs.map(pack => ( 17 | 24 | ))} 25 | 26 |
27 |
28 | ) 29 | } 30 | } 31 | 32 | export default SimilarPackagesSection 33 | -------------------------------------------------------------------------------- /pages/package/[...packageString]/components/SimilarPackagesSection/SimilarPackagesSection.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../stylesheets/variables'; 2 | @import '../../../../../stylesheets/colors'; 3 | 4 | .similar-packages-section__list { 5 | display: flex; 6 | flex-wrap: wrap; 7 | margin-left: -$global-spacing; 8 | margin-right: -$global-spacing; 9 | } 10 | 11 | .similar-packages-section__heading { 12 | margin-bottom: 0; 13 | } 14 | 15 | .similar-packages-section__subheading { 16 | color: lighten($raven, 15%); 17 | text-align: center; 18 | margin-top: $global-spacing; 19 | } 20 | -------------------------------------------------------------------------------- /pages/package/[...packageString]/components/SimilarPackagesSection/index.js: -------------------------------------------------------------------------------- 1 | import SimilarPackagesSection from './SimilarPackagesSection' 2 | 3 | export default SimilarPackagesSection 4 | -------------------------------------------------------------------------------- /pages/package/[...packageString]/index.page.js: -------------------------------------------------------------------------------- 1 | import ResultPage from './ResultPage' 2 | export { getServerSideProps } from './ResultPage' 3 | export default ResultPage 4 | -------------------------------------------------------------------------------- /pages/scan-results/index.page.js: -------------------------------------------------------------------------------- 1 | import ScanResults from './ScanResults' 2 | 3 | export default ScanResults 4 | -------------------------------------------------------------------------------- /pages/scan/Scan.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | @import '../../stylesheets/base'; 4 | @import '../../stylesheets/variables'; 5 | 6 | .scan__dropzone { 7 | border: 2px dashed lighten($raven, 20%); 8 | width: 50vw; 9 | height: 50vh; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | flex-direction: column; 14 | 15 | p { 16 | margin-top: 0; 17 | } 18 | } 19 | 20 | .scan__btn { 21 | @include font-size-xs; 22 | cursor: pointer; 23 | margin-top: $global-spacing; 24 | background: #212121; 25 | border-radius: 6px; 26 | border: none; 27 | padding: $global-spacing $global-spacing * 2; 28 | display: block; 29 | transition: background 0.2s; 30 | color: white; 31 | letter-spacing: 1px; 32 | font-weight: $font-weight-bold; 33 | font-family: $font-family-body; 34 | 35 | &:hover { 36 | background: $raven; 37 | } 38 | 39 | & ~ & { 40 | margin-left: $global-spacing * 1.5; 41 | } 42 | } 43 | 44 | .scan__package-container { 45 | list-style: none; 46 | columns: 3; 47 | 48 | @media screen and (max-width: 48em) { 49 | columns: 2; 50 | } 51 | 52 | @media screen and (max-width: 40em) { 53 | columns: 1; 54 | } 55 | } 56 | 57 | .scan__package-item-title { 58 | opacity: 0.5; 59 | } 60 | 61 | .scan__package-item { 62 | padding: math.div($global-spacing, 2.5); 63 | 64 | input { 65 | margin-right: $global-spacing; 66 | 67 | &:checked ~ .scan__package-item-title { 68 | opacity: 1; 69 | } 70 | } 71 | 72 | label { 73 | cursor: pointer; 74 | } 75 | } 76 | 77 | .scan__package-item-version { 78 | @include font-size-xs; 79 | font-family: $font-family-code; 80 | margin-left: $global-spacing; 81 | color: lighten($raven, 10%); 82 | } 83 | 84 | .scan__selection-header { 85 | display: flex; 86 | padding: 0 $global-spacing * 2; 87 | align-items: center; 88 | margin-bottom: $global-spacing * 2; 89 | 90 | .scan__page-title { 91 | margin: 0 $global-spacing * 4 0 0; 92 | } 93 | 94 | .scan__btn { 95 | @include font-size-xxs; 96 | text-transform: uppercase; 97 | padding: $global-spacing $global-spacing * 2; 98 | border: 2px solid #212121; 99 | background: white; 100 | color: #212121; 101 | 102 | &:hover { 103 | background: #212121; 104 | color: white; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /pages/scan/index.page.js: -------------------------------------------------------------------------------- 1 | import Scan from './Scan' 2 | export { getServerSideProps } from './Scan' 3 | export default Scan 4 | -------------------------------------------------------------------------------- /process.yml: -------------------------------------------------------------------------------- 1 | apps: 2 | - script: './index.js' 3 | args: '--project tsconfig.server.json ./index.ts' 4 | name: main 5 | instances: 1 6 | max_memory_restart: 500M 7 | error_file: logs/index.log 8 | out_file: logs/index.log 9 | kill_timeout: 20000 10 | exec_mode: cluster 11 | env: 12 | NODE_ENV: production 13 | DEBUG: 'bp:*' 14 | PORT: 5400 15 | 16 | - script: ./build-service/index.js 17 | name: build-service 18 | instances: 3 19 | max_memory_restart: 1000M 20 | exec_mode: cluster 21 | error_file: logs/build-service.log 22 | out_file: logs/build-service.log 23 | kill_timeout: 20000 24 | env: 25 | NODE_ENV: production 26 | DEBUG: 'bp:*' 27 | 28 | - script: ./cache-service/index.js 29 | name: cache-service 30 | instances: 1 31 | max_memory_restart: 200M 32 | exec_mode: cluster 33 | kill_timeout: 5000 34 | error_file: logs/cache-service.log 35 | out_file: logs/cache-service.log 36 | env: 37 | NODE_ENV: production 38 | DEBUG: 'bp:*' 39 | -------------------------------------------------------------------------------- /server/CustomError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wraps the original error with a identifiable 3 | * name. 4 | */ 5 | // Use ES6 supported by Node v6.10 only! 6 | 7 | module.exports = function CustomError(name, originalError, extra) { 8 | Error.captureStackTrace(this, this.constructor) 9 | this.name = name 10 | this.originalError = originalError 11 | this.extra = extra 12 | } 13 | -------------------------------------------------------------------------------- /server/Logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston') 2 | // const StatsD = require('hot-shots') 3 | // const WinstonGraylog2 = require('winston-graylog2') 4 | // 5 | // const statsClient = new StatsD({ 6 | // port: 8086, 7 | // globalTags: { env: process.env.NODE_ENV }, 8 | // errorHandler: (err, a) => console.error('error', err, a), 9 | // telegraf: true, 10 | // }) 11 | // 12 | // const graylogOptions = { 13 | // name: 'graylog', 14 | // level: 'debug', 15 | // silent: false, 16 | // handleExceptions: false, 17 | // graylog: { 18 | // servers: [ 19 | // { host: process.env.GRAYLOG_HOST, port: process.env.GRAYLOG_PORT }, 20 | // ], 21 | // hostname: 'bundlephobia', 22 | // bufferSize: 1400, 23 | // }, 24 | // } 25 | 26 | const logFormat = winston.format.printf(function (info) { 27 | let date = new Date().toISOString() 28 | return `${date} ${info.level}: ${info.message}` 29 | }) 30 | 31 | class Logger { 32 | constructor() { 33 | let transports = [ 34 | new winston.transports.Console({ 35 | format: winston.format.combine(winston.format.colorize(), logFormat), 36 | }), 37 | ] 38 | 39 | this.logger = winston.createLogger({ 40 | transports, 41 | }) 42 | } 43 | 44 | info(tag, json, message) { 45 | this.logger.info(message, { 46 | metadata: { 47 | message, 48 | tag, 49 | ...json, 50 | }, 51 | }) 52 | } 53 | 54 | error(tag, json, message) { 55 | this.logger.error(message, { 56 | metadata: { 57 | tag, 58 | ...json, 59 | }, 60 | }) 61 | } 62 | 63 | // statsd methods 64 | 65 | increment(label) { 66 | // statsClient.increment(label) 67 | } 68 | 69 | decrement(label) { 70 | // statsClient.increment(label) 71 | } 72 | 73 | histogram(label, value) { 74 | // statsClient.histogram(label, value) 75 | } 76 | 77 | set(label, value) { 78 | // statsClient.set(label, value) 79 | } 80 | 81 | timing(label, value) { 82 | // statsClient.timing(label, value) 83 | } 84 | } 85 | 86 | module.exports = new Logger() 87 | -------------------------------------------------------------------------------- /server/api/BuildService.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const { requestQueue } = require('../init') 3 | const CONFIG = require('../config') 4 | const { pool } = require('../init') 5 | const CustomError = require('../CustomError') 6 | const debug = require('debug')('bp:build') 7 | 8 | const OpertationType = { 9 | PACKAGE_BUILD_STATS: 'PACKAGE_BUILD_STATS', 10 | PACKAGE_EXPORTS: 'PACKAGE_EXPORTS', 11 | PACKAGE_EXPORTS_SIZES: 'PACKAGE_EXPORTS_SIZES', 12 | } 13 | 14 | class BuildService { 15 | constructor() { 16 | const operations = [ 17 | { 18 | type: OpertationType.PACKAGE_BUILD_STATS, 19 | endpoint: '/size', 20 | methodName: 'getPackageStats', 21 | }, 22 | { 23 | type: OpertationType.PACKAGE_EXPORTS, 24 | endpoint: '/exports', 25 | methodName: 'getAllPackageExports', 26 | }, 27 | { 28 | type: OpertationType.PACKAGE_EXPORTS_SIZES, 29 | endpoint: '/exports-sizes', 30 | methodName: 'getPackageExportSizes', 31 | }, 32 | ] 33 | 34 | operations.forEach(operation => { 35 | requestQueue.addExecutor(operation.type, async ({ packageString }) => { 36 | if (process.env.BUILD_SERVICE_ENDPOINT) { 37 | try { 38 | const response = await axios.get( 39 | `${process.env.BUILD_SERVICE_ENDPOINT}${ 40 | operation.endpoint 41 | }?p=${encodeURIComponent(packageString)}` 42 | ) 43 | return response.data 44 | } catch (error) { 45 | this._handleError(error, operation.type) 46 | } 47 | } else { 48 | return await pool 49 | .exec(operation.methodName, [packageString]) 50 | .timeout(CONFIG.WORKER_TIMEOUT) 51 | } 52 | }) 53 | }) 54 | } 55 | 56 | _handleError(error, operationType) { 57 | if (error.response) { 58 | // The request was made and the server responded with a status code 59 | // that falls out of the range of 2xx 60 | const contents = error.response.data 61 | throw new CustomError( 62 | contents.name || 'BuildError', 63 | contents.originalError, 64 | contents.extra 65 | ) 66 | } else if (error.request) { 67 | // The request was made but no response was received 68 | // `error.request` is an instance of XMLHttpRequest in the browser and an instance of 69 | // http.ClientRequest in node.js 70 | debug('No response received from build server. Is the server down?') 71 | throw new CustomError('BuildError', { 72 | operation: operationType, 73 | reason: 'BUILD_SERVICE_UNREACHABLE', 74 | url: error.request._currentUrl, 75 | }) 76 | } else { 77 | // Something happened in setting up the request that triggered an Error 78 | throw new CustomError('BuildError', error.message, { 79 | operation: operationType, 80 | }) 81 | } 82 | } 83 | 84 | async getPackageBuildStats(packageString, priority) { 85 | return await requestQueue.process( 86 | packageString, 87 | OpertationType.PACKAGE_BUILD_STATS, 88 | { packageString }, 89 | { priority } 90 | ) 91 | } 92 | 93 | async getPackageExports(packageString, priority) { 94 | return await requestQueue.process( 95 | packageString, 96 | OpertationType.PACKAGE_EXPORTS, 97 | { packageString }, 98 | { priority } 99 | ) 100 | } 101 | 102 | async getPackageExportSizes(packageString, priority) { 103 | return await requestQueue.process( 104 | packageString, 105 | OpertationType.PACKAGE_EXPORTS_SIZES, 106 | { packageString }, 107 | { priority } 108 | ) 109 | } 110 | } 111 | 112 | module.exports = BuildService 113 | -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | // Use ES6 supported by Node v6.10 only! 2 | 3 | const path = require('path') 4 | const dev = false //process.env.NODE_ENV === 'development' 5 | 6 | module.exports = { 7 | tmp: path.join(__dirname, '..', 'tmp-build'), 8 | 9 | MAX_WORKERS: require('os').cpus().length, 10 | 11 | MAX_FAILURE_CACHE_ENTRIES: 600, 12 | 13 | WORKER_TIMEOUT: 600 * 1000, //ms, 14 | 15 | DEFAULT_DEV_PORT: 5000, 16 | 17 | blackList: [ 18 | /hack-cheats/, 19 | /hacks?-cheats?/, 20 | /hack-unlimited/, 21 | /generator-unlimited/, 22 | /hack-\d+/, 23 | /cheat-\d+/, 24 | /-hacks?-/, 25 | /^nuxt$/, 26 | /^next$/, 27 | /^react-scripts/, 28 | /^polymer-cli/, 29 | /^parcel$/, 30 | /^devextreme$/, 31 | /^yarn$/, 32 | 33 | // big packages, that fail often 34 | /^styled-icons$/, 35 | /^razzle$/, 36 | ], 37 | 38 | unsupported: [ 39 | { 40 | test: /^@types\//, 41 | reason: "Type packages don't usually contain any runtime code.", 42 | }, 43 | ], 44 | 45 | CACHE: { 46 | PUBLIC_ASSETS: dev ? 0 : 24 * 60 * 60, 47 | RECENTS_API: dev ? 0 : 20 * 60, 48 | PACKAGE_HISTORY_API: dev ? 0 : 60 * 60, 49 | SIMILAR_API: dev ? 0 : 60 * 60 * 2, 50 | SIZE_API_DEFAULT: dev ? 0 : 30, 51 | SIZE_API_ERROR: dev ? 0 : 60, 52 | SIZE_API_ERROR_FATAL: dev ? 0 : 60 * 60, 53 | SIZE_API_ERROR_UNSUPPORTED: dev ? 0 : 24 * 60 * 60, 54 | SIZE_API_HAS_VERSION: dev ? 0 : 24 * 60 * 60, 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /server/data/similar-packages/date-time.js: -------------------------------------------------------------------------------- 1 | // Ununsed file 2 | 3 | export default [[{ name: 'moment' }, { name: 'luxon' }, { name: 'date-fns' }]] 4 | -------------------------------------------------------------------------------- /server/data/similar-packages/index.js: -------------------------------------------------------------------------------- 1 | // Ununsed file 2 | 3 | import dateTimeList from './date-time' 4 | import markdownList from './markdown' 5 | import storageList from './storage' 6 | 7 | export default [...dateTimeList, ...markdownList, ...storageList] 8 | -------------------------------------------------------------------------------- /server/data/similar-packages/markdown.js: -------------------------------------------------------------------------------- 1 | // Ununsed file 2 | -------------------------------------------------------------------------------- /server/data/similar-packages/storage.js: -------------------------------------------------------------------------------- 1 | // Ununsed file 2 | -------------------------------------------------------------------------------- /server/init.js: -------------------------------------------------------------------------------- 1 | const config = require('./config') 2 | const LRU = require('lru-cache') 3 | const workerpool = require('workerpool') 4 | const Queue = require('./Queue') 5 | const logger = require('./Logger') 6 | 7 | const failureCache = new LRU({ 8 | max: config.MAX_FAILURE_CACHE_ENTRIES, 9 | maxAge: 6 * 1000 * 60 * 60, 10 | }) 11 | 12 | const debug = require('debug')('bp:request') 13 | 14 | const requestQueue = new Queue({ 15 | concurrency: 4, 16 | maxAge: 60 * 2, 17 | }) 18 | 19 | const pool = workerpool.pool(`./server/worker.js`, { 20 | maxWorkers: config.MAX_WORKERS, 21 | }) 22 | 23 | if (process.env.BUILD_SERVICE_ENDPOINT) { 24 | pool.terminate() 25 | } 26 | 27 | module.exports = { 28 | failureCache, 29 | requestQueue, 30 | pool, 31 | debug, 32 | logger, 33 | } 34 | -------------------------------------------------------------------------------- /server/middlewares/exports.middleware.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver') 2 | const CONFIG = require('../config') 3 | const now = require('performance-now') 4 | const logger = require('../Logger') 5 | const { getRequestPriority } = require('../../utils/server.utils') 6 | const { parsePackageString } = require('../../utils/common.utils') 7 | const BuildService = require('../api/BuildService') 8 | 9 | const buildService = new BuildService() 10 | 11 | async function exportsMiddleware(ctx) { 12 | let result, 13 | priority = getRequestPriority(ctx) 14 | const { name, version, packageString } = ctx.state.resolved 15 | const { force, package: packageQuery } = ctx.query 16 | 17 | const buildStart = now() 18 | result = await buildService.getPackageExports(packageString, priority) 19 | 20 | const buildEnd = now() 21 | 22 | ctx.cacheControl = { 23 | maxAge: force 24 | ? 0 25 | : semver.valid(parsePackageString(packageQuery).version) 26 | ? CONFIG.CACHE.SIZE_API_HAS_VERSION 27 | : CONFIG.CACHE.SIZE_API_DEFAULT, 28 | } 29 | ctx.body = { name, version, exports: result } 30 | const time = buildEnd - buildStart 31 | 32 | logger.info( 33 | 'BUILD_EXPORTS', 34 | { 35 | result, 36 | requestId: ctx.state.id, 37 | packageString, 38 | time, 39 | }, 40 | `BUILD EXPORTS: ${packageString} built in ${time.toFixed()}s` 41 | ) 42 | } 43 | 44 | module.exports = exportsMiddleware 45 | -------------------------------------------------------------------------------- /server/middlewares/exportsSizes.middleware.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver') 2 | const CONFIG = require('../config') 3 | const now = require('performance-now') 4 | const logger = require('../Logger') 5 | const Cache = require('../../utils/cache.utils') 6 | const { getRequestPriority } = require('../../utils/server.utils') 7 | const { parsePackageString } = require('../../utils/common.utils') 8 | const BuildService = require('../api/BuildService') 9 | 10 | const cache = new Cache() 11 | const buildService = new BuildService() 12 | 13 | async function exportSizesMiddleware(ctx) { 14 | let result, 15 | priority = getRequestPriority(ctx) 16 | const { name, version, packageString } = ctx.state.resolved 17 | const { force, peek, package: packageQuery } = ctx.query 18 | 19 | if (peek) { 20 | return { name, version, peekSuccess: false } 21 | } 22 | 23 | const buildStart = now() 24 | result = await buildService.getPackageExportSizes(packageString, priority) 25 | 26 | const buildEnd = now() 27 | 28 | ctx.cacheControl = { 29 | maxAge: force 30 | ? 0 31 | : semver.valid(parsePackageString(packageQuery).version) 32 | ? CONFIG.CACHE.SIZE_API_HAS_VERSION 33 | : CONFIG.CACHE.SIZE_API_DEFAULT, 34 | } 35 | 36 | const body = { name, version, ...result } 37 | ctx.body = body 38 | const time = buildEnd - buildStart 39 | 40 | logger.info( 41 | 'BUILD_EXPORTS_SIZES', 42 | { 43 | result, 44 | requestId: ctx.state.id, 45 | packageString, 46 | time, 47 | }, 48 | `BUILD EXPORTS SIZES: ${packageString} built in ${time.toFixed()}s` 49 | ) 50 | 51 | if (force === 'true') { 52 | cache.setExportsSize({ name, version }, body) 53 | } 54 | } 55 | 56 | module.exports = exportSizesMiddleware 57 | -------------------------------------------------------------------------------- /server/middlewares/generateImg.middleware.js: -------------------------------------------------------------------------------- 1 | const { drawStatsImg } = require('../../utils/draw.utils') 2 | const Cache = require('../../utils/cache.utils') 3 | const send = require('koa-send') 4 | const path = require('path') 5 | const queryString = require('query-string') 6 | const { resolvePackage } = require('../../utils/server.utils') 7 | const semver = require('semver') 8 | 9 | const cache = new Cache() 10 | 11 | async function generateImgMiddleware(ctx, next) { 12 | // See https://github.com/facebook/react/issues/13838 13 | const url = ctx.url.replace(/&/g, '&') 14 | 15 | let { name, version, theme, wide } = queryString.parseUrl(url).query 16 | 17 | try { 18 | if (!semver.valid(version)) { 19 | const resolved = await resolvePackage(name) 20 | version = resolved.version 21 | } 22 | 23 | const result = await cache.getPackageSize({ name, version }) 24 | 25 | ctx.type = 'png' 26 | ctx.cacheControl = { 27 | maxAge: 60 * 60 * 60, 28 | } 29 | ctx.body = drawStatsImg({ 30 | name: result.name, 31 | version: result.version, 32 | min: result.size, 33 | gzip: result.gzip, 34 | theme, 35 | wide, 36 | }) 37 | } catch (err) { 38 | console.error(err) 39 | ctx.cacheControl = { 40 | noCache: true, 41 | } 42 | await send(ctx, 'client/assets/public/android-chrome-192x192.png') 43 | } 44 | } 45 | 46 | module.exports = generateImgMiddleware 47 | -------------------------------------------------------------------------------- /server/middlewares/jsonCache.middleware.js: -------------------------------------------------------------------------------- 1 | const koaCache = require('koa-cash') 2 | 3 | function jsonCacheMiddleware({ get, set, hash: hashFn }) { 4 | return koaCache({ 5 | async get(key) { 6 | // Emulate koa-cash cache value 7 | const value = await get(key) 8 | return { 9 | body: value, 10 | type: 'application/json', 11 | } 12 | }, 13 | set(key, value) { 14 | // We only need the body part from what 15 | // koa-cash gives us 16 | set(key, JSON.parse(value.body)) 17 | }, 18 | hash(ctx) { 19 | return hashFn(ctx) 20 | }, 21 | }) 22 | } 23 | 24 | module.exports = jsonCacheMiddleware 25 | -------------------------------------------------------------------------------- /server/middlewares/rateLimit.middleware.js: -------------------------------------------------------------------------------- 1 | // Modified package `koa-better-ratelimit` 2 | // to accept original client ips from cloudflare 3 | 4 | const ipchecker = require('ipchecker') 5 | const defaults = { 6 | duration: 1000 * 60 * 60, 7 | whiteList: [], 8 | blackList: [], 9 | accessLimited: '429: Too Many Requests.', 10 | accessForbidden: '403: This is forbidden area for you.', 11 | max: 100, 12 | env: null, 13 | } 14 | 15 | /** 16 | * With options through init you can control 17 | * black/white lists, limit per ip and reset interval. 18 | * 19 | * @param {Object} options 20 | * @api public 21 | */ 22 | module.exports = function betterlimit(options = {}) { 23 | const db = {} 24 | 25 | for (const key in defaults) { 26 | if (!options[key]) { 27 | options[key] = defaults[key] 28 | } 29 | } 30 | 31 | if (options.message_429) { 32 | options.accessLimited = options.message_429 33 | } 34 | 35 | if (options.message_403) { 36 | options.accessForbidden = options.message_403 37 | } 38 | 39 | const whiteListMap = ipchecker.map(options.whiteList) 40 | const blackListMap = ipchecker.map(options.blackList) 41 | 42 | return function* ratelimit(next) { 43 | const ip = 44 | this.request.header['x-koaip'] || 45 | this.request.header['cf-connecting-ip'] || 46 | this.ip 47 | 48 | if (!ip) { 49 | return yield* next 50 | } 51 | if (ipchecker.check(ip, blackListMap)) { 52 | this.response.status = 403 53 | this.response.body = options.accessForbidden 54 | return 55 | } 56 | if (ipchecker.check(ip, whiteListMap)) { 57 | return yield* next 58 | } 59 | 60 | const now = Date.now() 61 | const reset = now + options.duration 62 | 63 | if (!db.hasOwnProperty(ip)) { 64 | db[ip] = { ip, reset, limit: options.max } 65 | } 66 | 67 | const delta = db[ip].reset - now 68 | const retryAfter = (delta / 1000) | 0 69 | 70 | db[ip].limit = db[ip].limit - 1 71 | this.response.set('X-RateLimit-Limit', options.max) 72 | 73 | if (db[ip].reset > now) { 74 | const rateLimiting = db[ip].limit < 0 ? 0 : db[ip].limit 75 | this.response.set('X-RateLimit-Remaining', rateLimiting) 76 | } 77 | 78 | if (db[ip].limit < 0 && db[ip].reset < now) { 79 | db[ip] = { ip, reset, limit: options.max } 80 | db[ip].limit = db[ip].limit - 1 81 | this.response.set('X-RateLimit-Remaining', db[ip].limit) 82 | } 83 | 84 | this.response.set('X-RateLimit-Reset', db[ip].reset) 85 | 86 | if (db[ip].limit < 0) { 87 | this.response.set('Retry-After', retryAfter) 88 | this.response.status = 429 89 | this.response.body = options.accessLimited 90 | return 91 | } 92 | 93 | return yield* next 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /server/middlewares/requestLogger.middleware.js: -------------------------------------------------------------------------------- 1 | const logger = require('../Logger') 2 | const now = require('performance-now') 3 | 4 | async function requestLoggerMiddleware(ctx, next) { 5 | if (!ctx.request.url.includes('/api/')) { 6 | await next() 7 | return 8 | } 9 | 10 | const requestStart = now() 11 | await next() 12 | const requestEnd = now() 13 | const time = requestEnd - requestStart 14 | 15 | logger.info( 16 | 'REQUEST', 17 | { 18 | url: ctx.request.url, 19 | type: ctx.request.type, 20 | query: ctx.request.query, 21 | headers: ctx.request.headers, 22 | ip: 23 | ctx.request.header['x-koaip'] || 24 | ctx.request.header['cf-connecting-ip'] || 25 | ctx.ip, 26 | requestId: ctx.state.id, 27 | method: ctx.request.method, 28 | origin: ctx.request.origin, 29 | hostname: ctx.request.hostname, 30 | status: ctx.response.status, 31 | time, 32 | }, 33 | `REQUEST: ${ctx.response.status} ${(time / 1000).toFixed(2)}s ${ 34 | ctx.req.method 35 | } ${ctx.request.url}` 36 | ) 37 | } 38 | 39 | module.exports = requestLoggerMiddleware 40 | -------------------------------------------------------------------------------- /server/middlewares/results/blockBlacklist.middleware.js: -------------------------------------------------------------------------------- 1 | const { parsePackageString } = require('../../../utils/common.utils') 2 | const CustomError = require('./../../CustomError') 3 | const CONFIG = require('../../config') 4 | 5 | async function blockBlacklistMiddleware(ctx, next) { 6 | const { package: packageString, force } = ctx.query 7 | if (force) { 8 | await next() 9 | return 10 | } 11 | 12 | const parsedPackage = parsePackageString(packageString) 13 | 14 | // If package is blacklisted, fail fast 15 | if (CONFIG.blackList.some(entry => entry.test(parsedPackage.name))) { 16 | throw new CustomError('BlocklistedPackageError', { ...parsedPackage }) 17 | } 18 | 19 | // If package is unsupported, fail fast 20 | const matchedUnsupportedRule = CONFIG.unsupported.find(rule => 21 | new RegExp(rule.test).test(parsedPackage.name) 22 | ) 23 | if (matchedUnsupportedRule) { 24 | throw new CustomError( 25 | 'UnsupportedPackageError', 26 | { ...parsedPackage }, 27 | { reason: matchedUnsupportedRule.reason } 28 | ) 29 | } 30 | 31 | await next() 32 | } 33 | 34 | module.exports = blockBlacklistMiddleware 35 | -------------------------------------------------------------------------------- /server/middlewares/results/build.middleware.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver') 2 | const CONFIG = require('../../config') 3 | const firebaseUtils = require('../../../utils/firebase.utils') 4 | const now = require('performance-now') 5 | const logger = require('../../Logger') 6 | const Cache = require('../../../utils/cache.utils') 7 | const BuildService = require('../../api/BuildService') 8 | const { getRequestPriority } = require('../../../utils/server.utils') 9 | const { parsePackageString } = require('../../../utils/common.utils') 10 | 11 | const cache = new Cache() 12 | const buildService = new BuildService() 13 | 14 | async function buildMiddleware(ctx, next) { 15 | let result, 16 | priority = getRequestPriority(ctx) 17 | const { scoped, name, version, description, repository, packageString } = 18 | ctx.state.resolved 19 | const { force, record, package: packageQuery } = ctx.query 20 | 21 | const buildStart = now() 22 | result = await buildService.getPackageBuildStats(packageString, priority) 23 | const buildEnd = now() 24 | 25 | ctx.cacheControl = { 26 | maxAge: force 27 | ? 0 28 | : semver.valid(parsePackageString(packageQuery).version) 29 | ? CONFIG.CACHE.SIZE_API_HAS_VERSION 30 | : CONFIG.CACHE.SIZE_API_DEFAULT, 31 | } 32 | 33 | const body = { scoped, name, version, description, repository, ...result } 34 | ctx.body = body 35 | ctx.state.buildResult = body 36 | const time = buildEnd - buildStart 37 | 38 | logger.info( 39 | 'BUILD', 40 | { 41 | result, 42 | requestId: ctx.state.id, 43 | packageString, 44 | time, 45 | }, 46 | `BUILD: ${packageString} built in ${time.toFixed()}s and is ${ 47 | result.size 48 | } bytes` 49 | ) 50 | 51 | if (record === 'true') { 52 | firebaseUtils.setRecentSearch(name, { name, version }) 53 | } 54 | 55 | if (force === 'true') { 56 | cache.setPackageSize({ name, version }, body) 57 | } 58 | } 59 | 60 | module.exports = buildMiddleware 61 | -------------------------------------------------------------------------------- /server/middlewares/results/cachedResponse.middleware.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver') 2 | const CONFIG = require('../../config') 3 | const { failureCache, debug } = require('../../init') 4 | const logger = require('../../Logger') 5 | 6 | async function cachedResponse(ctx, next) { 7 | const { force, peep } = ctx.query 8 | if (force) { 9 | await next() 10 | return 11 | } 12 | const { name, version, packageString } = ctx.state.resolved 13 | 14 | const logCache = ({ hit, type = '', message }) => 15 | logger.info( 16 | 'CACHE', 17 | { 18 | name, 19 | version, 20 | packageString, 21 | hit, 22 | type, 23 | requestId: ctx.state.id, 24 | }, 25 | message 26 | ) 27 | 28 | const cached = await ctx.cashed() 29 | if (cached) { 30 | ctx.cacheControl = { 31 | maxAge: force 32 | ? 0 33 | : semver.valid(version) 34 | ? CONFIG.CACHE.SIZE_API_HAS_VERSION 35 | : CONFIG.CACHE.SIZE_API_DEFAULT, 36 | } 37 | 38 | logCache({ hit: true, message: `CACHE HIT: ${packageString}` }) 39 | return 40 | } 41 | 42 | const failureCacheEntry = failureCache.get(packageString) 43 | if (failureCacheEntry) { 44 | debug('fetched %s from failure cache', packageString) 45 | 46 | logCache({ 47 | hit: true, 48 | type: 'failure', 49 | message: `FAILURE CACHE HIT: ${packageString}`, 50 | }) 51 | 52 | ctx.status = failureCacheEntry.status 53 | ctx.body = failureCacheEntry.body 54 | return 55 | } 56 | 57 | logCache({ hit: false, message: `CACHE MISS: ${packageString}` }) 58 | 59 | // When peeping into the built results, 60 | // we return a 404 if a build is required. 61 | if (peep) { 62 | ctx.status = 404 63 | return 64 | } 65 | await next() 66 | } 67 | 68 | module.exports = cachedResponse 69 | -------------------------------------------------------------------------------- /server/middlewares/results/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pastelsky/bundlephobia/8e5ae109bfbd152b497c8b5f8299e33d23918303/server/middlewares/results/index.js -------------------------------------------------------------------------------- /server/middlewares/results/resolvePackage.middleware.js: -------------------------------------------------------------------------------- 1 | const { resolvePackage } = require('../../../utils/server.utils') 2 | const { parsePackageString } = require('../../../utils/common.utils') 3 | const gitURLParse = require('git-url-parse') 4 | const { debug, logger } = require('../../init') 5 | const now = require('performance-now') 6 | 7 | async function resolvePackageMiddleware(ctx, next) { 8 | const { package: packageString } = ctx.query 9 | const parsedPackage = parsePackageString(packageString) 10 | let resolvedPackage 11 | 12 | // prefill values in case resolution fails 13 | ctx.state.resolved = { 14 | ...parsedPackage, 15 | packageString: `${parsedPackage.name}@${parsedPackage.version}`, 16 | } 17 | 18 | const resolveStart = now() 19 | resolvedPackage = await resolvePackage(packageString) 20 | const resolveEnd = now() 21 | 22 | const { name, version, repository, description } = resolvedPackage 23 | let truncatedDescription = '' 24 | let repositoryURL = '' 25 | 26 | try { 27 | repositoryURL = gitURLParse(repository.url || repository).toString('https') 28 | } catch (e) { 29 | console.error('failed to parse repository url', repository) 30 | } 31 | 32 | if (description) { 33 | truncatedDescription = 34 | description.length > 300 35 | ? description.substring(0, 300) + '…' 36 | : description 37 | } 38 | 39 | const result = { 40 | name, 41 | version, 42 | scoped: parsedPackage.scoped, 43 | packageString: `${name}@${version}`, 44 | description: truncatedDescription, 45 | repository: repositoryURL, 46 | } 47 | 48 | ctx.state.resolved = result 49 | 50 | debug('resolved to %s@%s', name, version) 51 | const time = resolveEnd - resolveStart 52 | logger.info( 53 | 'RESOLVE_PACKAGE', 54 | { ...result, time, requestId: ctx.state.id }, 55 | `RESOLVED: ${result.packageString} in ${time.toFixed(0)}ms` 56 | ) 57 | 58 | await next() 59 | } 60 | 61 | module.exports = resolvePackageMiddleware 62 | -------------------------------------------------------------------------------- /server/worker.js: -------------------------------------------------------------------------------- 1 | const workerpool = require('workerpool') 2 | const { 3 | getPackageStats, 4 | getAllPackageExports, 5 | getPackageExportSizes, 6 | } = require('package-build-stats') 7 | 8 | // create a worker and register public functions 9 | workerpool.worker({ 10 | getPackageStats, 11 | getAllPackageExports, 12 | getPackageExportSizes, 13 | }) 14 | -------------------------------------------------------------------------------- /stylesheets/base.scss: -------------------------------------------------------------------------------- 1 | @import './colors'; 2 | @import './variables'; 3 | @import '../node_modules/normalize.css/normalize'; 4 | @import '../node_modules/balloon-css/balloon'; 5 | 6 | * > * { 7 | box-sizing: border-box; 8 | } 9 | 10 | body, 11 | html { 12 | background: white; 13 | font-family: $font-family-body; 14 | max-width: 100vw; 15 | overflow-x: hidden; 16 | color: #212121; 17 | } 18 | 19 | code { 20 | font-family: $font-family-code; 21 | } 22 | 23 | // reset svg value received from normalize 24 | svg:not(:root) { 25 | overflow: visible; 26 | } 27 | 28 | ::-moz-selection { 29 | background: rgba(0, 170, 255, 0.2); 30 | } 31 | 32 | ::selection { 33 | background: rgba(0, 170, 255, 0.2); 34 | } 35 | 36 | a { 37 | text-decoration: none; 38 | } 39 | 40 | // Tooltips 41 | 42 | [data-balloon]::before, 43 | [data-balloon]::after { 44 | transition-delay: 0.15s; 45 | white-space: pre; 46 | } 47 | 48 | [data-balloon]::after { 49 | background: rgba(60, 60, 60, 0.9); 50 | } 51 | 52 | [data-balloon]:hover::before { 53 | opacity: 0.82; 54 | } 55 | 56 | input { 57 | background: transparent; 58 | line-height: 1; 59 | border: 1px solid $autocomplete-border-color; 60 | transition: border-top-left-radius 0.1s, border-top-right-radius 0.1s; 61 | border-radius: 0.3em; 62 | padding: $global-spacing * 0.5 $global-spacing; 63 | 64 | &:focus { 65 | outline: none; 66 | caret-color: $pastel-green; 67 | } 68 | 69 | &::placeholder { 70 | color: lighten($raven, 20%); 71 | } 72 | } 73 | 74 | // Safari's caret color is equal to the 75 | // text color. Hence, we need to set a 76 | // text color targeting just Safari :( 77 | @media not all and (min-resolution: 0.001dpcm) { 78 | @supports (-webkit-appearance: none) { 79 | input { 80 | color: #ccc; 81 | -webkit-text-stroke: 2px white; 82 | 83 | &::placeholder { 84 | -webkit-text-stroke: 0px white; 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /stylesheets/colors.scss: -------------------------------------------------------------------------------- 1 | // Use http://www.htmlcsscolor.com to name colors 2 | $gulf-blue: #2f3f5f; 3 | $dark-gulf-blue: #152231; 4 | $neptune: #82b5b3; 5 | $cornflower-blue: #65a1f8; 6 | $maya-blue: #65c3f8; 7 | $pastel-green: #7cd690; 8 | $dandelion: #fff3cf; 9 | $carrot-orange: #eb841f; 10 | $dark-raven: #5c5c66; 11 | $raven: #666e78; 12 | 13 | $autocomplete-border-color: transparentize(black, 0.93); 14 | -------------------------------------------------------------------------------- /stylesheets/index.scss: -------------------------------------------------------------------------------- 1 | // pages 2 | @import '../pages/compare/ComparePage.scss'; 3 | @import '../pages/package/[...packageString]/components/ExportAnalysisSection/ExportAnalysisSection.scss'; 4 | @import '../pages/package/[...packageString]/components/InterLinksSection/InterLinksSectionCard/InterLinksSectionCard.scss'; 5 | @import '../pages/package/[...packageString]/components/SimilarPackagesSection/SimilarPackagesSection.scss'; 6 | @import '../pages/package/[...packageString]/components/InterLinksSection/InterLinksSection.scss'; 7 | @import '../pages/package/[...packageString]/ResultPage.scss'; 8 | @import '../pages/scan/Scan.scss'; 9 | @import '../pages/scan-results/ScanResults.scss'; 10 | @import '../pages/index.scss'; 11 | 12 | // components 13 | @import '../client/components/AutocompleteInput/AutocompleteInput.scss'; 14 | @import '../client/components/AutocompleteInputBox/AutocompleteInputBox.scss'; 15 | @import '../client/components/BarGraph/BarGraph.scss'; 16 | @import '../client/components/BarVersion/BarVersion.scss'; 17 | @import '../client/components/BlogLayout/BlogLayout.scss'; 18 | @import '../client/components/BuildProgressIndicator/BuildProgressIndicator.scss'; 19 | @import '../client/components/JumpingDots/JumpingDots.scss'; 20 | @import '../client/components/Layout/Layout.scss'; 21 | @import '../client/components/ProgressHex/ProgressHex.scss'; 22 | @import '../client/components/QuickStatsBar/QuickStatsBar.scss'; 23 | @import '../client/components/ResultLayout/ResultLayout.scss'; 24 | @import '../client/components/Icons/SideEffectIcon.scss'; 25 | @import '../client/components/Stat/Stat.scss'; 26 | @import '../client/components/SimilarPackageCard/SimilarPackageCard.scss'; 27 | @import '../client/components/Icons/TreeShakeIcon.scss'; 28 | @import '../client/components/Warning/Warning.scss'; 29 | -------------------------------------------------------------------------------- /stylesheets/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin horizontal-scroll-overflow-indicators { 2 | overflow-y: scroll; 3 | height: 100%; 4 | background-image: /* Shadows */ linear-gradient(to right, white, white), 5 | linear-gradient(to right, white, white), 6 | /* Shadow covers */ 7 | linear-gradient(to right, rgba(30, 30, 30, 0.08), rgba(255, 255, 255, 0)), 8 | linear-gradient(to left, rgba(30, 30, 30, 0.08), rgba(255, 255, 255, 0)); 9 | 10 | background-position: left center, right center, left center, right center; 11 | background-repeat: no-repeat; 12 | background-size: 2vw 100%, 2vw 100%, 1vw 100%, 1vw 100%; 13 | 14 | /* Opera doesn't support this in the shorthand */ 15 | background-attachment: local, local, scroll, scroll; 16 | } 17 | 18 | @mixin hide-scrollbars { 19 | /* this will hide the scrollbar in mozilla based browsers */ 20 | overflow: -moz-scrollbars-none; 21 | scrollbar-width: none; 22 | /* this will hide the scrollbar in internet explorers */ 23 | -ms-overflow-style: none; 24 | 25 | /* this will hide the scrollbar in webkit based browsers - safari, chrome, etc */ 26 | &::-webkit-scrollbar { 27 | width: 0 !important; 28 | display: none; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /stylesheets/variables.scss: -------------------------------------------------------------------------------- 1 | // Font related 2 | @mixin font-size-xxxl { 3 | font-size: 5.5rem; 4 | 5 | @media screen and (max-width: 48em) { 6 | font-size: 4rem; 7 | } 8 | 9 | @media screen and (max-width: 40em) { 10 | font-size: 3rem; 11 | } 12 | } 13 | 14 | @mixin font-size-xxl { 15 | font-size: 3rem; 16 | 17 | @media screen and (max-width: 48em) { 18 | font-size: 2.5rem; 19 | } 20 | 21 | @media screen and (max-width: 40em) { 22 | font-size: 2rem; 23 | } 24 | } 25 | 26 | @mixin font-size-xl { 27 | font-size: 2.4rem; 28 | 29 | @media screen and (max-width: 40em) { 30 | font-size: 1.4rem; 31 | } 32 | } 33 | 34 | @mixin font-size-lg { 35 | font-size: 2rem; 36 | 37 | @media screen and (max-width: 40em) { 38 | font-size: 1.45rem; 39 | } 40 | } 41 | 42 | @mixin font-size-md { 43 | font-size: 1.35rem; 44 | 45 | @media screen and (max-width: 48em) { 46 | font-size: 1.25rem; 47 | } 48 | 49 | @media screen and (max-width: 40em) { 50 | font-size: 1.05rem; 51 | } 52 | } 53 | 54 | @mixin font-size-md-2 { 55 | font-size: 1.7rem; 56 | 57 | @media screen and (max-width: 40em) { 58 | font-size: 1.25rem; 59 | } 60 | } 61 | 62 | @mixin font-size-reg { 63 | font-size: 1rem; 64 | 65 | @media screen and (max-width: 48em) { 66 | font-size: 0.9rem; 67 | } 68 | 69 | @media screen and (max-width: 40em) { 70 | font-size: 0.8rem; 71 | } 72 | } 73 | 74 | @mixin font-size-sm { 75 | font-size: 0.9rem; 76 | 77 | @media screen and (max-width: 48em) { 78 | font-size: 0.8rem; 79 | } 80 | 81 | @media screen and (max-width: 40em) { 82 | font-size: 0.75rem; 83 | } 84 | } 85 | 86 | @mixin font-size-xs { 87 | font-size: 0.8rem; 88 | 89 | @media screen and (max-width: 48em) { 90 | font-size: 0.75rem; 91 | } 92 | 93 | @media screen and (max-width: 40em) { 94 | font-size: 0.7rem; 95 | } 96 | } 97 | 98 | @mixin font-size-xxs { 99 | font-size: 0.7rem; 100 | 101 | @media screen and (max-width: 48em) { 102 | font-size: 0.65rem; 103 | } 104 | 105 | @media screen and (max-width: 40em) { 106 | font-size: 0.65rem; 107 | } 108 | } 109 | 110 | $font-weight-hairline: 200; 111 | $font-weight-thin: 300; 112 | $font-weight-light: 400; 113 | $font-weight-bold: 600; 114 | $font-weight-very-bold: 700; 115 | 116 | $font-family-body: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 117 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 118 | $font-family-code: 'Source Code Pro', 'SF Mono', Consolas, 'Liberation Mono', 119 | Menlo, Courier, monospace; 120 | 121 | // Padding / Spacing 122 | $global-spacing: 10px; 123 | 124 | // Content width 125 | $max-content-width: $global-spacing * 85; 126 | -------------------------------------------------------------------------------- /test-packages/blacklist-error/index.js: -------------------------------------------------------------------------------- 1 | console.log("I'm not a blacklisted package, hence will throw") 2 | -------------------------------------------------------------------------------- /test-packages/blacklist-error/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bundlephobia/test-hack-unlimited-2018", 3 | "version": "0.0.3", 4 | "license": "MIT", 5 | "private": false 6 | } 7 | -------------------------------------------------------------------------------- /test-packages/build-error/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node node 2 | console.log('This is a cli shell script') 3 | -------------------------------------------------------------------------------- /test-packages/build-error/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bundlephobia/test-build-error", 3 | "version": "0.0.2", 4 | "license": "MIT", 5 | "private": false 6 | } 7 | -------------------------------------------------------------------------------- /test-packages/entry-point-error/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bundlephobia/test-entry-point-error", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "private": false 6 | } 7 | -------------------------------------------------------------------------------- /test-packages/entry-point-error/random-js-file.js: -------------------------------------------------------------------------------- 1 | console.log("I'm not an entry point, hence will throw") 2 | -------------------------------------------------------------------------------- /test-packages/missing-dependency-error/index.js: -------------------------------------------------------------------------------- 1 | const test = require('missing-package') 2 | console.log('I have missing dependencies') 3 | -------------------------------------------------------------------------------- /test-packages/missing-dependency-error/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bundlephobia/missing-dependency-error", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "private": false 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es2017" 6 | }, 7 | "ts-node": { 8 | "swc": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /types/amplitude.d.ts: -------------------------------------------------------------------------------- 1 | // stub typings for amplitudeScript loaded in the _document 2 | declare global { 3 | var amplitude: { 4 | getInstance: () => { 5 | logEvent: (event: string, data?: Record) => void 6 | } 7 | } 8 | } 9 | 10 | export {} 11 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export type PackageInfo = { 4 | name: string 5 | description: string 6 | repository: string 7 | dependencyCount: number 8 | isTreeShakeable: boolean 9 | hasSideEffects: string[] | boolean 10 | } 11 | 12 | export type WithClassName = Pick, 'className'> 13 | -------------------------------------------------------------------------------- /types/react-contentful.d.ts: -------------------------------------------------------------------------------- 1 | import { HookQueryProps } from 'react-contentful' 2 | import { Document } from '@contentful/rich-text-types' 3 | 4 | declare module 'react-contentful' { 5 | interface Item { 6 | fields: { 7 | title: string 8 | content: Document 9 | slug: string 10 | createdAt: string 11 | } 12 | sys: { 13 | createdAt: string 14 | } 15 | } 16 | 17 | interface Data { 18 | items: Item[] 19 | } 20 | 21 | interface TypedHookResponse { 22 | data?: Data 23 | error?: any 24 | fetched: boolean 25 | loading: boolean 26 | } 27 | 28 | export function useContentful(props: HookQueryProps): TypedHookResponse 29 | } 30 | -------------------------------------------------------------------------------- /utils/cache.utils.js: -------------------------------------------------------------------------------- 1 | require('dotenv-defaults').config() 2 | const debug = require('debug')('bp:cache') 3 | const axios = require('axios') 4 | const logger = require('../server/Logger') 5 | 6 | const API = axios.create({ 7 | baseURL: process.env.CACHE_SERVICE_ENDPOINT, 8 | timeout: 5000, 9 | }) 10 | 11 | class Cache { 12 | async getPackageSize({ name, version }) { 13 | try { 14 | const result = await API.get('/package-cache', { 15 | params: { name, version }, 16 | }) 17 | return result.data 18 | } catch (err) { 19 | console.error(err.statusText) 20 | } 21 | } 22 | 23 | async setPackageSize({ name, version }, result) { 24 | debug('set package %O to %O', { name, version }, result) 25 | try { 26 | await API.post('/package-cache', { name, version, result }) 27 | } catch (err) { 28 | console.error(err.data) 29 | logger.error( 30 | 'CACHE_SET_ERROR', 31 | { 32 | name, 33 | version, 34 | error: err.data, 35 | }, 36 | `CACHE ERROR for package ${name}@${version}` 37 | ) 38 | } 39 | } 40 | 41 | async getExportsSize({ name, version }) { 42 | debug('get exports %s@%s', name, version) 43 | try { 44 | const result = await API.get('/exports-cache', { 45 | params: { name, version }, 46 | }) 47 | debug('cache hit') 48 | return result.data 49 | } catch (err) {} 50 | } 51 | 52 | async setExportsSize({ name, version }, result) { 53 | debug('set exports %O to %O', { name, version }, result) 54 | try { 55 | await API.post('/exports-cache', { name, version, result }) 56 | } catch (err) { 57 | console.error(err.data) 58 | logger.error( 59 | 'CACHE_SET_ERROR', 60 | { 61 | name, 62 | version, 63 | error: err.data, 64 | }, 65 | `CACHE ERROR for package exports ${name}@${version}` 66 | ) 67 | } 68 | } 69 | } 70 | 71 | module.exports = Cache 72 | -------------------------------------------------------------------------------- /utils/common.utils.js: -------------------------------------------------------------------------------- 1 | // Used by the server as well as the client 2 | // Use ES5 only 3 | 4 | const DOMPurify = require('dompurify') 5 | 6 | function parsePackageString(packageString) { 7 | // Scoped packages 8 | let name, 9 | version, 10 | scope, 11 | scoped = false 12 | const lastAtIndex = packageString.lastIndexOf('@') 13 | const firstSlashIndex = packageString.indexOf('/') 14 | 15 | if (packageString.startsWith('@')) { 16 | scoped = true 17 | scope = packageString.substring(1, firstSlashIndex) 18 | if (lastAtIndex === 0) { 19 | name = packageString 20 | version = null 21 | } else { 22 | name = packageString.substring(0, lastAtIndex) 23 | version = packageString.substring(lastAtIndex + 1) 24 | } 25 | } else { 26 | if (lastAtIndex === -1) { 27 | name = packageString 28 | version = null 29 | } else { 30 | name = packageString.substring(0, lastAtIndex) 31 | version = packageString.substring(lastAtIndex + 1) 32 | } 33 | } 34 | 35 | return { name, version, scope, scoped } 36 | } 37 | 38 | function daysFromToday(date) { 39 | const date1 = new Date() 40 | const date2 = new Date(date) 41 | const diffTime = Math.abs(date2 - date1) 42 | const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) 43 | return diffDays 44 | } 45 | 46 | function sanitizeHTML(html) { 47 | return DOMPurify.sanitize(html, { 48 | ALLOWED_TAGS: ['b', 'i', 'div'], 49 | ALLOWED_ATTR: [''], 50 | }) 51 | } 52 | 53 | module.exports = { parsePackageString, daysFromToday, sanitizeHTML } 54 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | // Firebase does not accept a 2 | // few special characters for keys 3 | function encodeFirebaseKey(key) { 4 | return key.replace(/[.]/g, ',').replace(/\//g, '__') 5 | } 6 | 7 | function decodeFirebaseKey(key) { 8 | return key.replace(/[,]/g, '.').replace(/__/g, '/') 9 | } 10 | 11 | const formatSize = value => { 12 | let unit, size 13 | if (Math.log10(value) < 3) { 14 | unit = 'B' 15 | size = value 16 | } else if (Math.log10(value) < 6) { 17 | unit = 'kB' 18 | size = value / 1024 19 | } else { 20 | unit = 'MB' 21 | size = value / 1024 / 1024 22 | } 23 | 24 | return { unit, size } 25 | } 26 | 27 | const formatTime = value => { 28 | let unit, size 29 | if (value < 0.0005) { 30 | unit = 'μs' 31 | size = Math.round(value * 1000000) 32 | } else if (value < 0.5) { 33 | unit = 'ms' 34 | size = Math.round(value * 1000) 35 | } else { 36 | unit = 's' 37 | size = value 38 | } 39 | 40 | return { unit, size } 41 | } 42 | 43 | // Picked up from http://www.webpagetest.org/ 44 | // Speed in KB/s 45 | 46 | const DownloadSpeed = { 47 | THREE_G: 400 / 8, // Slow 3G 48 | FOUR_G: 7000 / 8, // 4G 49 | } 50 | const getTimeFromSize = sizeInBytes => { 51 | return { 52 | threeG: sizeInBytes / 1024 / DownloadSpeed.THREE_G, 53 | fourG: sizeInBytes / 1024 / DownloadSpeed.FOUR_G, 54 | } 55 | } 56 | 57 | function randomFromArray(arr) { 58 | return arr[Math.floor(Math.random() * arr.length)] 59 | } 60 | 61 | function zeroToN(n) { 62 | return Array.from(Array(n).keys()) 63 | } 64 | 65 | function resolveBuildError(resultsError) { 66 | if (!resultsError) { 67 | return { 68 | errorName: null, 69 | errorBody: null, 70 | errorDetails: null, 71 | } 72 | } 73 | const errorName = resultsError.error 74 | ? resultsError.error.code 75 | : 'InternalServerError' 76 | const errorBody = resultsError.error 77 | ? resultsError.error.message 78 | : 'Something went wrong!' 79 | const errorDetails = 80 | resultsError.error && 81 | resultsError.error.details && 82 | resultsError.error.details.originalError 83 | ? Array.isArray(resultsError.error.details.originalError) 84 | ? resultsError.error.details.originalError[0] 85 | : resultsError.error.details.originalError.toString() 86 | : null 87 | 88 | return { 89 | errorName, 90 | errorBody, 91 | errorDetails, 92 | } 93 | } 94 | 95 | module.exports = { 96 | encodeFirebaseKey, 97 | decodeFirebaseKey, 98 | formatTime, 99 | formatSize, 100 | getTimeFromSize, 101 | randomFromArray, 102 | zeroToN, 103 | resolveBuildError, 104 | DownloadSpeed, 105 | } 106 | -------------------------------------------------------------------------------- /utils/server.utils.js: -------------------------------------------------------------------------------- 1 | require('dotenv-defaults').config() 2 | const semver = require('semver') 3 | const axios = require('axios') 4 | const fetch = require('node-fetch') 5 | const pacote = require('pacote') 6 | const Queue = require('../server/Queue') 7 | 8 | const CustomError = require('../server/CustomError') 9 | /** 10 | * Given a package string 11 | * this function resolves to a valid version and name. 12 | */ 13 | async function resolvePackage(packageString) { 14 | try { 15 | return await pacote.manifest(packageString, { fullMetadata: true }) 16 | } catch (err) { 17 | if (err.code === 'ETARGET') { 18 | throw new CustomError('PackageVersionMismatchError', null, { 19 | validVersions: Object.keys(err.distTags).concat(err.versions), 20 | }) 21 | } else { 22 | throw new CustomError('PackageNotFoundError', err) 23 | } 24 | } 25 | } 26 | 27 | function getRequestPriority(ctx) { 28 | const client = ctx.headers['x-bundlephobia-user'] 29 | 30 | switch (client) { 31 | case 'bundlephobia website': 32 | return Queue.priority.HIGH 33 | break 34 | case 'yarn website': 35 | return Queue.priority.LOW 36 | break 37 | 38 | default: 39 | return Queue.priority.MEDIUM 40 | } 41 | } 42 | 43 | module.exports = { getRequestPriority, resolvePackage } 44 | --------------------------------------------------------------------------------