├── .github └── workflows │ ├── build.yml │ └── scrape.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── scripts ├── create-gh-pages-branch.sh └── scrape.ts ├── src ├── cli.ts ├── fetch-figma-plugins-stats-async.ts ├── index.ts ├── types.ts └── utilities │ ├── create-date-table.ts │ ├── create-plugins-table.ts │ ├── fetch-async.ts │ ├── fetch-historical-plugin-stats-async.ts │ ├── fetch-live-plugins-data-async.ts │ ├── parse-data.ts │ ├── plugins-stats-keys.ts │ └── sort-comparators.ts ├── test ├── cli.ts └── fetch-live-plugins-data.ts └── tsconfig.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: 16 16 | - run: npm ci 17 | - run: npm run lint 18 | - run: npm run fix 19 | - run: npm run build 20 | - run: npm run test 21 | -------------------------------------------------------------------------------- /.github/workflows/scrape.yml: -------------------------------------------------------------------------------- 1 | name: scrape 2 | 3 | on: 4 | schedule: 5 | - cron: "0 6 * * *" 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: 16 15 | - run: npm ci 16 | - run: npm run scrape 17 | - run: | 18 | rm -rf .git 19 | git clone --single-branch --branch gh-pages --depth 1 https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git gh-pages 20 | cd gh-pages 21 | git config user.email actions@github.com 22 | git config user.name 'GitHub Actions' 23 | cp -r ../data/. . 24 | git add . 25 | git commit -m 'Scrape' 26 | git push origin gh-pages 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | data/ 4 | lib/ 5 | node_modules/ 6 | *.log 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Yuan Qing Lim 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 | > # ❌ *Deprecated* ❌ 2 | 3 | --- 4 | 5 | # Figma Plugins Stats [![npm Version](https://img.shields.io/npm/v/figma-plugins-stats?cacheSeconds=1800)](https://www.npmjs.com/package/figma-plugins-stats) [![build](https://github.com/yuanqing/figma-plugins-stats/workflows/build/badge.svg)](https://github.com/yuanqing/figma-plugins-stats/actions?query=workflow%3Abuild) 6 | 7 | > A CLI to get live and historical stats for your [Figma plugins](https://figma.com/community/explore?tab=plugins) 8 | 9 | *N.B.* Figma Plugin Stats is not official software by Figma. It relies on Figma’s internal APIs, which may break or become unavailable at any time. 10 | 11 | ## Quick start 12 | 13 | *Requires [Node.js](https://nodejs.org/).* 14 | 15 | To get the plugin stats for a particular plugin publisher, enter `npx --yes -- figma-plugins-stats` followed by a [profile handle](https://help.figma.com/hc/en-us/articles/360038510833--Create-a-Community-Profile#Creator_profiles): 16 | 17 | ``` 18 | $ npx --yes -- figma-plugins-stats yuanqing 19 | 20 | period 7d 21 | from 2022-06-25 14:04:59 UTC+8 22 | to 2022-07-03 00:14:46 UTC+8 23 | 24 | no name publisher runs installs likes views 25 | 1 Clean Document Yuan Qing Lim ▁▁▁▁█ 39,082 ↑39,082 ▆▇█▅▁ 75,926 ↑397 ▄▂█▆▁ 949 ↑10 ▄▅█▇▁ 49,777 ↑467 26 | 2 Insert Big Image Yuan Qing Lim ▁▁▁▁█ 36,468 ↑36,468 ▇██▃▁ 44,379 ↑456 ▆▆█▁▁ 471 ↑7 ▄▅█▇▁ 40,870 ↑667 27 | 3 Draw Connector Yuan Qing Lim ▁▁▁▁█ 19,737 ↑19,737 ▄▆█▄▁ 21,266 ↑302 █▄▁█▁ 353 ↑5 ▄▅█▆▁ 33,135 ↑606 28 | 4 Select Layers Yuan Qing Lim ▁▁▁▁█ 12,066 ↑12,066 ▅▇█▄▁ 16,563 ↑188 ▆▁█▁▁ 281 ↑5 ▃▄█▄▁ 17,691 ↑335 29 | 5 Sort Layers Yuan Qing Lim ▁▁▁▁█ 8,536 ↑8,536 ▅▅█▂▁ 11,375 ↑81 ▁▁▁▁▁ 169 ▃▃█▇▁ 11,694 ↑172 30 | 6 Organize Layers Yuan Qing Lim ▁▁▁▁█ 8,458 ↑8,458 ███▆▁ 12,958 ↑73 ▁██▁▁ 217 ↑2 ▃▅█▆▁ 15,518 ↑127 31 | 7 Flatten Selection to Bitmap Yuan Qing Lim ▁▁▁▁█ 8,236 ↑8,236 ▇█▇▂▁ 10,484 ↑68 ▁█▁▁▁ 142 ↑1 ▅▅█▇▁ 13,725 ↑180 32 | 8 Component Utilities Yuan Qing Lim ▁▁▁▁█ 5,661 ↑5,661 ▄█▃▂▁ 7,458 ↑23 █▁▁▁▁ 213 ↑1 ▄▅█▄▁ 17,855 ↑148 33 | 9 Upscale Image Yuan Qing Lim ▁▁▁▁█ 2,898 ↑2,898 █▅▆▃▁ 3,428 ↑46 ▁▁▁▁▁ 63 █▅▇▆▁ 4,745 ↑81 34 | 10 Language Tester Yuan Qing Lim ▁▁▁▁█ 2,764 ↑2,764 █▄▄▃▁ 4,202 ↑27 ▁▁▁▁▁ 72 ▃▆█▅▁ 5,402 ↑71 35 | 11 Text Utilities Yuan Qing Lim ▁▁▁▁█ 1,476 ↑1,476 ██▄▁▁ 1,969 ↑10 █▁▁▁▁ 78 ↑1 ▆▄█▄▁ 6,038 ↑53 36 | 12 Set Layer Size Yuan Qing Lim ▁▁▁▁█ 1,290 ↑1,290 ██▄▄▁ 2,121 ↑12 ▁▁▁▁▁ 46 ▅██▄▁ 3,641 ↑36 37 | 13 Format Currency Yuan Qing Lim ▁▁▁▁█ 1,227 ↑1,227 ▅█▁▄▁ 2,208 ↑10 ▁▁▁▁▁ 32 ▄▅█▅▂ 4,214 ↑58 38 | 14 Distribute Layers Yuan Qing Lim ▁▁▁▁█ 767 ↑767 ▄▅█▁▁ 1,975 ↑10 █▁▁▁▁ 32 ↑1 ▆██▇▂ 2,506 ↑22 39 | 15 Move Layers Yuan Qing Lim ▁▁▁▁█ 676 ↑676 █▆▃▁▁ 1,959 ↑6 ▁▁▁▁▁ 33 ▄█▆▄▃ 2,676 ↑18 40 | 16 Draw Mask Under Selection Yuan Qing Lim ▁▁▁▁█ 608 ↑608 █▄▄▁▁ 1,129 ↑4 ▁▁▁▁▁ 24 ▄▄█▄▁ 1,801 ↑11 41 | 17 Draw Slice Over Selection Yuan Qing Lim ▁▁▁▁█ 418 ↑418 ▁█▄▁▁ 1,246 ↑2 ▁▁▁▁▁ 20 ▁▁█▂▁ 1,388 ↑15 42 | 43 | totals ▁▁▁▁█ 150,368 ↑150,368 ▆██▄▁ 220,646 ↑1,715 █▅█▄▁ 3,195 ↑33 ▄▅█▆▁ 232,676 ↑3,067 44 | 45 | ``` 46 | 47 | In the above example, for the plugin `Clean Document`, we see that: 48 | 49 | - `75,926` is the current install count. 50 | - `397` is the increase in install count over the 7-day period. 51 | - The [sparkline](https://www.edwardtufte.com/bboard/q-and-a-fetch-msg?msg_id=0001OR) (`▆▇█▅▁`) shows the trend in the increase in install count over the period. 52 | 53 | By default, the historical time period is 7 days. 54 | 55 | - Set this using the `--time` flag. For example: `--time 7d`, `--time 2w`. 56 | - *N.B.* Historical data goes back to 1 April 2020 at the most. 57 | 58 | By default, plugins are sorted in descending order of the increase in run count. 59 | 60 | - Set this using the `--sort` flag. For example: `--sort publisher`, `--sort name`, `--sort installs`, `--sort installs-delta`, `--sort likes`, `--sort likes-delta`, `--sort runs`, `--sort runs-delta`, `--sort views`, `--sort views-delta`. 61 | 62 | Omit the profile handle to get the stats for *all* Figma plugins: 63 | 64 | ``` 65 | $ npx --yes -- figma-plugins-stats | less -r 66 | ``` 67 | 68 | ## CLI 69 | 70 | ``` 71 | $ npx --yes -- figma-plugins-stats --help 72 | 73 | A CLI to get live and historical stats for your Figma plugins. 74 | 75 | Usage: 76 | $ figma-plugins-stats [options] 77 | 78 | Arguments: 79 | A Figma profile handle. 80 | 81 | Options: 82 | -h, --help Print this message. 83 | -l, --limit Limit the number of plugins returned. 84 | -s, --sort Set the sort order. One of 'name', 'publisher', 'installs', 85 | 'installs-delta', 'likes', 'likes-delta', 'runs', 86 | 'runs-delta', 'views' or 'views-delta'. Defaults to 87 | 'runs-delta'. 88 | -t, --time Set the period of historical data to show. Defaults to 89 | '7d'. 90 | -v, --version Print the version. 91 | 92 | Examples: 93 | $ figma-plugins-stats | less -r 94 | $ figma-plugins-stats yuanqing 95 | $ figma-plugins-stats --limit 10 96 | $ figma-plugins-stats --sort name 97 | $ figma-plugins-stats --sort publisher 98 | $ figma-plugins-stats --sort installs 99 | $ figma-plugins-stats --sort installs-delta 100 | $ figma-plugins-stats --sort likes 101 | $ figma-plugins-stats --sort likes-delta 102 | $ figma-plugins-stats --sort runs 103 | $ figma-plugins-stats --sort runs-delta 104 | $ figma-plugins-stats --sort views 105 | $ figma-plugins-stats --sort views-delta 106 | $ figma-plugins-stats --time 7d 107 | $ figma-plugins-stats --time 2w 108 | 109 | ``` 110 | 111 | ## API 112 | 113 | ```js 114 | import { fetchLivePluginsDataAsync } from 'figma-plugins-stats' 115 | ``` 116 | 117 | #### const plugins = await fetchLivePluginsDataAsync() 118 | 119 | Fetches the latest meta data and stats of all public Figma plugins. 120 | 121 | Returns a Promise for an array of objects that each have the following keys: 122 | 123 | - `id` 124 | - `name` 125 | - `description` 126 | - `lastUpdateDate` 127 | - `publisherHandle` 128 | - `publisherId` 129 | - `publisherName` 130 | - `installCount` 131 | - `likeCount` 132 | - `runCount` 133 | - `viewCount` 134 | 135 | ## Installation 136 | 137 | ``` 138 | $ npm install --global figma-plugins-stats 139 | ``` 140 | 141 | ## Shields.io badges 142 | 143 | Figma Plugins Stats also provides a JSON API for displaying stats as [Shields.io](https://shields.io/) badges on a GitHub `README` page. 144 | 145 | ### Plugin stats 146 | 147 | *Replace `` with your Figma plugin ID* 148 | 149 | [![runs](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/plugin/767379019764649932/runs.json)](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/plugin/767379019764649932/runs.json) 150 | 151 | ```md 152 | ![runs](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/plugin//runs.json) 153 | ``` 154 | 155 | [![installs](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/plugin/767379019764649932/installs.json)](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/plugin/767379019764649932/installs.json) 156 | 157 | ```md 158 | ![installs](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/plugin//installs.json) 159 | ``` 160 | 161 | [![likes](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/plugin/767379019764649932/likes.json)](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/plugin/767379019764649932/likes.json) 162 | 163 | ```md 164 | ![likes](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/plugin//likes.json) 165 | ``` 166 | 167 | [![views](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/plugin/767379019764649932/views.json)](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/plugin/767379019764649932/views.json) 168 | 169 | ```md 170 | ![views](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/plugin//views.json) 171 | ``` 172 | 173 | ### Publisher stats 174 | 175 | *Replace `` with your Figma profile handle* 176 | 177 | [![total runs](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/publisher/yuanqing/runs.json)](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/publisher/yuanqing/runs.json) 178 | 179 | ```md 180 | ![total runs](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/publisher//runs.json) 181 | ``` 182 | 183 | [![total installs](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/publisher/yuanqing/installs.json)](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/publisher/yuanqing/installs.json) 184 | 185 | ```md 186 | ![total installs](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/publisher//installs.json) 187 | ``` 188 | 189 | [![total likes](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/publisher/yuanqing/likes.json)](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/publisher/yuanqing/likes.json) 190 | 191 | ```md 192 | ![total likes](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/publisher//likes.json) 193 | ``` 194 | 195 | [![total views](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/publisher/yuanqing/views.json)](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/publisher/yuanqing/views.json) 196 | 197 | ```md 198 | ![total views](https://img.shields.io/endpoint?url=https://yuanqing.github.io/figma-plugins-stats/publisher//views.json) 199 | ``` 200 | 201 | ## Implementation details 202 | 203 | A snapshot of the stats for all Figma plugins is taken everyday at approximately 6 AM UTC+0, [via a GitHub action](.github/workflows/scrape.yml). (The first snapshot was taken on 1 April 2020.) Each snapshot is stored as a JSON file and [served on GitHub pages](https://github.com/yuanqing/figma-plugins-stats/tree/gh-pages). Historical data surfaced in the CLI is backed by these snapshots. 204 | 205 | ## License 206 | 207 | [MIT](/LICENSE.md) 208 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-plugins-stats", 3 | "version": "0.0.26", 4 | "description": "A CLI to get live and historical stats for your Figma plugins", 5 | "keywords": [ 6 | "figma", 7 | "figma-plugin", 8 | "figma-plugins" 9 | ], 10 | "license": "MIT", 11 | "author": "Yuan Qing Lim", 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/yuanqing/figma-plugins-stats.git" 15 | }, 16 | "type": "module", 17 | "engines": { 18 | "node": ">=16" 19 | }, 20 | "files": [ 21 | "lib" 22 | ], 23 | "bin": { 24 | "figma-plugins-stats": "lib/cli.js" 25 | }, 26 | "main": "lib/index.js", 27 | "scripts": { 28 | "build": "npm run clean && tsc", 29 | "clean": "rimraf '*.log' build data lib", 30 | "fix": "eslint --fix '{scripts,src,test}/**/*.ts'", 31 | "lint": "eslint '{scripts,src,test}/**/*.ts'", 32 | "prepublishOnly": "npm run build", 33 | "reset": "npm run clean && rimraf node_modules package-lock.json && npm install", 34 | "scrape": "node --loader ts-node/esm scripts/scrape.ts", 35 | "start": "node --loader ts-node/esm src/cli.ts", 36 | "test": "ava --serial 'test/**/*.ts'" 37 | }, 38 | "dependencies": { 39 | "@yuanqing/cli": "^0.0.9", 40 | "date-fns": "^2.28.0", 41 | "date-time": "^4.0.0", 42 | "fs-extra": "^10.1.0", 43 | "indent-string": "^5.0.0", 44 | "kleur": "^4.1.4", 45 | "ms": "^2.1.3", 46 | "node-fetch": "^3.2.5", 47 | "ora": "^6.1.0", 48 | "sparkly": "^6.0.0", 49 | "strip-ansi": "^7.0.1", 50 | "text-table": "^0.2.0" 51 | }, 52 | "devDependencies": { 53 | "@types/fs-extra": "^9.0.13", 54 | "@types/ms": "^0.7.31", 55 | "@types/node": "^17.0.40", 56 | "@types/node-fetch": "^2.6.1", 57 | "@types/text-table": "^0.2.2", 58 | "ava": "^4.3.0", 59 | "eslint": "^8.17.0", 60 | "eslint-config-yuanqing": "^0.0.6", 61 | "lint-staged": "^13.0.0", 62 | "prettier": "^2.6.2", 63 | "rimraf": "^3.0.2", 64 | "simple-git-hooks": "^2.8.0", 65 | "ts-node": "^10.8.1", 66 | "typescript": "^4.7.3" 67 | }, 68 | "ava": { 69 | "extensions": { 70 | "ts": "module" 71 | }, 72 | "failFast": true, 73 | "nodeArguments": [ 74 | "--loader", 75 | "ts-node/esm" 76 | ], 77 | "timeout": "2m", 78 | "workerThreads": false 79 | }, 80 | "eslintConfig": { 81 | "extends": "eslint-config-yuanqing" 82 | }, 83 | "lint-staged": { 84 | "*.ts": [ 85 | "eslint" 86 | ] 87 | }, 88 | "prettier": "eslint-config-yuanqing/prettier", 89 | "simple-git-hooks": { 90 | "pre-commit": "npx lint-staged", 91 | "pre-push": "npm run lint && npm run fix && npm run test" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /scripts/create-gh-pages-branch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | git checkout --orphan gh-pages 3 | git rm -r --cached . 4 | git commit --allow-empty -m 'Create gh-pages' 5 | git checkout master --force 6 | -------------------------------------------------------------------------------- /scripts/scrape.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import * as path from 'path' 3 | 4 | import { PluginData } from '../src/types.js' 5 | import { fetchLivePluginsDataAsync } from '../src/utilities/fetch-live-plugins-data-async.js' 6 | 7 | const DATA_DIRECTORY_NAME = 'data' 8 | const META_DATA_FILE_NAME = 'index.json' 9 | const STATS_DATA_FILE_NAME = 'stats.json' 10 | 11 | const PUBLISHERS_DIRECTORY_NAME = 'publisher' 12 | const PLUGINS_DIRECTORY_NAME = 'plugin' 13 | const INSTALLS_DATA_FILE_NAME = 'installs.json' 14 | const LIKES_DATA_FILE_NAME = 'likes.json' 15 | const RUNS_DATA_FILE_NAME = 'runs.json' 16 | const VIEWS_DATA_FILE_NAME = 'views.json' 17 | 18 | async function main() { 19 | const date = new Date().toISOString() 20 | const plugins = await fetchLivePluginsDataAsync() 21 | await writeFileAsync({ date, plugins }, META_DATA_FILE_NAME) 22 | await writeStatsAsync(date, plugins) 23 | await writePluginsAsync(plugins) 24 | await writePublishersAsync(plugins) 25 | } 26 | main() 27 | 28 | async function writeStatsAsync( 29 | date: string, 30 | plugins: Array 31 | ): Promise { 32 | const stats: { [id: string]: [number, number, number, number] } = {} 33 | for (const { id, installCount, likeCount, runCount, viewCount } of plugins) { 34 | stats[id] = [installCount, likeCount, runCount, viewCount] 35 | } 36 | await writeFileAsync({ date, stats }, STATS_DATA_FILE_NAME) 37 | await writeFileAsync({ date, stats }, `${date.slice(0, 10)}.json`) 38 | } 39 | 40 | const shieldsIoJson = { 41 | color: 'brightgreen', 42 | schemaVersion: 1 43 | } 44 | 45 | async function writePluginsAsync(plugins: Array): Promise { 46 | await fs.remove(path.join(DATA_DIRECTORY_NAME, PLUGINS_DIRECTORY_NAME)) 47 | for (const { id, installCount, likeCount, runCount, viewCount } of plugins) { 48 | await writeFileAsync( 49 | { 50 | ...shieldsIoJson, 51 | label: 'installs', 52 | message: formatNumber(installCount) 53 | }, 54 | path.join(PLUGINS_DIRECTORY_NAME, id, INSTALLS_DATA_FILE_NAME) 55 | ) 56 | await writeFileAsync( 57 | { 58 | ...shieldsIoJson, 59 | label: 'likes', 60 | message: formatNumber(likeCount) 61 | }, 62 | path.join(PLUGINS_DIRECTORY_NAME, id, LIKES_DATA_FILE_NAME) 63 | ) 64 | await writeFileAsync( 65 | { 66 | ...shieldsIoJson, 67 | label: 'runs', 68 | message: formatNumber(runCount) 69 | }, 70 | path.join(PLUGINS_DIRECTORY_NAME, id, RUNS_DATA_FILE_NAME) 71 | ) 72 | await writeFileAsync( 73 | { 74 | ...shieldsIoJson, 75 | label: 'views', 76 | message: formatNumber(viewCount) 77 | }, 78 | path.join(PLUGINS_DIRECTORY_NAME, id, VIEWS_DATA_FILE_NAME) 79 | ) 80 | } 81 | } 82 | 83 | async function writePublishersAsync(plugins: Array): Promise { 84 | await fs.remove(path.join(DATA_DIRECTORY_NAME, PUBLISHERS_DIRECTORY_NAME)) 85 | const publishers: { 86 | [key: string]: { 87 | installCount: number 88 | likeCount: number 89 | runCount: number 90 | viewCount: number 91 | } 92 | } = {} 93 | for (const { 94 | installCount, 95 | likeCount, 96 | publisherHandle, 97 | runCount, 98 | viewCount 99 | } of plugins) { 100 | if (typeof publishers[publisherHandle] === 'undefined') { 101 | publishers[publisherHandle] = { 102 | installCount: 0, 103 | likeCount: 0, 104 | runCount: 0, 105 | viewCount: 0 106 | } 107 | } 108 | publishers[publisherHandle].installCount += installCount 109 | publishers[publisherHandle].likeCount += likeCount 110 | publishers[publisherHandle].runCount += runCount 111 | publishers[publisherHandle].viewCount += viewCount 112 | } 113 | for (const publisherHandle of Object.keys(publishers)) { 114 | const publisher = publishers[publisherHandle] 115 | await writeFileAsync( 116 | { 117 | ...shieldsIoJson, 118 | label: 'total installs', 119 | message: formatNumber(publisher.installCount) 120 | }, 121 | path.join( 122 | PUBLISHERS_DIRECTORY_NAME, 123 | publisherHandle, 124 | INSTALLS_DATA_FILE_NAME 125 | ) 126 | ) 127 | await writeFileAsync( 128 | { 129 | ...shieldsIoJson, 130 | label: 'total likes', 131 | message: formatNumber(publisher.likeCount) 132 | }, 133 | path.join( 134 | PUBLISHERS_DIRECTORY_NAME, 135 | publisherHandle, 136 | LIKES_DATA_FILE_NAME 137 | ) 138 | ) 139 | await writeFileAsync( 140 | { 141 | ...shieldsIoJson, 142 | label: 'total runs', 143 | message: formatNumber(publisher.runCount) 144 | }, 145 | path.join(PUBLISHERS_DIRECTORY_NAME, publisherHandle, RUNS_DATA_FILE_NAME) 146 | ) 147 | await writeFileAsync( 148 | { 149 | ...shieldsIoJson, 150 | label: 'total views', 151 | message: formatNumber(publisher.viewCount) 152 | }, 153 | path.join( 154 | PUBLISHERS_DIRECTORY_NAME, 155 | publisherHandle, 156 | VIEWS_DATA_FILE_NAME 157 | ) 158 | ) 159 | } 160 | } 161 | 162 | async function writeFileAsync(data: any, fileName: string): Promise { 163 | const file = path.join(DATA_DIRECTORY_NAME, fileName) 164 | await fs.outputFile(file, `${JSON.stringify(data)}\n`, 'utf8') 165 | } 166 | 167 | function formatNumber(number: number): string { 168 | return Intl.NumberFormat('en-US', { 169 | compactDisplay: 'short', 170 | maximumFractionDigits: 1, 171 | minimumFractionDigits: number > 999 && number < 100000 ? 1 : 0, 172 | notation: 'compact' 173 | } as Intl.NumberFormatOptions) 174 | .format(number) 175 | .replace('.0', '') 176 | } 177 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable no-console */ 3 | 4 | import { createCli } from '@yuanqing/cli' 5 | import { readFileSync } from 'fs' 6 | import indentString from 'indent-string' 7 | import { dirname, resolve } from 'path' 8 | import { fileURLToPath } from 'url' 9 | 10 | import { fetchFigmaPluginsStatsAsync } from './fetch-figma-plugins-stats-async.js' 11 | import { CliOptions, CliPositionals } from './types.js' 12 | import { createDateTable } from './utilities/create-date-table.js' 13 | import { createPluginsTable } from './utilities/create-plugins-table.js' 14 | 15 | const __dirname = dirname(fileURLToPath(import.meta.url)) 16 | 17 | const packageJson = JSON.parse( 18 | readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8') 19 | ) 20 | 21 | const cliConfig = { 22 | name: packageJson.name, 23 | version: packageJson.version 24 | } 25 | 26 | const commandConfig = { 27 | description: `${packageJson.description}.`, 28 | examples: [ 29 | '| less -r', 30 | 'yuanqing', 31 | '--limit 10', 32 | '--sort name', 33 | '--sort publisher', 34 | '--sort installs', 35 | '--sort installs-delta', 36 | '--sort likes', 37 | '--sort likes-delta', 38 | '--sort runs', 39 | '--sort runs-delta', 40 | '--sort views', 41 | '--sort views-delta', 42 | '--time 7d', 43 | '--time 2w' 44 | ], 45 | options: [ 46 | { 47 | aliases: ['l'], 48 | default: -1, 49 | description: 'Limit the number of plugins returned.', 50 | name: 'limit', 51 | type: 'NON_ZERO_POSITIVE_INTEGER' 52 | }, 53 | { 54 | aliases: ['s'], 55 | default: 'runs-delta', 56 | description: 57 | "Set the sort order. One of 'name', 'publisher', 'installs', 'installs-delta', 'likes', 'likes-delta', 'runs', 'runs-delta', 'views' or 'views-delta'. Defaults to 'runs-delta'.", 58 | name: 'sort', 59 | type: [ 60 | 'name', 61 | 'publisher', 62 | 'installs', 63 | 'installs-delta', 64 | 'likes', 65 | 'likes-delta', 66 | 'runs', 67 | 'runs-delta', 68 | 'views', 69 | 'views-delta' 70 | ] 71 | }, 72 | { 73 | aliases: ['t'], 74 | default: '7d', 75 | description: 76 | "Set the period of historical data to show. Defaults to '7d'.", 77 | name: 'time', 78 | type: 'STRING' 79 | } 80 | ], 81 | positionals: [ 82 | { 83 | default: null, 84 | description: 'A Figma profile handle.', 85 | name: 'handle', 86 | type: 'STRING' 87 | } 88 | ] 89 | } 90 | 91 | async function main() { 92 | try { 93 | const result = createCli(cliConfig, commandConfig)(process.argv.slice(2)) 94 | if (typeof result !== 'undefined') { 95 | const positionals = result.positionals as CliPositionals 96 | const options = result.options as CliOptions 97 | const { plugins, totals, startDate, endDate } = 98 | await fetchFigmaPluginsStatsAsync({ 99 | handle: positionals.handle, 100 | limit: options.limit, 101 | sort: options.sort, 102 | timeOffset: options.time 103 | }) 104 | console.log() 105 | const dateTable = createDateTable(startDate, endDate) 106 | console.log(indentString(dateTable, 2)) 107 | console.log() 108 | const pluginsTable = createPluginsTable({ plugins, totals }) 109 | console.log(indentString(pluginsTable, 2)) 110 | console.log() 111 | } 112 | } catch (error: any) { 113 | console.error(`${packageJson.name}: ${error.message}`) 114 | process.exit(1) 115 | } 116 | } 117 | main() 118 | -------------------------------------------------------------------------------- /src/fetch-figma-plugins-stats-async.ts: -------------------------------------------------------------------------------- 1 | import ms from 'ms' 2 | import ora from 'ora' 3 | 4 | import { Counts, Plugin, SortKey } from './types.js' 5 | import { fetchHistoricalPluginStatsAsync } from './utilities/fetch-historical-plugin-stats-async.js' 6 | import { fetchLivePluginsDataAsync } from './utilities/fetch-live-plugins-data-async.js' 7 | import { parseData } from './utilities/parse-data.js' 8 | import { sortComparators } from './utilities/sort-comparators.js' 9 | 10 | export async function fetchFigmaPluginsStatsAsync({ 11 | handle, 12 | limit, 13 | sort, 14 | timeOffset 15 | }: { 16 | handle: null | string 17 | limit: number 18 | sort: SortKey 19 | timeOffset: string 20 | }): Promise<{ 21 | endDate: Date 22 | plugins: Array 23 | startDate: Date 24 | totals: Counts 25 | }> { 26 | const timeOffsetInMilliseconds = ms(timeOffset) 27 | if (timeOffsetInMilliseconds < ms('1d')) { 28 | throw new Error('Time offset must be at least 1 day (`1d`)') 29 | } 30 | const endDate = new Date() 31 | const spinner = ora({ 32 | color: 'gray' 33 | }) 34 | spinner.start() 35 | spinner.text = 'Fetching live plugins stats...' 36 | const livePluginsData = await fetchLivePluginsDataAsync() 37 | spinner.text = 'Fetching historical plugins stats...' 38 | const { startDate, stats } = await fetchHistoricalPluginStatsAsync( 39 | endDate, 40 | timeOffsetInMilliseconds 41 | ) 42 | spinner.stop() 43 | const { plugins, totals } = parseData(livePluginsData, stats, { 44 | handle, 45 | limit, 46 | sortComparator: sortComparators[sort] 47 | }) 48 | if (plugins.length === 0) { 49 | throw new Error(`User \`${handle}\` has no public plugins`) 50 | } 51 | return { 52 | endDate, 53 | plugins, 54 | startDate, 55 | totals 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { fetchLivePluginsDataAsync } from './utilities/fetch-live-plugins-data-async.js' 2 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type CliPositionals = { 2 | handle: string 3 | } 4 | export type CliOptions = { 5 | limit: number 6 | sort: SortKey 7 | time: string 8 | } 9 | 10 | export type RawPluginData = { 11 | id: string 12 | versions: { 13 | [key: string]: { 14 | name: string 15 | description: string 16 | created_at: string 17 | } 18 | } 19 | publisher: { 20 | profile_handle: string 21 | id: string 22 | name: string 23 | } 24 | install_count: number 25 | like_count: number 26 | unique_run_count: number 27 | view_count: number 28 | } 29 | 30 | export interface PluginData extends PluginStats { 31 | id: string 32 | name: string 33 | description: string 34 | lastUpdateDate: string 35 | publisherHandle: string 36 | publisherId: string 37 | publisherName: string 38 | } 39 | export interface PluginStats { 40 | installCount: number 41 | likeCount: number 42 | runCount: number 43 | viewCount: number 44 | } 45 | 46 | export interface Plugin extends Counts { 47 | name: string 48 | publisher: string 49 | } 50 | export interface Counts { 51 | installCount: Count 52 | likeCount: Count 53 | runCount: Count 54 | viewCount: Count 55 | } 56 | export type Count = { 57 | count: number 58 | deltas: Array 59 | totalDelta: number 60 | } 61 | 62 | export type SortKey = 63 | | 'name' 64 | | 'publisher' 65 | | 'installs' 66 | | 'installs-delta' 67 | | 'likes' 68 | | 'likes-delta' 69 | | 'runs' 70 | | 'runs-delta' 71 | | 'views' 72 | | 'views-delta' 73 | -------------------------------------------------------------------------------- /src/utilities/create-date-table.ts: -------------------------------------------------------------------------------- 1 | import { differenceInMilliseconds } from 'date-fns' 2 | import dateTime from 'date-time' 3 | import kleur from 'kleur' 4 | import ms from 'ms' 5 | import stripAnsi from 'strip-ansi' 6 | import textTable from 'text-table' 7 | 8 | export function createDateTable(startDate: Date, endDate: Date): string { 9 | const difference = differenceInMilliseconds(endDate, startDate) 10 | const rows = [ 11 | [kleur.gray('period'), ms(difference)], 12 | [kleur.gray('from'), dateTime({ date: startDate, showTimeZone: true })], 13 | [kleur.gray('to'), dateTime({ date: endDate, showTimeZone: true })] 14 | ] 15 | return textTable(rows, { 16 | stringLength: function (string: any) { 17 | return stripAnsi(string).length 18 | } 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/utilities/create-plugins-table.ts: -------------------------------------------------------------------------------- 1 | import kleur from 'kleur' 2 | import sparkly from 'sparkly' 3 | import stripAnsi from 'strip-ansi' 4 | import textTable from 'text-table' 5 | 6 | import { Plugin } from '../types.js' 7 | import { pluginStatsKeys } from './plugins-stats-keys.js' 8 | 9 | export function createPluginsTable({ 10 | plugins, 11 | totals 12 | }: { 13 | plugins: Array 14 | totals: any 15 | }): string { 16 | const headers = [ 17 | kleur.gray('no'), 18 | kleur.gray(' name'), 19 | kleur.gray(' publisher'), 20 | kleur.gray(' runs'), 21 | '', 22 | kleur.gray(' installs'), 23 | '', 24 | kleur.gray(' likes'), 25 | '', 26 | kleur.gray(' views'), 27 | '' 28 | ] 29 | const rows = [headers] 30 | plugins.forEach(function (plugin: Plugin, index: number) { 31 | const row: Array = [ 32 | `${index + 1}`, 33 | ` ${plugin.name.trim()}`, 34 | ` ${plugin.publisher.trim()}` 35 | ] 36 | for (const key of pluginStatsKeys) { 37 | const count = plugin[key] 38 | row.push(` ${sparkly(count.deltas)} ${count.count.toLocaleString()}`) 39 | row.push(formatDelta(count.totalDelta)) 40 | } 41 | rows.push(row) 42 | }) 43 | const totalRow = [ 44 | '', 45 | '', 46 | kleur.gray(' totals'), 47 | ` ${sparkly( 48 | totals.runCount.deltas 49 | )} ${totals.runCount.count.toLocaleString()}`, 50 | formatDelta(totals.runCount.totalDelta), 51 | ` ${sparkly( 52 | totals.installCount.deltas 53 | )} ${totals.installCount.count.toLocaleString()}`, 54 | formatDelta(totals.installCount.totalDelta), 55 | ` ${sparkly( 56 | totals.likeCount.deltas 57 | )} ${totals.likeCount.count.toLocaleString()}`, 58 | formatDelta(totals.likeCount.totalDelta), 59 | ` ${sparkly( 60 | totals.viewCount.deltas 61 | )} ${totals.viewCount.count.toLocaleString()}`, 62 | formatDelta(totals.viewCount.totalDelta) 63 | ] 64 | rows.push([]) 65 | rows.push(totalRow) 66 | return textTable(rows, { 67 | hsep: ' ', 68 | stringLength: function (string: any) { 69 | return stripAnsi(string).length 70 | } 71 | }) 72 | } 73 | 74 | function formatDelta(delta: number): string { 75 | if (delta < 0) { 76 | return kleur.red(`↓${Math.abs(delta).toLocaleString()}`) 77 | } 78 | if (delta > 0) { 79 | return kleur.green(`↑${delta.toLocaleString()}`) 80 | } 81 | return '' 82 | } 83 | -------------------------------------------------------------------------------- /src/utilities/fetch-async.ts: -------------------------------------------------------------------------------- 1 | import fetch, { Response } from 'node-fetch' 2 | 3 | export function fetchAsync(url: string): Promise { 4 | return fetch(url, { 5 | headers: { 6 | 'X-Requested-With': 'XMLHttpRequest' 7 | } 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/utilities/fetch-historical-plugin-stats-async.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addMilliseconds, 3 | differenceInMilliseconds, 4 | isBefore, 5 | isSameDay, 6 | parseISO, 7 | subMilliseconds 8 | } from 'date-fns' 9 | import ms from 'ms' 10 | 11 | import { PluginStats } from '../types.js' 12 | import { fetchAsync } from './fetch-async.js' 13 | 14 | export async function fetchHistoricalPluginStatsAsync( 15 | endDate: Date, 16 | timeOffsetInMilliseconds: number 17 | ): Promise<{ startDate: Date; stats: Array<{ [id: string]: PluginStats }> }> { 18 | const lastStats = await fetchHistoricalPluginStatsForDate(null) 19 | const dates = computeDates(lastStats.date, endDate, timeOffsetInMilliseconds) 20 | const promises = [] 21 | for (const date of dates) { 22 | promises.push(fetchHistoricalPluginStatsForDate(date)) 23 | } 24 | const results = await Promise.all(promises) 25 | results.push(lastStats) 26 | return { 27 | startDate: results[0].date, 28 | stats: results.map(function ({ stats }) { 29 | return stats 30 | }) 31 | } 32 | } 33 | 34 | const GITHUB_PAGES_BASE_URL = 'https://yuanqing.github.io/figma-plugins-stats/' 35 | 36 | async function fetchHistoricalPluginStatsForDate( 37 | date: null | Date 38 | ): Promise<{ date: Date; stats: { [id: string]: PluginStats } }> { 39 | const response = await fetchAsync( 40 | `${GITHUB_PAGES_BASE_URL}${ 41 | date === null ? 'stats' : date.toISOString().slice(0, 10) 42 | }.json` 43 | ) 44 | const json: any = await response.json() 45 | const stats: { [id: string]: PluginStats } = {} 46 | for (const id in json.stats) { 47 | stats[id] = { 48 | installCount: json.stats[id][0], 49 | likeCount: json.stats[id][1], 50 | runCount: json.stats[id][2], 51 | viewCount: json.stats[id][3] 52 | } 53 | } 54 | return { 55 | date: parseISO(json.date), 56 | stats 57 | } 58 | } 59 | 60 | const EARLIEST_STATS_DATE = new Date('2020-04-01') // only have data going back to this date 61 | const GRANULARITY = 4 // number of segments in our sparklines 62 | const ONE_DAY_IN_MILLISECONDS = ms('1d') 63 | 64 | function computeDates( 65 | lastStatsDate: Date, 66 | endDate: Date, 67 | timeOffsetInMilliseconds: number 68 | ): Array { 69 | let date = subMilliseconds( 70 | lastStatsDate, 71 | timeOffsetInMilliseconds - differenceInMilliseconds(endDate, lastStatsDate) 72 | ) 73 | if (isBefore(date, EARLIEST_STATS_DATE) === true) { 74 | date = new Date(EARLIEST_STATS_DATE.getTime()) 75 | } 76 | const intervalInMilliseconds = Math.max( 77 | parseInt( 78 | `${differenceInMilliseconds(lastStatsDate, date) / GRANULARITY}`, 79 | 10 80 | ), 81 | ONE_DAY_IN_MILLISECONDS // each interval to be 1 day at the least 82 | ) 83 | const dates = [] 84 | let i = -1 85 | while (++i < GRANULARITY && isSameDay(date, lastStatsDate) === false) { 86 | dates.push(new Date(date.getTime())) 87 | date = addMilliseconds(date, intervalInMilliseconds) 88 | } 89 | return dates 90 | } 91 | -------------------------------------------------------------------------------- /src/utilities/fetch-live-plugins-data-async.ts: -------------------------------------------------------------------------------- 1 | import { PluginData, RawPluginData } from '../types.js' 2 | import { fetchAsync } from './fetch-async.js' 3 | 4 | export async function fetchLivePluginsDataAsync(): Promise> { 5 | const data = await fetchRawPluginsDataAsync() 6 | return parseRawPluginsData(data) 7 | } 8 | 9 | async function fetchRawPluginsDataAsync(): Promise> { 10 | let result: any = [] 11 | let url = 12 | '/api/plugins/browse?sort_order=desc&resource_type=plugins&page_size=50' 13 | while (typeof url !== 'undefined') { 14 | const response = await fetchAsync(`https://www.figma.com${url}`) 15 | const json: any = await response.json() 16 | result = result.concat(json.meta.plugins) 17 | url = json.pagination.next_page 18 | } 19 | return deduplicate(result) 20 | } 21 | 22 | function deduplicate(data: Array): Array { 23 | const result: Array = [] 24 | const ids: { [id: string]: boolean } = {} 25 | for (const item of data) { 26 | const id = item.id 27 | if (ids[id] !== true) { 28 | result.push(item) 29 | ids[id] = true 30 | } 31 | } 32 | return result 33 | } 34 | 35 | function parseRawPluginsData(data: Array): Array { 36 | const plugins: Array = [] 37 | for (const item of data) { 38 | const metaData = Object.values(item.versions)[0] 39 | plugins.push({ 40 | description: metaData.description, 41 | id: item.id, 42 | installCount: item.install_count, 43 | lastUpdateDate: metaData.created_at, 44 | likeCount: item.like_count, 45 | name: metaData.name, 46 | publisherHandle: item.publisher.profile_handle, 47 | publisherId: item.publisher.id, 48 | publisherName: item.publisher.name, 49 | runCount: item.unique_run_count, 50 | viewCount: item.view_count 51 | }) 52 | } 53 | return plugins.sort(function (a: PluginData, b: PluginData) { 54 | return a.id.localeCompare(b.id, undefined, { numeric: true }) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /src/utilities/parse-data.ts: -------------------------------------------------------------------------------- 1 | import { Count, Counts, Plugin, PluginData, PluginStats } from '../types.js' 2 | import { pluginStatsKeys } from './plugins-stats-keys.js' 3 | 4 | export function parseData( 5 | pluginsData: Array, 6 | stats: Array<{ [id: string]: PluginStats }>, 7 | { 8 | handle, 9 | limit, 10 | sortComparator 11 | }: { 12 | handle: null | string 13 | limit: number 14 | sortComparator: (a: Plugin, b: Plugin) => number 15 | } 16 | ): { 17 | plugins: Array 18 | totals: Counts 19 | } { 20 | const filteredPluginsData = 21 | handle === null ? pluginsData : filterPluginsByHandle(pluginsData, handle) 22 | let pluginsSummary: Array = [] 23 | for (const pluginData of filteredPluginsData) { 24 | const pluginSummaryCounts = {} as { 25 | [key in keyof PluginStats]: Count 26 | } 27 | for (const key of pluginStatsKeys) { 28 | const counts = [] 29 | for (const statsItem of stats) { 30 | const pluginStats = statsItem[pluginData.id] 31 | counts.push(typeof pluginStats === 'undefined' ? 0 : pluginStats[key]) 32 | } 33 | counts.push(pluginData[key]) 34 | pluginSummaryCounts[key] = { 35 | count: counts[counts.length - 1], 36 | deltas: computeDeltas(counts), 37 | totalDelta: counts[counts.length - 1] - counts[0] 38 | } 39 | } 40 | pluginsSummary.push({ 41 | name: pluginData.name, 42 | publisher: pluginData.publisherName, 43 | ...pluginSummaryCounts 44 | }) 45 | } 46 | pluginsSummary.sort(sortComparator) 47 | if (typeof limit !== 'undefined' && limit !== -1) { 48 | pluginsSummary = pluginsSummary.slice(0, limit) 49 | } 50 | return { 51 | plugins: pluginsSummary, 52 | totals: computeTotals(pluginsSummary) 53 | } 54 | } 55 | 56 | function filterPluginsByHandle( 57 | pluginsData: Array, 58 | handle: string 59 | ): Array { 60 | return pluginsData.filter(function (plugin: any) { 61 | return plugin.publisherHandle === handle 62 | }) 63 | } 64 | 65 | function computeDeltas(counts: Array): Array { 66 | const result: Array = [] 67 | counts.forEach(function (count: any, index: any) { 68 | if (index === 0) { 69 | return 70 | } 71 | result.push(count - counts[index - 1]) 72 | }) 73 | return result 74 | } 75 | 76 | function computeTotals(pluginsSummary: Array): Counts { 77 | const totals = {} as { [key in keyof PluginStats]: Count } 78 | for (const key of pluginStatsKeys) { 79 | totals[key] = { 80 | count: 0, 81 | deltas: [], 82 | totalDelta: 0 83 | } 84 | for (const plugin of pluginsSummary) { 85 | totals[key].count += plugin[key].count 86 | plugin[key].deltas.forEach(function (delta: any, index: any) { 87 | if (typeof totals[key].deltas[index] === 'undefined') { 88 | totals[key].deltas[index] = 0 89 | } 90 | totals[key].deltas[index] += delta 91 | }) 92 | totals[key].totalDelta += plugin[key].totalDelta 93 | } 94 | } 95 | return totals 96 | } 97 | -------------------------------------------------------------------------------- /src/utilities/plugins-stats-keys.ts: -------------------------------------------------------------------------------- 1 | import { PluginStats } from '../types.js' 2 | 3 | export const pluginStatsKeys: Array = [ 4 | 'runCount', 5 | 'installCount', 6 | 'likeCount', 7 | 'viewCount' 8 | ] 9 | -------------------------------------------------------------------------------- /src/utilities/sort-comparators.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, SortKey } from '../types.js' 2 | 3 | export const sortComparators: { 4 | [key in SortKey]: (a: Plugin, b: Plugin) => number 5 | } = { 6 | 'installs': function (a: Plugin, b: Plugin) { 7 | return b.installCount.count - a.installCount.count 8 | }, 9 | 'installs-delta': function (a: Plugin, b: Plugin) { 10 | return b.installCount.totalDelta - a.installCount.totalDelta 11 | }, 12 | 'likes': function (a: Plugin, b: Plugin) { 13 | return b.likeCount.count - a.likeCount.count 14 | }, 15 | 'likes-delta': function (a: Plugin, b: Plugin) { 16 | return b.likeCount.totalDelta - a.likeCount.totalDelta 17 | }, 18 | 'name': function (a: Plugin, b: Plugin) { 19 | return a.name.toLowerCase().localeCompare(b.name.toLowerCase()) 20 | }, 21 | 'publisher': function (a: Plugin, b: Plugin) { 22 | return a.publisher.toLowerCase().localeCompare(b.publisher.toLowerCase()) 23 | }, 24 | 'runs': function (a: Plugin, b: Plugin) { 25 | return b.runCount.count - a.runCount.count 26 | }, 27 | 'runs-delta': function (a: Plugin, b: Plugin) { 28 | return b.runCount.totalDelta - a.runCount.totalDelta 29 | }, 30 | 'views': function (a: Plugin, b: Plugin) { 31 | return b.viewCount.count - a.viewCount.count 32 | }, 33 | 'views-delta': function (a: Plugin, b: Plugin) { 34 | return b.viewCount.totalDelta - a.viewCount.totalDelta 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/cli.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { exec } from 'child_process' 3 | import { dirname, resolve } from 'path' 4 | import { fileURLToPath } from 'url' 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)) 7 | 8 | test('runs and exits without error', async function (t) { 9 | t.plan(1) 10 | const cliPath = resolve(__dirname, '..', 'src', 'cli.ts') 11 | await new Promise(function (resolve, reject) { 12 | exec(`node --loader ts-node/esm ${cliPath}`, function (error) { 13 | if (error) { 14 | t.fail() 15 | reject(error) 16 | } 17 | t.pass() 18 | resolve() 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/fetch-live-plugins-data.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { PluginData } from '../src/types.js' 4 | import { fetchLivePluginsDataAsync } from '../src/utilities/fetch-live-plugins-data-async.js' 5 | 6 | test('fetches the stats of all public Figma plugins', async function (t) { 7 | t.plan(3) 8 | const plugins = await fetchLivePluginsDataAsync() 9 | t.true(Array.isArray(plugins) === true) 10 | t.true(plugins.length > 0) 11 | const result = plugins.find(function (plugin: PluginData) { 12 | if (plugin.publisherHandle === 'yuanqing') { 13 | return true 14 | } 15 | }) 16 | t.true(typeof result !== 'undefined') 17 | }) 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": true, 5 | "esModuleInterop": true, 6 | "module": "ES2020", 7 | "moduleResolution": "node", 8 | "outDir": "./lib", 9 | "removeComments": true, 10 | "rootDir": "./src", 11 | "strict": true, 12 | "target": "ES2020", 13 | "typeRoots": ["./node_modules/@types"] 14 | }, 15 | "include": ["./src/**/*.ts"] 16 | } 17 | --------------------------------------------------------------------------------