├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── extension.yml │ ├── format.yml │ └── lint.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── github-repo-size-icon256.png ├── github-repo-size-popup.png └── github-repo-size-screenshot.png ├── astro.config.mjs ├── bun.lockb ├── bundler.ts ├── entrypoints.ts ├── eslint.config.mjs ├── extract-inline.ts ├── package.json ├── page ├── .gitignore ├── astro.config.mjs ├── bun.lockb ├── package.json ├── patches │ └── typedoc+0.25.1.patch ├── public │ ├── .nojekyll │ └── favicon.png ├── src │ ├── env.d.ts │ └── pages │ │ ├── auth │ │ └── index.astro │ │ └── index.astro ├── tailwind.config.cjs ├── tsconfig.json ├── typedoc.ts └── yarn.lock ├── pnpm-lock.yaml ├── privacy-policy.md ├── public ├── content.css ├── images │ ├── icon128.png │ ├── icon16.png │ ├── icon32.png │ ├── icon48.png │ └── icon64.png └── manifest.json ├── src ├── components │ ├── Authenticator.svelte │ ├── Expander.svelte │ ├── Popup.svelte │ ├── PopupTitle.svelte │ ├── TokenInput.svelte │ └── TokenStatus.svelte ├── env.d.ts ├── pages │ └── index.astro ├── scripts │ ├── background.ts │ ├── content.ts │ └── internal │ │ ├── api.ts │ │ ├── crypto.ts │ │ ├── dom-manipulation.ts │ │ ├── element-factory.ts │ │ ├── format.ts │ │ ├── get-path-object.ts │ │ ├── get-size.ts │ │ ├── index.ts │ │ ├── selectors.ts │ │ └── types │ │ ├── index.ts │ │ └── types.ts └── shared │ ├── branch.ts │ ├── get-token.ts │ ├── index.ts │ ├── storage-keys.ts │ └── storage.ts ├── svelte.config.js ├── tailwind.config.cjs ├── tsconfig.json ├── types.d.ts └── update-version.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | groups: 8 | all-updates: 9 | patterns: 10 | - '*' 11 | - package-ecosystem: 'npm' 12 | directory: '/page' 13 | schedule: 14 | interval: 'weekly' 15 | groups: 16 | all-updates: 17 | patterns: 18 | - '*' 19 | -------------------------------------------------------------------------------- /.github/workflows/extension.yml: -------------------------------------------------------------------------------- 1 | name: Extension Build CI 2 | 3 | on: 4 | push: 5 | branches: '**' 6 | pull_request: 7 | branches: main 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Use bun 18 | uses: oven-sh/setup-bun@v1 19 | with: 20 | bun-version: latest 21 | 22 | - name: Install dependencies 23 | run: | 24 | bun install 25 | 26 | - name: Build extension 27 | run: | 28 | bun run build 29 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Extension Format CI 2 | 3 | on: 4 | push: 5 | branches: '**' 6 | pull_request: 7 | branches: main 8 | 9 | jobs: 10 | format: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Use bun 18 | uses: oven-sh/setup-bun@v1 19 | with: 20 | bun-version: latest 21 | 22 | - name: Install dependencies 23 | run: | 24 | bun install 25 | 26 | - name: Check code formatting 27 | run: bun run checkFormat 28 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Extension Lint CI 2 | 3 | on: 4 | push: 5 | branches: '**' 6 | pull_request: 7 | branches: main 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Use bun 18 | uses: oven-sh/setup-bun@v1 19 | with: 20 | bun-version: latest 21 | 22 | - name: Install dependencies 23 | run: bun install 24 | 25 | - name: Check code linting 26 | run: bun run lint 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | build 4 | .astro/ 5 | github-repo-size-extension/ 6 | github-repo-size-extension.zip 7 | *.pem 8 | *.crx 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | .pnpm-debug.log* 17 | 18 | # Diagnostic reports (https://nodejs.org/api/report.html) 19 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 20 | 21 | # Runtime data 22 | pids 23 | *.pid 24 | *.seed 25 | *.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | *.lcov 33 | 34 | # nyc test coverage 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | .grunt 39 | 40 | # Bower dependency directory (https://bower.io/) 41 | bower_components 42 | 43 | # node-waf configuration 44 | .lock-wscript 45 | 46 | # Compiled binary addons (https://nodejs.org/api/addons.html) 47 | build/Release 48 | 49 | # Dependency directories 50 | node_modules/ 51 | jspm_packages/ 52 | 53 | # Snowpack dependency directory (https://snowpack.dev/) 54 | web_modules/ 55 | 56 | # TypeScript cache 57 | *.tsbuildinfo 58 | 59 | # Optional npm cache directory 60 | .npm 61 | 62 | # Optional eslint cache 63 | .eslintcache 64 | 65 | # Optional stylelint cache 66 | .stylelintcache 67 | 68 | # Microbundle cache 69 | .rpt2_cache/ 70 | .rts2_cache_cjs/ 71 | .rts2_cache_es/ 72 | .rts2_cache_umd/ 73 | 74 | # Optional REPL history 75 | .node_repl_history 76 | 77 | # Output of 'npm pack' 78 | *.tgz 79 | 80 | # Yarn Integrity file 81 | .yarn-integrity 82 | 83 | # dotenv environment variable files 84 | .env 85 | .env.development.local 86 | .env.test.local 87 | .env.production.local 88 | .env.local 89 | .env.production 90 | 91 | # parcel-bundler cache (https://parceljs.org/) 92 | .cache 93 | .parcel-cache 94 | 95 | # Next.js build output 96 | .next 97 | out 98 | 99 | # Nuxt.js build / generate output 100 | .nuxt 101 | 102 | # Gatsby files 103 | .cache/ 104 | 105 | # vuepress build output 106 | .vuepress/dist 107 | 108 | # vuepress v2.x temp and cache directory 109 | .temp 110 | .cache 111 | 112 | # Docusaurus cache and generated files 113 | .docusaurus 114 | 115 | # Serverless directories 116 | .serverless/ 117 | 118 | # FuseBox cache 119 | .fusebox/ 120 | 121 | # DynamoDB Local files 122 | .dynamodb/ 123 | 124 | # TernJS port file 125 | .tern-port 126 | 127 | # Stores VSCode versions used for testing VSCode extensions 128 | .vscode-test 129 | 130 | # yarn v2 131 | .yarn/cache 132 | .yarn/unplugged 133 | .yarn/build-state.yml 134 | .yarn/install-state.gz 135 | .pnp.* 136 | 137 | # IntelliJ based IDEs 138 | .idea 139 | 140 | # macOS-specific files 141 | .DS_Store 142 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | github-repo-size-extension/ 2 | node_modules/ 3 | package-lock.json 4 | pnpm-lock.yaml 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.2.7 2 | 3 | Add headers (including auth token) to fetching default branch. 4 | 5 | # 0.2.6 6 | 7 | Fixed a bug where having a submodule in a repository would hinder the extension from adding size information elements to the DOM. 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project uses [Bun](https://bun.sh/docs)! If you are on Windows, please refer to [this post](https://github.com/oven-sh/bun/issues/43) on how to make it work on your machine. 4 | 5 | #### Install dependencies 6 | 7 | ```bash 8 | bun install 9 | ``` 10 | 11 | #### Build the project 12 | 13 | ```bash 14 | bun run build 15 | ``` 16 | 17 | OR 18 | 19 | ```bash 20 | bun run build:firefox 21 | ``` 22 | 23 | #### Manual Installation: 24 | 25 | ##### Chrome 26 | 27 | - Open your Chrome browser and navigate to [chrome://extensions/](chrome://extensions/). 28 | - Enable "Developer mode" in the top right corner. 29 | - Click on "Load unpacked" and select the github-repo-size-extension folder inside the github-repo-size directory (generated after running bun run build:firefox). 30 | 31 | ##### Firefox 32 | 33 | - Open your Firefox browser and navigate to [about:debugging#/runtime/this-firefox](about:debugging#/runtime/this-firefox). 34 | - Click on "Load Temporary Add-on…" and select the github-repo-size-extension folder inside the github-repo-size directory (generated after running bun run build). 35 | 36 | Please format and fix any linting problems in the project before creating a PR. 37 | 38 | ```bash 39 | bun run format 40 | ``` 41 | 42 | ```bash 43 | bun run lint 44 | ``` 45 | 46 | Most of the business logic for this extension happens inside the [src/scripts/](https://github.com/AminoffZ/github-repo-size/tree/main/src/scripts) folder. If you are looking to make changes, this is most likely the place to start. 47 | 48 | ### Docs 49 | 50 | Visit the [docs page](https://aminoffz.github.io/github-repo-size/docs) for information about specific functions. There are even a few [examples](https://aminoffz.github.io/github-repo-size/docs/functions/internal_crypto.hashClass.html). 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 AminoffZ 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 | [![Google Chrome](https://img.shields.io/chrome-web-store/v/jpdmfnflpdgefbfkafcikmhipofhanfl?label=Get%20GRS%20for%20Chrome&logo=Google%20Chrome)](https://chrome.google.com/webstore/detail/github-repo-size/jpdmfnflpdgefbfkafcikmhipofhanfl) 2 | [![Mozilla Add-on Version](https://img.shields.io/amo/v/github-repo-size-extension?label=Get%20GRS%20for%20Firefox&logo=Firefox)](https://addons.mozilla.org/en-US/firefox/addon/github-repo-size-extension/) 3 | 4 | 5 | # GitHub Repo Size 6 | 7 | An extension to display the size of GitHub repositories. 8 | Inspired by [github-repo-size](https://github.com/harshjv/github-repo-size). 9 | 10 | ## Features 11 | 12 | View summary of file sizes in repositories. 13 | 14 |
15 | 16 |
17 | 18 | It's also possible to view private repository size summaries by adding a personal access token.[^1] 19 | 20 |
21 | 22 |
23 | 24 | GitHubs API has a limit of 50 (at time of writing) requests per hour for unauthenticated requests. By clicking Authenticate and signing in with OAuth, you can make up to 5000 requests per hour.[^2] 25 | 26 | [^1]: [Managing your personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) 27 | 28 | [^2]: [Rate limiting](https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limits) 29 | 30 | ## Requirements 31 | 32 | Chromium or Firefox based browser. 33 | 34 | ## Installation 35 | 36 | For Chrome, install the extension from the [Chrome Web Store](https://chrome.google.com/webstore/detail/github-repo-size/jpdmfnflpdgefbfkafcikmhipofhanfl). 37 | 38 | For Firefox, install the extension from the [Firefox Add-on Store](https://addons.mozilla.org/en-US/firefox/addon/github-repo-size-extension/). 39 | 40 | ## Contributing 41 | 42 | Whether you've found a bug, have a suggestion, or want to add a new feature, your contributions are valuable to us. To ensure a smooth collaboration, please take a moment to read our [Contributing Guide](https://github.com/AminoffZ/github-repo-size/blob/main/CONTRIBUTING.md). 43 | 44 | **Code of Conduct:** 45 | Please note that we have a [Code of Conduct](https://github.com/AminoffZ/github-repo-size/blob/main/CODE_OF_CONDUCT.md) in place. We expect all contributors to adhere to it. It outlines the standards of behavior that everyone involved in this project should follow. Be kind and respectful. 46 | 47 | **How to Contribute:** 48 | 49 | 1. If you've spotted a bug or have a feature request, feel free to submit an [Issue](https://github.com/AminoffZ/github-repo-size/issues) and explain your concern. 50 | 2. Interested in fixing issues or adding new features? Check out our [Contributing Guide](https://github.com/AminoffZ/github-repo-size/blob/main/CONTRIBUTING.md) for a detailed walkthrough. 51 | 52 | We're thrilled to have you on board, and your contributions will make GitHub Repo Size even better for everyone. 53 | Thank you all and happy coding! 54 | 55 | ## LICENSE 56 | 57 | Github Repo Size is under [MIT License](https://github.com/AminoffZ/github-repo-size/blob/main/LICENSE). 58 | -------------------------------------------------------------------------------- /assets/github-repo-size-icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AminoffZ/github-repo-size/3bd39aac57d18d216fbb3e78d95dca33eb10c1a5/assets/github-repo-size-icon256.png -------------------------------------------------------------------------------- /assets/github-repo-size-popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AminoffZ/github-repo-size/3bd39aac57d18d216fbb3e78d95dca33eb10c1a5/assets/github-repo-size-popup.png -------------------------------------------------------------------------------- /assets/github-repo-size-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AminoffZ/github-repo-size/3bd39aac57d18d216fbb3e78d95dca33eb10c1a5/assets/github-repo-size-screenshot.png -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import svelte from '@astrojs/svelte'; 3 | import tailwind from '@astrojs/tailwind'; 4 | 5 | import purgecss from 'astro-purgecss'; 6 | 7 | // https://astro.build/config 8 | export default defineConfig({ 9 | integrations: [ 10 | svelte(), 11 | tailwind(), 12 | purgecss({ 13 | safelist: { 14 | deep: [/grs/], 15 | }, 16 | }), 17 | ], 18 | build: { 19 | assets: 'app', 20 | }, 21 | outDir: './github-repo-size-extension', 22 | }); 23 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AminoffZ/github-repo-size/3bd39aac57d18d216fbb3e78d95dca33eb10c1a5/bun.lockb -------------------------------------------------------------------------------- /bundler.ts: -------------------------------------------------------------------------------- 1 | import entryPoints from './entrypoints'; 2 | 3 | const entrypoints = await entryPoints(); 4 | 5 | await Bun.build({ 6 | entrypoints: entrypoints, 7 | outdir: './github-repo-size-extension', 8 | minify: true, 9 | }); 10 | -------------------------------------------------------------------------------- /entrypoints.ts: -------------------------------------------------------------------------------- 1 | import { readdir } from 'fs/promises'; 2 | import { extname, join } from 'path'; 3 | 4 | const sourceDir = './src/scripts'; 5 | 6 | /** 7 | * Recursively get all .ts and .js entrypoints from the directory 8 | * 9 | * @param dir Directory path to scan 10 | * @returns {Promise} The entrypoints 11 | */ 12 | async function getFiles(options?: { 13 | root?: string; 14 | deep?: boolean; 15 | }): Promise { 16 | const dir = options?.root ?? sourceDir; 17 | const deep = options?.deep ?? false; 18 | const dirents = await readdir(dir, { withFileTypes: true }); 19 | const files = await Promise.all( 20 | dirents.map((dirent) => { 21 | const res = join(dir, dirent.name); 22 | if (dirent.isDirectory() && deep) { 23 | return getFiles({ ...options, root: res }); 24 | } else { 25 | return Promise.resolve(res); 26 | } 27 | }) 28 | ); 29 | 30 | // Flatten the array and filter only .ts and .js files 31 | return Array.prototype 32 | .concat(...files) 33 | .filter((file) => ['.ts', '.js'].includes(extname(file))); 34 | } 35 | 36 | /** 37 | * Get all entrypoints from the src directory 38 | * 39 | * @returns {Promise} The entrypoints 40 | */ 41 | export default async function entryPoints(options?: { 42 | root?: string; 43 | deep?: boolean; 44 | }): Promise { 45 | return await getFiles(options); 46 | } 47 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 2 | import globals from 'globals'; 3 | import tsParser from '@typescript-eslint/parser'; 4 | import path from 'node:path'; 5 | import { fileURLToPath } from 'node:url'; 6 | import js from '@eslint/js'; 7 | import { FlatCompat } from '@eslint/eslintrc'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all, 15 | }); 16 | 17 | export default [ 18 | { 19 | ignores: ['**/node_modules/', '**/github-repo-size-extension/'], 20 | }, 21 | ...compat.extends( 22 | 'eslint:recommended', 23 | 'plugin:@typescript-eslint/recommended' 24 | ), 25 | { 26 | plugins: { 27 | '@typescript-eslint': typescriptEslint, 28 | }, 29 | 30 | languageOptions: { 31 | globals: { 32 | ...globals.browser, 33 | ...globals.node, 34 | }, 35 | 36 | parser: tsParser, 37 | ecmaVersion: 2021, 38 | sourceType: 'module', 39 | }, 40 | 41 | rules: {}, 42 | }, 43 | { 44 | files: ['**/.eslintrc.{js,cjs}'], 45 | 46 | languageOptions: { 47 | ecmaVersion: 5, 48 | sourceType: 'script', 49 | }, 50 | }, 51 | ]; 52 | -------------------------------------------------------------------------------- /extract-inline.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'fs'; 2 | import { join, resolve } from 'path'; 3 | import glob from 'tiny-glob'; 4 | 5 | function hash(value: string) { 6 | let hash = 5381; 7 | let i = value.length; 8 | while (i) hash = (hash * 33) ^ value.charCodeAt(--i); 9 | return (hash >>> 0).toString(36); 10 | } 11 | 12 | async function removeInlineScriptAndStyle(directory: string) { 13 | console.log('Removing Inline Scripts and Styles'); 14 | const scriptRegx = /]*>([\s\S]+?)<\/script>/g; 15 | const styleRegx = /]*>([\s\S]+?)<\/style>/g; 16 | const files = await glob('**/*.html', { 17 | cwd: directory, 18 | dot: true, 19 | absolute: false, 20 | filesOnly: true, 21 | }); 22 | 23 | console.log(`Found ${files.length} files`); 24 | 25 | for (const file of files.map((f) => join(directory, f))) { 26 | console.log(`Edit file: ${file}`); 27 | let f = readFileSync(file, { encoding: 'utf-8' }); 28 | 29 | let script; 30 | while ((script = scriptRegx.exec(f))) { 31 | const inlineScriptContent = script[1] 32 | .replace('__sveltekit', 'const __sveltekit') 33 | .replace( 34 | 'document.currentScript.parentElement', 35 | 'document.body.firstElementChild' 36 | ); 37 | const fn = `/script-${hash(inlineScriptContent)}.js`; 38 | f = f.replace( 39 | script[0], // Using script[0] to replace the entire matched script tag 40 | `` 41 | ); 42 | writeFileSync(`${directory}${fn}`, inlineScriptContent); 43 | console.log(`Inline script extracted and saved at: ${directory}${fn}`); 44 | } 45 | 46 | let style; 47 | while ((style = styleRegx.exec(f))) { 48 | const inlineStyleContent = style[1]; 49 | const fn = `/style-${hash(inlineStyleContent)}.css`; 50 | f = f.replace( 51 | style[0], // Using style[0] to replace the entire matched style tag 52 | `` 53 | ); 54 | writeFileSync(`${directory}${fn}`, inlineStyleContent); 55 | console.log(`Inline style extracted and saved at: ${directory}${fn}`); 56 | } 57 | 58 | writeFileSync(file, f); 59 | } 60 | } 61 | 62 | removeInlineScriptAndStyle( 63 | resolve(import.meta.dir, 'github-repo-size-extension') 64 | ); 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-repo-size-extension", 3 | "type": "module", 4 | "version": "0.2.7", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "bun run versionCTRL && bun run astro build && bun run csp && bun bundler.ts", 9 | "build:firefox": "bun run versionCTRL firefox && bun run astro build && bun run csp && bun bundler.ts", 10 | "preview": "astro preview", 11 | "astro": "astro", 12 | "csp": "bun -b extract-inline.ts", 13 | "versionCTRL": "bun -b update-version.ts", 14 | "format": "prettier --write .", 15 | "checkFormat": "prettier --check .", 16 | "lint": "eslint ." 17 | }, 18 | "dependencies": { 19 | "@astrojs/svelte": "^4.0.2", 20 | "@astrojs/tailwind": "^5.0.0", 21 | "astro": "^3.6.5", 22 | "svelte": "^4.2.18" 23 | }, 24 | "devDependencies": { 25 | "@catppuccin/tailwindcss": "^0.1.6", 26 | "@tailwindcss/forms": "^0.5.6", 27 | "@types/chrome": "^0.0.269", 28 | "@typescript-eslint/eslint-plugin": "^7.18.0", 29 | "@typescript-eslint/parser": "^7.18.0", 30 | "astro-purgecss": "^3.2.1", 31 | "bun-types": "latest", 32 | "chalk": "5.3.0", 33 | "eslint": "^9.1.0", 34 | "prettier": "^3.3.3", 35 | "purgecss": "^5.0.0", 36 | "tailwindcss": "^3.4.7", 37 | "tiny-glob": "^0.2.9" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /page/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /page/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | 3 | import tailwind from '@astrojs/tailwind'; 4 | 5 | // https://astro.build/config 6 | export default defineConfig({ 7 | integrations: [tailwind()], 8 | site: 'https://aminoffz.github.io', 9 | base: '/github-repo-size', 10 | build: { 11 | assets: 'app', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /page/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AminoffZ/github-repo-size/3bd39aac57d18d216fbb3e78d95dca33eb10c1a5/page/bun.lockb -------------------------------------------------------------------------------- /page/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview", 10 | "astro": "astro", 11 | "docs": "bun typedoc.ts", 12 | "deploy": "bun run build && bun run docs && gh-pages -d dist", 13 | "postinstall": "patch-package" 14 | }, 15 | "dependencies": { 16 | "@astrojs/tailwind": "^5.1.3", 17 | "astro": "^5.0.5", 18 | "marked": "^15.0.4", 19 | "tailwindcss": "^3.4.16" 20 | }, 21 | "devDependencies": { 22 | "@catppuccin/tailwindcss": "^0.1.6", 23 | "@types/marked": "^6.0.0", 24 | "gh-pages": "^6.2.0", 25 | "typedoc": "^0.27.5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /page/patches/typedoc+0.25.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/typedoc/dist/lib/utils/loggers.js b/node_modules/typedoc/dist/lib/utils/loggers.js 2 | index 727254d..16b2932 100644 3 | --- a/node_modules/typedoc/dist/lib/utils/loggers.js 4 | +++ b/node_modules/typedoc/dist/lib/utils/loggers.js 5 | @@ -5,10 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) { 6 | Object.defineProperty(exports, "__esModule", { value: true }); 7 | exports.ConsoleLogger = exports.Logger = exports.LogLevel = void 0; 8 | const typescript_1 = __importDefault(require("typescript")); 9 | -const inspector_1 = require("inspector"); 10 | const path_1 = require("path"); 11 | const paths_1 = require("./paths"); 12 | -const isDebugging = () => !!(0, inspector_1.url)(); 13 | +const isDebugging = () => false; 14 | /** 15 | * List of known log levels. Used to specify the urgency of a log message. 16 | */ 17 | -------------------------------------------------------------------------------- /page/public/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AminoffZ/github-repo-size/3bd39aac57d18d216fbb3e78d95dca33eb10c1a5/page/public/.nojekyll -------------------------------------------------------------------------------- /page/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AminoffZ/github-repo-size/3bd39aac57d18d216fbb3e78d95dca33eb10c1a5/page/public/favicon.png -------------------------------------------------------------------------------- /page/src/env.d.ts: -------------------------------------------------------------------------------- 1 | import '../.astro/types.d.ts'; 2 | import 'astro/client'; 3 | -------------------------------------------------------------------------------- /page/src/pages/auth/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const progress = 120; 3 | --- 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Astro 12 | 13 | 14 |

GitHub Repo Size

15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 |

26 | Authentication complete. Your API rate limit is now 5000 requests per 27 | hour. You can now close this window. 28 |

29 |
30 | 31 | 32 | 33 | 62 | -------------------------------------------------------------------------------- /page/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { marked } from 'marked'; 3 | const response = await fetch('https://raw.githubusercontent.com/AminoffZ/github-repo-size/main/README.md'); 4 | const markdown = await response.text(); 5 | const content = marked.parse(markdown); 6 | const updatedContent = content.replace(/src="\/([^"]+)"/g, `src="${"/github-repo-size/"}$1"`); 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Astro 16 | 17 | 18 |
19 |
20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /page/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | import forms from '@tailwindcss/forms'; 2 | import catppuccin from '@catppuccin/tailwindcss'; 3 | 4 | export default { 5 | content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], 6 | theme: { 7 | extend: {}, 8 | }, 9 | safelist: [ 10 | { 11 | pattern: /bg-.+/, 12 | }, 13 | 'mocha', 14 | 'macchiato', 15 | 'frappe', 16 | 'latte', 17 | ], 18 | plugins: [ 19 | forms, 20 | catppuccin({ 21 | prefix: 'ctp', 22 | defaultFlavour: 'macchiato', 23 | }), 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /page/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": ["../src/scripts/**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /page/typedoc.ts: -------------------------------------------------------------------------------- 1 | import * as TypeDoc from 'typedoc'; 2 | import entryPoints from '../entrypoints.ts'; 3 | 4 | async function main() { 5 | // Application.bootstrap also exists, which will not load plugins 6 | // Also accepts an array of option readers if you want to disable 7 | // TypeDoc's tsconfig.json/package.json/typedoc.json option readers 8 | const entrypoints = await entryPoints({ root: '../src/scripts', deep: true }); 9 | const app = await TypeDoc.Application.bootstrapWithPlugins({ 10 | entryPoints: entrypoints, 11 | plugin: ['typedoc-plugin-extras'], 12 | name: 'GitHub Repo Size', 13 | // @ts-expect-error: This favicon URL is necessary for GitHub Repo Size extension. 14 | favicon: 15 | 'https://raw.githubusercontent.com/AminoffZ/github-repo-size/main/assets/github-repo-size-icon256.png', 16 | }); 17 | 18 | const project = await app.convert(); 19 | 20 | if (project) { 21 | // Project may not have converted correctly 22 | const outputDir = './dist/docs'; 23 | 24 | // Rendered docs 25 | await app.generateDocs(project, outputDir); 26 | // Alternatively generate JSON output 27 | await app.generateJson(project, outputDir + '/documentation.json'); 28 | } 29 | } 30 | 31 | main().catch(console.error); 32 | -------------------------------------------------------------------------------- /privacy-policy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | We take your privacy seriously. To better protect your privacy we provide this privacy policy notice explaining the way your personal information is collected and used. 4 | 5 | ## Links to Third Party Websites 6 | 7 | We have included links on this page for your use and reference. We are not responsible for the privacy policies on these websites. You should be aware that the privacy policies of these websites may differ from our own. 8 | 9 | ## OAuth 10 | 11 | We provide an easy way to authenticate GitHub's API with OAuth. As a necessary step of authenticating this way, your e-mail address will be stored in a [Supabase](https://supabase.com/) database. We **do not** use or share this information with anyone else. This is completely optional and we recommend using a GitHub PAT token for authentication which is not stored or shared at all. 12 | 13 | ## Security 14 | 15 | The security of your personal information is important to us, but remember that no method of transmission over the Internet, or method of electronic storage, is 100% secure. While we strive to use commercially acceptable means to protect your personal information, we cannot guarantee its absolute security. 16 | 17 | ## Changes To This Privacy Policy 18 | 19 | This Privacy Policy is effective as of **December 24, 2023** and will remain in effect except with respect to any changes in its provisions in the future, which will be in effect immediately after being posted on this page. 20 | 21 | We reserve the right to update or change our Privacy Policy at any time and you should check this Privacy Policy periodically. If we make any material changes to this Privacy Policy, we will be placing a prominent notice on our [GitHub](https://github.com/AminoffZ/github-repo-size) and [Chrome Web Store Page](https://chrome.google.com/webstore/detail/github-repo-size/jpdmfnflpdgefbfkafcikmhipofhanfl). 22 | -------------------------------------------------------------------------------- /public/content.css: -------------------------------------------------------------------------------- 1 | .grs { 2 | color: var(--fgColor-muted, var(--color-fg-muted)); 3 | } 4 | -------------------------------------------------------------------------------- /public/images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AminoffZ/github-repo-size/3bd39aac57d18d216fbb3e78d95dca33eb10c1a5/public/images/icon128.png -------------------------------------------------------------------------------- /public/images/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AminoffZ/github-repo-size/3bd39aac57d18d216fbb3e78d95dca33eb10c1a5/public/images/icon16.png -------------------------------------------------------------------------------- /public/images/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AminoffZ/github-repo-size/3bd39aac57d18d216fbb3e78d95dca33eb10c1a5/public/images/icon32.png -------------------------------------------------------------------------------- /public/images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AminoffZ/github-repo-size/3bd39aac57d18d216fbb3e78d95dca33eb10c1a5/public/images/icon48.png -------------------------------------------------------------------------------- /public/images/icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AminoffZ/github-repo-size/3bd39aac57d18d216fbb3e78d95dca33eb10c1a5/public/images/icon64.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "GitHub Repo Size", 4 | "author": "mouiylus@gmail.com", 5 | "description": "Show size summaries of GitHub repos", 6 | "version": "0.2.7", 7 | "web_accessible_resources": [ 8 | { 9 | "resources": ["script.js"], 10 | "matches": ["https://github.com/*"] 11 | } 12 | ], 13 | "content_scripts": [ 14 | { 15 | "matches": ["https://github.com/*"], 16 | "js": ["content.js"], 17 | "run_at": "document_end", 18 | "css": ["content.css"] 19 | } 20 | ], 21 | "action": { 22 | "default_popup": "index.html" 23 | }, 24 | "permissions": ["storage", "tabs", "webNavigation"], 25 | "background": { 26 | "service_worker": "background.js" 27 | }, 28 | "icons": { 29 | "16": "images/icon16.png", 30 | "32": "images/icon32.png", 31 | "48": "images/icon48.png", 32 | "128": "images/icon128.png" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Authenticator.svelte: -------------------------------------------------------------------------------- 1 | 47 | 48 |
49 | 54 |
55 |
56 |
Calls remaining:
57 | {#if rate} 58 |
{rate?.remaining}
59 | {:else} 60 |
0
61 | {/if} 62 |
63 | -------------------------------------------------------------------------------- /src/components/Expander.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | 29 |
30 | 31 | 39 | -------------------------------------------------------------------------------- /src/components/Popup.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | (tokenValid = event.detail)} /> 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/PopupTitle.svelte: -------------------------------------------------------------------------------- 1 |

4 | GitHub Repo Size 5 |

6 | -------------------------------------------------------------------------------- /src/components/TokenInput.svelte: -------------------------------------------------------------------------------- 1 | 63 | 64 |
69 |
74 | {#if showToken} 75 | 82 | {:else} 83 | 90 | {/if} 91 | 118 |
119 |
120 | 125 |
126 |
127 | 132 |
133 | {#if tokenUpdated} 134 | Token updated, refresh the page to see changes. 137 | {:else} 138 |
139 | {/if} 140 |
141 | 142 | 168 | -------------------------------------------------------------------------------- /src/components/TokenStatus.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

10 | Token 11 |

12 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Popup from '../components/Popup.svelte' 3 | --- 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Astro 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/scripts/background.ts: -------------------------------------------------------------------------------- 1 | import { storage } from '../shared'; 2 | 3 | /** 4 | * Listen for navigation events and send a message to the content script 5 | * to update the DOM. This is needed because GitHub uses pushState to 6 | * navigate between pages. This means that the content script is not 7 | * reloaded when the user navigates to a new page. We need to send a 8 | * message to the content script to update the DOM. We only send the 9 | * message every other time to avoid sending the message twice when 10 | * navigating to a new page. 11 | * @see https://developer.chrome.com/docs/extensions/reference/webNavigation/#event-order 12 | */ 13 | function setupNavigationHandler() { 14 | const redirects: { [url: string]: number } = {}; 15 | 16 | chrome.webNavigation.onHistoryStateUpdated.addListener(function (details) { 17 | const url = new URL(details.url); 18 | if (url.hostname !== 'github.com') { 19 | return; 20 | } 21 | redirects[url.href] = (redirects[url.href] || 0) + 1; 22 | if ((redirects[url.href] + 1) % 2 == 0) { 23 | return; 24 | } 25 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 26 | const activeTab = tabs[0]; 27 | if (!activeTab) { 28 | return; 29 | } 30 | sendMessageWithRetry(activeTab.id!, { event: 'grs-update' }); 31 | }); 32 | }); 33 | } 34 | 35 | /** 36 | * Send a message to the content script. If the message fails to send, 37 | * retry sending the message a few times. 38 | * 39 | * @param tabId - The ID of the tab to send the message to 40 | * @param message - The message to send 41 | * @param attemptsLeft - The number of attempts left to send the message 42 | * @see https://developer.chrome.com/docs/extensions/mv3/messaging/#sending-messages 43 | */ 44 | function sendMessageWithRetry( 45 | tabId: number, 46 | message: { event: string }, 47 | attemptsLeft = 3 48 | ) { 49 | chrome.tabs.sendMessage(tabId, message, async function (response) { 50 | // If an error occurs and there are attempts left, retry sending the message 51 | if (chrome.runtime.lastError || !response) { 52 | console.error(chrome.runtime.lastError); 53 | if (attemptsLeft > 0) { 54 | console.info(`Retrying... Attempts left: ${attemptsLeft}`); 55 | setTimeout(() => { 56 | sendMessageWithRetry(tabId, message, attemptsLeft - 1); 57 | }, 1000); // Wait 1 second before retrying 58 | } else { 59 | console.error('Failed to send message after all attempts'); 60 | } 61 | } else { 62 | console.info('Message sent successfully', response); 63 | } 64 | }); 65 | } 66 | 67 | /* Check if the extension has been installed before. If not, open the 68 | * extension's page in a new tab. This is to show the user how to use 69 | * the extension. The page is only opened once. The extension is considered 70 | * installed if the 'grs-installed' key is set to true in the storage. 71 | * This key is set to true when the page is opened. 72 | */ 73 | storage.get('grs-installed', (result) => { 74 | if (!(result && result['grs-installed'] === true)) { 75 | chrome.tabs.create({ 76 | url: 'https://aminoffz.github.io/github-repo-size', 77 | }); 78 | storage.set({ 79 | 'grs-installed': true, 80 | }); 81 | } 82 | }); 83 | 84 | /* Listen for updates to the tabs. If the URL is the authentication page, 85 | * extract the token from the URL and store it in the storage. 86 | */ 87 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 88 | if ( 89 | tab.url && 90 | tab.url.startsWith( 91 | 'https://aminoffz.github.io/github-repo-size/auth/#access_token=' 92 | ) 93 | ) { 94 | // Extract the token from the URL. 95 | const url: URL = new URL(tab.url); 96 | const providerTokenParam: string | undefined = url.hash 97 | .split('&') 98 | .find((param) => param.startsWith('provider_token=')); 99 | const providerToken: string | undefined = providerTokenParam?.split('=')[1]; 100 | 101 | if (providerToken) { 102 | storage.set({ 103 | 'repo-size-oauth-token': providerToken, 104 | }); 105 | } 106 | } 107 | }); 108 | 109 | /* Listen for messages from the popup. If the message is 'authenticate', open the 110 | * authentication page in a new tab using the URL from the message payload. 111 | */ 112 | chrome.runtime.onMessage.addListener((request) => { 113 | if (request.action === 'grs-authenticate') { 114 | chrome.tabs.create({ 115 | url: request.data, 116 | }); 117 | } 118 | }); 119 | 120 | setupNavigationHandler(); 121 | -------------------------------------------------------------------------------- /src/scripts/content.ts: -------------------------------------------------------------------------------- 1 | import { updateDOM } from './internal'; 2 | 3 | /** 4 | * Update the DOM. 5 | * If our elements have not been added, wait 500 ms and try again. 6 | * If we have tried 5 times, give up. 7 | * @param attempts - The number of times we have tried to update the DOM 8 | */ 9 | async function main(attempts: number) { 10 | setTimeout(async () => await updateDOM(), 500); 11 | const grsElements = document.getElementsByClassName('grs'); 12 | if (grsElements.length < 2) { 13 | if (attempts >= 4) { 14 | console.warn('GRS: Could not find any elements to update, stopping.'); 15 | return false; 16 | } 17 | setTimeout(async () => await main((attempts += 1)), 500); 18 | } else { 19 | return true; 20 | } 21 | } 22 | 23 | /** 24 | * Listen for messages from the background script. If the message is 25 | * 'grs-update', update the DOM. We add a delay to increase that the 26 | * likelyhood that the DOM has been changed before we try to update it. 27 | * Resets the attempts counter. Send a response to the background script. 28 | * 29 | * @param request - The message from the background script 30 | * @param sender - The sender of the message 31 | * @param sendResponse - The function to call when we are done 32 | */ 33 | chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { 34 | if (request.event === 'grs-update') { 35 | sendResponse(true); 36 | setTimeout(async () => await main(0), 750); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /src/scripts/internal/api.ts: -------------------------------------------------------------------------------- 1 | import { getToken, setDefaultBranch } from '../../shared'; 2 | import type { GitHubTree } from './types'; 3 | 4 | /** 5 | * Get the API URL. 6 | * 7 | * @param repo - The repo name 8 | * @param branch - The branch name 9 | * @returns The API URL 10 | * @example 11 | * ```ts 12 | * API('AminoffZ/github-repo-size', 'main'); 13 | * // 'https://api.github.com/repos/AminoffZ/github-repo-size/git/trees/main?recursive=1' 14 | * ``` 15 | */ 16 | function API(repo: string, branch: string) { 17 | return `https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1`; 18 | } 19 | 20 | /** 21 | * Generate the headers for the request, includes the token if it exists. 22 | * @returns The headers object 23 | * @example 24 | * ```ts 25 | * createHeaders(); 26 | * // Headers { 27 | * // 'User-Agent': 'AminoffZ/github-repo-size', 28 | * // 'Authorization': 'Bearer ...' 29 | * // } 30 | * ``` 31 | */ 32 | async function createHeaders() { 33 | const headers = new Headers(); 34 | headers.append('User-Agent', 'AminoffZ/github-repo-size'); 35 | const token = await getToken(); 36 | if (token) { 37 | headers.append('Authorization', `Bearer ${token}`); 38 | } 39 | return headers; 40 | } 41 | 42 | /** 43 | * Create a tree request object. 44 | * 45 | * @param repo - The repo name 46 | * @param branch - The branch name 47 | * @returns The tree request object 48 | * @example 49 | * ```ts 50 | * createTreeRequest('AminoffZ/github-repo-size', 'main'); 51 | * // Request { 52 | * // url: 'https://api.github.com/repos/AminoffZ/github-repo-size/git/trees/main?recursive=1', 53 | * // method: 'GET', 54 | * // headers: Headers { 'User-Agent': 'AminoffZ/github-repo-size' }, 55 | * // ... 56 | * // } 57 | * ``` 58 | */ 59 | async function createTreeRequest(repo: string, branch: string) { 60 | const headers = await createHeaders(); 61 | const request = new Request(API(repo, branch), { 62 | headers, 63 | }); 64 | return request; 65 | } 66 | 67 | /** 68 | * Get the default branch of a repo from the GitHub API. 69 | * 70 | * @returns The default branch name 71 | * @example 72 | * ```ts 73 | * getDefaultBranch(); 74 | * // 'main' 75 | * ``` 76 | */ 77 | async function getDefaultBranch(repo: string) { 78 | let branch = ''; 79 | const headers = await createHeaders(); 80 | await fetch(`https://api.github.com/repos/${repo}`, { headers }) 81 | .then(async (res) => { 82 | const data = await res.json(); 83 | branch = data.default_branch; 84 | }) 85 | .catch(async (err) => console.error(err)); 86 | return branch; 87 | } 88 | 89 | /** 90 | * Check if the response has errors. 91 | * 92 | * @param res - The response 93 | * @returns True if the response has errors 94 | * @example 95 | * ```ts 96 | * hasErrors(res); 97 | * // false 98 | * ``` 99 | */ 100 | async function hasErrors(res: Response) { 101 | return !res.ok && res.status === 404 && res.type === 'cors'; 102 | } 103 | 104 | /** 105 | * Get the repo info from the GitHub API using the repo name and branch name. 106 | * If the request fails, assume that the branch name is incorrect and try again. 107 | * If the default branch name is not found, the function will return undefined. 108 | * 109 | * @param repo - The repo name 110 | * @param branch - (Optional) The branch name (default: 'main') 111 | * @param attempts - The number of attempts 112 | * @returns The repo info 113 | * @example 114 | * ```ts 115 | * getRepoInfo('AminoffZ/github-repo-size'); 116 | * // { 117 | * // sha: '...', 118 | * // url: '...', 119 | * // tree: [ 120 | * // { 121 | * // path: '...', 122 | * // mode: '...', 123 | * // type: '...', 124 | * // sha: '...', 125 | * // size: 0, 126 | * // url: '...', 127 | * // }, 128 | * // ... 129 | * // ], 130 | * // truncated: false, 131 | * // } 132 | */ 133 | export async function getRepoInfo( 134 | repo: string, 135 | branch: string = 'main', 136 | attempts: number = 0 137 | ): Promise { 138 | const branchName = (await getDefaultBranch(repo)) || branch; 139 | const request = await createTreeRequest(repo, branchName); 140 | const response = await fetch(request).then(async (res) => { 141 | if (await hasErrors(res)) { 142 | if (attempts < 1) { 143 | const defaultBranch = await getDefaultBranch(repo); 144 | return getRepoInfo(repo, defaultBranch, attempts + 1); 145 | } 146 | } 147 | if (attempts > 0) { 148 | await setDefaultBranch(repo, branch); 149 | } 150 | const data = await res.json(); 151 | return data as GitHubTree; 152 | }); 153 | return response; 154 | } 155 | -------------------------------------------------------------------------------- /src/scripts/internal/crypto.ts: -------------------------------------------------------------------------------- 1 | // 🤓 will 😂 2 | /** 3 | * djb2 hash function. 4 | * 5 | * @param string - The string to hash 6 | * @returns The hash 7 | * @example 8 | * ```ts 9 | * djb2('💩'); 10 | * // 7743179 11 | * ``` 12 | */ 13 | function djb2(string: string) { 14 | let hash = '🔏'.codePointAt(0)! & 0x1505; // 5381. 15 | for (let i = 0; i < string.length; i++) { 16 | hash = ((hash << 5) + hash + string.charCodeAt(i)) & 0x7fffffff; // multiply by 33 and force positive. 17 | } 18 | return hash; 19 | } 20 | 21 | /** 22 | * Hash a string to a class name. 23 | * 24 | * @param toHash - The string to hash 25 | * @param prefix - The prefix of the class name 26 | * @returns The class name 27 | * @example 28 | * ```ts 29 | * hashClass('💩', 'grs'); 30 | * // 'grs-7743179' 31 | * ``` 32 | */ 33 | export function hashClass(toHash: string, prefix: string = 'grs') { 34 | return `${prefix}-${djb2(toHash)}`; 35 | } 36 | -------------------------------------------------------------------------------- /src/scripts/internal/dom-manipulation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createSizeLabel, 3 | createSizeSpan, 4 | createEmptySizeSpan, 5 | createTotalSizeElement, 6 | formatBytes, 7 | getFileAnchors, 8 | getFirstTd, 9 | getNavButtons, 10 | getPathObject, 11 | getRepoInfo, 12 | getSize, 13 | getSizeLabel, 14 | getTable, 15 | getThead, 16 | getTotalSizeButton, 17 | getTotalSizeSpan, 18 | } from '.'; 19 | import type { GRSUpdate, GitHubTree } from './types'; 20 | 21 | /** 22 | * Insert the size label element into the table head. 23 | * This is the element that is shown at the top of the size column in the GitHub file browser. 24 | * 25 | * @param headRow - The head row element 26 | * @param th - The size label element 27 | */ 28 | function insertSizeLabel(headRow: ChildNode, th: HTMLTableCellElement) { 29 | if (headRow) { 30 | headRow.insertBefore(th, headRow.childNodes[headRow.childNodes.length - 1]); 31 | } 32 | } 33 | 34 | /** 35 | * Expand the navigate up element to span the entire table. 36 | * This is the element that is shown at the top of the GitHub file browser 37 | * and is used to navigate up in the file tree. 38 | */ 39 | function expandFirstTd() { 40 | const firstTd = getFirstTd(); 41 | if (!firstTd) { 42 | console.warn('Could not find first td.'); 43 | return; 44 | } 45 | firstTd.setAttribute('colspan', '4'); 46 | } 47 | 48 | /** 49 | * Insert the size column into the table on the GitHub file browser. 50 | * This is the column that displays the size of each file. 51 | */ 52 | function insertSizeColumn() { 53 | if (getSizeLabel()) { 54 | console.warn('Size label already exists.'); 55 | return; 56 | } 57 | 58 | const thead = getThead(); 59 | const headRow = thead?.firstChild; 60 | if (!headRow) { 61 | return; 62 | } 63 | 64 | expandFirstTd(); 65 | 66 | const th = createSizeLabel(); 67 | insertSizeLabel(headRow, th); 68 | } 69 | 70 | /** 71 | * Insert when in the GitHub file browser. 72 | * 73 | * @param anchor - The anchor element as reference 74 | * @param span - The span element to insert 75 | */ 76 | function insertToFileExplorer( 77 | anchor: HTMLAnchorElement, 78 | span: HTMLSpanElement 79 | ) { 80 | const row = anchor.closest('tr'); 81 | if (!row) { 82 | return; 83 | } 84 | 85 | const td = row.childNodes[row.childNodes.length - 2].cloneNode(false); 86 | if (!(td instanceof HTMLElement)) { 87 | return; 88 | } 89 | 90 | td.classList.add('grs'); 91 | td.style.setProperty('text-wrap', 'nowrap'); 92 | td.style.setProperty('text-align', 'right'); 93 | td.appendChild(span); 94 | row.insertBefore(td, row.childNodes[row.childNodes.length - 1]); 95 | } 96 | 97 | /** 98 | * Set the total size of the files in the repository. 99 | * This concerns the element shown in the navigation bar next to Settings. 100 | * 101 | * @param repoInfo - The repo info 102 | */ 103 | function setTotalSize(repoInfo: GitHubTree) { 104 | const navButtons = getNavButtons(); 105 | if (!navButtons) { 106 | return; 107 | } 108 | 109 | let totalSizeButton = getTotalSizeButton(); 110 | 111 | // Check if total size button already exists 112 | if (!totalSizeButton) { 113 | totalSizeButton = createTotalSizeElement(); 114 | 115 | // If creating the button fails, exit the function 116 | if (!totalSizeButton) { 117 | return; 118 | } 119 | } 120 | 121 | const existingCounterSpan = totalSizeButton.querySelector('span.Counter'); 122 | if (existingCounterSpan) { 123 | existingCounterSpan.remove(); 124 | } 125 | 126 | const span = getTotalSizeSpan(totalSizeButton); 127 | if (!span) { 128 | return; 129 | } 130 | 131 | let totalSize = 0; 132 | repoInfo.tree.forEach((item) => { 133 | totalSize += item.size ?? 0; 134 | }); 135 | 136 | span.innerText = formatBytes(totalSize); 137 | 138 | navButtons.appendChild(totalSizeButton); 139 | } 140 | 141 | /** 142 | * Update the DOM. 143 | * This is the main function that is called when the DOM should be updated. 144 | */ 145 | export async function updateDOM() { 146 | const table = getTable(); 147 | if (!table) { 148 | console.warn('Could not find file table.'); 149 | return; 150 | } 151 | 152 | const anchors = getFileAnchors(table); 153 | if (!anchors || anchors.length === 0) { 154 | console.warn('Could not find any file anchors.'); 155 | return; 156 | } 157 | 158 | const pathObject = getPathObject(); 159 | if (!pathObject || !pathObject.owner || !pathObject.repo) { 160 | console.warn('Could not get path object.'); 161 | return; 162 | } 163 | 164 | const type = pathObject.type; 165 | let branch = pathObject.branch; 166 | if (type !== 'tree' && type !== 'blob') { 167 | branch = 'main'; 168 | } 169 | 170 | const repoInfo = await getRepoInfo( 171 | pathObject.owner + '/' + pathObject.repo, 172 | branch 173 | ); 174 | if (!repoInfo || !repoInfo.tree) { 175 | const warnMessage = ` 176 | Could not get repo info, aborting...\n 177 | Click the extension button to see remaining requests.\n 178 | If you see 0 remaining requests, you have exceeded the rate limit.\n 179 | Use OAuth or a personal access token to increase the rate limit. 180 | `; 181 | console.warn(warnMessage); 182 | return false; 183 | } 184 | 185 | const updates: GRSUpdate = []; 186 | 187 | for (let index = 0; index < anchors.length; index++) { 188 | const anchor = anchors[index]; 189 | const anchorPath = anchor.getAttribute('href'); 190 | if (!anchorPath) { 191 | console.warn('Could not get anchor path.'); 192 | return; 193 | } 194 | 195 | const anchorPathObject = getPathObject(anchorPath); 196 | let size: number; 197 | let span: HTMLSpanElement | undefined; 198 | if (!repoInfo.tree.some((file) => file.path === anchorPathObject.path)) { 199 | console.warn('Could not find file in repo info.'); 200 | span = createEmptySizeSpan(anchorPath); 201 | } else { 202 | size = getSize(anchorPathObject, repoInfo.tree); 203 | span = createSizeSpan(anchorPath, size); 204 | } 205 | 206 | if (!span) { 207 | console.warn('Could not create size span.'); 208 | return; 209 | } 210 | 211 | updates.push({ anchor, span, index }); 212 | } 213 | 214 | insertSizeColumn(); 215 | 216 | setTotalSize(repoInfo); 217 | 218 | updates.forEach(({ anchor, span, index }) => { 219 | // for some reason the rows have two td's with name of each file 220 | if (index % 2 === 1) { 221 | insertToFileExplorer(anchor, span); 222 | } 223 | }); 224 | } 225 | -------------------------------------------------------------------------------- /src/scripts/internal/element-factory.ts: -------------------------------------------------------------------------------- 1 | import { formatBytes, hashClass } from '.'; 2 | 3 | /** 4 | * Create a table header element for the size column. 5 | * 6 | * @returns The table header element 7 | * @example 8 | * ```ts 9 | * createSizeLabel(); 10 | * // Size 11 | * ``` 12 | */ 13 | export function createSizeLabel() { 14 | const th = document.createElement('th'); 15 | const span = document.createElement('span'); 16 | span.innerText = 'Size'; 17 | th.className = 'grs grs-size'; 18 | th.style.setProperty('text-align', 'right'); 19 | th.style.setProperty('text-wrap', 'nowrap'); 20 | th.style.setProperty('overflow', 'hidden'); 21 | th.appendChild(span); 22 | return th; 23 | } 24 | 25 | /** 26 | * Create a total size element. The element is shown at the end of the navigation bar. 27 | * It displays the total size of the repository. 28 | * 29 | * @returns The total size element 30 | * @example 31 | * ```ts 32 | * createTotalSizeElement(); 33 | * //
  • 34 | * // database-icon 35 | * // ... 36 | * //
  • 37 | * ``` 38 | */ 39 | export function createTotalSizeElement() { 40 | const totalSizeButton = document.createElement('li'); 41 | // unique identifier and GitHub's li style class 42 | totalSizeButton.classList.add('grs-total-size', 'd-inline-flex'); 43 | // align the svg icon with the text 44 | totalSizeButton.style.setProperty('align-items', 'center'); 45 | // create the span element that will contain the total size and append it to the button 46 | const span = createTotalSizeSpan(); 47 | totalSizeButton.appendChild(span); 48 | // add the 'database' icon 49 | span.insertAdjacentHTML( 50 | 'beforebegin', 51 | ` 52 | 53 | database 54 | 55 | 56 | ` 57 | ); 58 | return totalSizeButton; 59 | } 60 | 61 | function createTotalSizeSpan() { 62 | const span = document.createElement('span'); 63 | // add loading dots to be replaced by the total size 64 | span.innerText = '...'; 65 | return span; 66 | } 67 | 68 | /** 69 | * Create a span element to display the size of a file or directory. 70 | * 71 | * @param anchorPath - The path of the anchor element used as reference 72 | * @param size - The size of the file or directory 73 | * @returns The size span element 74 | * @example 75 | * ```ts 76 | * createSizeSpan(anchorPath, size); 77 | * // ... 78 | * ``` 79 | */ 80 | export function createSizeSpan(anchorPath: string, size: number) { 81 | const sizeString = formatBytes(size); 82 | const span = document.createElement('span'); 83 | const spanClass = hashClass(anchorPath); 84 | span.classList.add('grs', spanClass); 85 | 86 | if (document.querySelector(`span.${spanClass}`)) { 87 | console.warn(`Duplicate span class: ${spanClass}`); 88 | return; 89 | } 90 | 91 | span.innerText = sizeString; 92 | return span; 93 | } 94 | 95 | /** 96 | * Create an empty size span element. 97 | * 98 | * @param anchorPath - The path of the anchor element used as reference 99 | * @returns The empty size span element 100 | * @example 101 | * ```ts 102 | * createEmptySizeSpan(anchorPath); 103 | * // ... 104 | * ``` 105 | */ 106 | export function createEmptySizeSpan(anchorPath: string) { 107 | const span = document.createElement('span'); 108 | const spanClass = hashClass(anchorPath); 109 | span.classList.add('grs', spanClass); 110 | 111 | if (document.querySelector(`span.${spanClass}`)) { 112 | console.warn(`Duplicate span class: ${spanClass}`); 113 | return; 114 | } 115 | 116 | span.innerText = '...'; 117 | return span; 118 | } 119 | -------------------------------------------------------------------------------- /src/scripts/internal/format.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats a number to a string with a given amount of decimals. 3 | * 4 | * @param bytes - The number to format 5 | * @param decimals - The number of decimals 6 | * @returns The formatted string 7 | * @example 8 | * ```ts 9 | * formatBytes(1024); 10 | * // '1 KB' 11 | * ``` 12 | * @example 13 | * ```ts 14 | * formatBytes(1024, 2); 15 | * // '1.00 KB' 16 | * ``` 17 | */ 18 | export function formatBytes(bytes: number, decimals: number = 2): string { 19 | if (bytes === 0) return '0 B'; 20 | const k = 1024; 21 | const dm = decimals < 0 ? 0 : decimals; 22 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 23 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 24 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; 25 | } 26 | -------------------------------------------------------------------------------- /src/scripts/internal/get-path-object.ts: -------------------------------------------------------------------------------- 1 | import type { PathObject } from './types'; 2 | 3 | /** 4 | * Get the path object from a path string 5 | * 6 | * @param path - The path string 7 | * @returns PathObject 8 | * @example 9 | * ```ts 10 | * getPathObject('/owner/repo/tree/branch/path/to/file'); 11 | * // { 12 | * // owner: 'owner', 13 | * // repo: 'repo', 14 | * // type: 'tree', 15 | * // branch: 'branch', 16 | * // path: 'path/to/file', 17 | * // } 18 | * ``` 19 | */ 20 | export const getPathObject = (path?: string) => { 21 | path = path ?? window.location.pathname; 22 | const pathObject = {}; 23 | try { 24 | const paths = path.split('/'); 25 | Object.assign(pathObject, { 26 | owner: paths.at(1), 27 | repo: paths.at(2), 28 | type: paths.at(3), 29 | branch: paths.at(4), 30 | path: paths.slice(5)?.join('/'), 31 | }); 32 | } catch (e) { 33 | console.error(e); 34 | } 35 | return pathObject as PathObject; 36 | }; 37 | -------------------------------------------------------------------------------- /src/scripts/internal/get-size.ts: -------------------------------------------------------------------------------- 1 | import type { GitHubTreeItem, PathObject } from './types'; 2 | 3 | /** 4 | * Get the size of a path object. 5 | * 6 | * @param anchorPath - The path object 7 | * @param tree - The tree 8 | * @returns The size of the path object 9 | * @example 10 | * ```ts 11 | * getSize({ 12 | * owner: 'AminoffZ', 13 | * repo: 'github-repo-size', 14 | * type: 'blob', 15 | * branch: 'main', 16 | * path: '.prettierrc', 17 | * }, tree); 18 | * // 85 19 | * ``` 20 | */ 21 | export function getSize(anchorPath: PathObject, tree: GitHubTreeItem[]) { 22 | let size = 0; 23 | if (anchorPath.type === 'blob') { 24 | return tree.find((item) => item.path === anchorPath.path)?.size ?? 0; 25 | } 26 | const nestedItems = tree.filter((item) => { 27 | return item.path.startsWith(anchorPath.path + '/'); 28 | }); 29 | nestedItems.forEach((item) => { 30 | size += item.size ?? 0; 31 | }); 32 | return size; 33 | } 34 | -------------------------------------------------------------------------------- /src/scripts/internal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-path-object'; 2 | export * from './api'; 3 | export * from './format'; 4 | export * from './crypto'; 5 | export * from './selectors'; 6 | export * from './element-factory'; 7 | export * from './dom-manipulation'; 8 | export * from './get-size'; 9 | -------------------------------------------------------------------------------- /src/scripts/internal/selectors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets the nav button list. 3 | * 4 | * @returns element containing the nav buttons 5 | */ 6 | export function getNavButtons() { 7 | return document.querySelector('.js-repo-nav > ul') as HTMLUListElement | null; 8 | } 9 | 10 | /** 11 | * Get the filename anchor elements. 12 | * 13 | * @param table - The table element 14 | * @returns The filename anchor elements 15 | * @example 16 | * ```ts 17 | * getFileAnchors(table); 18 | * // { 19 | * // "0": {}, 20 | * // "1": {}, 21 | * // "2": {}, 22 | * // ... 23 | * // } 24 | * ``` 25 | */ 26 | export function getFileAnchors(table: HTMLTableElement) { 27 | return table.querySelectorAll( 28 | 'a.Link--primary' 29 | ) as NodeListOf | null; 30 | } 31 | 32 | /** 33 | * Get the total size element. 34 | * 35 | * @returns The total size element 36 | * @example 37 | * ```ts 38 | * getTotalSizeButton(); 39 | * // 40 | * // ... 41 | * // 42 | * ``` 43 | */ 44 | export function getTotalSizeButton() { 45 | return document.querySelector('.grs-total-size') as HTMLElement | undefined; 46 | } 47 | 48 | /** 49 | * Get the size label element. This is the element that is shown at the top 50 | * of the size column in the GitHub file browser. 51 | * 52 | * @returns The size label element 53 | * @example 54 | * ```ts 55 | * getSizeLabel(); 56 | * // Size 57 | * ``` 58 | */ 59 | export function getSizeLabel() { 60 | return document.querySelector('th.grs-size') as HTMLElement | undefined; 61 | } 62 | 63 | /** 64 | * Get the head of the table in GitHub's file browser. 65 | * 66 | * @returns The table head element 67 | * @example 68 | * ```ts 69 | * getThead(); 70 | * // ... 71 | * ``` 72 | */ 73 | export function getThead() { 74 | return document.querySelector('thead'); 75 | } 76 | 77 | /** 78 | * Get the span element that displays the total size of the files in the repository. 79 | * 80 | * @param totalSizeButton - The total size element 81 | * @returns The span element 82 | * @example 83 | * ```ts 84 | * getTotalSizeSpan(totalSizeButton); 85 | * // ... 86 | * ``` 87 | * @example 88 | * ```ts 89 | * getTotalSizeSpan(totalSizeButton); 90 | * // 806.07 KB 91 | * ``` 92 | */ 93 | export function getTotalSizeSpan(totalSizeButton: HTMLElement) { 94 | return totalSizeButton.querySelector('span'); 95 | } 96 | 97 | /** 98 | * Gets the top td in GitHubs file browser. 99 | * 100 | * @returns The top element 101 | * @example 102 | * ```ts 103 | * getFirstTd(); 104 | * // 105 | * //

    106 | * // 107 | * //
    108 | * // 109 | * // 110 | * // 111 | * // .. 112 | * //
    113 | * //
    114 | * // 115 | * ``` 116 | */ 117 | export function getFirstTd() { 118 | return getTable()?.querySelector('td') as HTMLTableRowElement | null; 119 | } 120 | 121 | /** 122 | * Gets the table in GitHubs file browser. 123 | * 124 | * @returns The table element 125 | * @example 126 | * ```ts 127 | * getTable(); 128 | * // 129 | * // ... 130 | * // ... 131 | * //
    132 | * ``` 133 | */ 134 | export function getTable() { 135 | return document.querySelector( 136 | 'table[aria-labelledby="folders-and-files"]' 137 | ) as HTMLTableElement | null; 138 | } 139 | -------------------------------------------------------------------------------- /src/scripts/internal/types/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | PathObject, 3 | GitHubTree, 4 | GitHubTreeItem, 5 | GRSUpdate, 6 | } from './types'; 7 | -------------------------------------------------------------------------------- /src/scripts/internal/types/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Object to provide context. 3 | */ 4 | export type PathObject = { 5 | /** 6 | * The owner of the repository. 7 | */ 8 | owner: string | undefined; 9 | /** 10 | * The repository name. 11 | */ 12 | repo: string | undefined; 13 | /** 14 | * The type of the path. 15 | */ 16 | type?: PathType | undefined; 17 | /** 18 | * The branch name. 19 | */ 20 | branch?: string | undefined; 21 | /** 22 | * The path to the file. Indicates nesting. 23 | */ 24 | path?: string | undefined; 25 | }; 26 | 27 | /** 28 | * The type of the path. A tree represents a directory and a blob represents a file. 29 | */ 30 | export type PathType = 'tree' | 'blob'; 31 | 32 | export type GitHubTreeItem = { 33 | path: string; 34 | mode: string; 35 | type: string; 36 | sha: string; 37 | size?: number; 38 | url: string; 39 | }; 40 | 41 | export type GitHubTree = { 42 | sha: string; 43 | url: string; 44 | tree: GitHubTreeItem[]; 45 | truncated: boolean; 46 | }; 47 | 48 | export type GRSUpdate = Array<{ 49 | anchor: HTMLAnchorElement; 50 | span: HTMLSpanElement; 51 | index: number; 52 | }>; 53 | -------------------------------------------------------------------------------- /src/shared/branch.ts: -------------------------------------------------------------------------------- 1 | import { storage } from '.'; 2 | 3 | /** 4 | * Get the default branch for a repo. 5 | * 6 | * @param repo - The repo name 7 | * @returns The default branch name 8 | * @example 9 | * ```ts 10 | * getDefaultBranch('neovim/neovim'); 11 | * // 'master' 12 | * ``` 13 | */ 14 | export async function getDefaultBranch(repo: string) { 15 | const storedBranch = await storage.get(repo); 16 | return storedBranch[repo]; 17 | } 18 | 19 | /** 20 | * Set the default branch for a repo. 21 | * 22 | * @param repo - The repo name 23 | * @param branch - The branch name 24 | * @example 25 | * ```ts 26 | * setDefaultBranch('AminoffZ/github-repo-size', 'master'); 27 | * ``` 28 | */ 29 | export async function setDefaultBranch(repo: string, branch: string) { 30 | await storage.set({ 31 | [repo]: branch, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/shared/get-token.ts: -------------------------------------------------------------------------------- 1 | import { GITHUB_TOKEN_KEY, OAUTH_TOKEN_KEY, storage } from '.'; 2 | 3 | /** 4 | * Get the token from the storage. If the token is not found, return undefined. 5 | * 6 | * @returns The token 7 | * @example 8 | * ```ts 9 | * getToken(); 10 | * // 'ghp_abcdefghijklmnopqrstuvwxyzABCD012345' 11 | * ``` 12 | */ 13 | export const getToken = async () => { 14 | const oauthToken: string | undefined = (await storage.get(OAUTH_TOKEN_KEY))[ 15 | OAUTH_TOKEN_KEY 16 | ]; 17 | const githubToken: string | undefined = (await storage.get(GITHUB_TOKEN_KEY))[ 18 | GITHUB_TOKEN_KEY 19 | ]; 20 | const token = githubToken || oauthToken; 21 | return token; 22 | }; 23 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export { GITHUB_TOKEN_KEY, OAUTH_TOKEN_KEY } from './storage-keys'; 2 | export { default as storage } from './storage'; 3 | export { getToken } from './get-token'; 4 | export { getDefaultBranch, setDefaultBranch } from './branch'; 5 | -------------------------------------------------------------------------------- /src/shared/storage-keys.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * OAuth token key. Used to store the OAuth token in the storage. The token is 3 | * used to authenticate requests to the GitHub API, increasing the rate limit. 4 | */ 5 | export const OAUTH_TOKEN_KEY = 'repo-size-oauth-token'; 6 | /** 7 | * GitHub PAT. Used to store the GitHub PAT in the storage. 8 | * The token is used to authenticate requests to the GitHub API 9 | * to see the sizes of private repos. 10 | */ 11 | export const GITHUB_TOKEN_KEY = 'x-github-token'; 12 | -------------------------------------------------------------------------------- /src/shared/storage.ts: -------------------------------------------------------------------------------- 1 | let storage; 2 | 3 | if (typeof chrome !== 'undefined') { 4 | storage = chrome.storage.sync || chrome.storage.local; 5 | } 6 | 7 | export default storage! as chrome.storage.SyncStorageArea; 8 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@astrojs/svelte'; 2 | 3 | export default { 4 | preprocess: vitePreprocess(), 5 | }; 6 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | /** @type {import('tailwindcss').Config} */ 3 | module.exports = { 4 | content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], 5 | theme: { 6 | extend: {}, 7 | }, 8 | safelist: [ 9 | { 10 | pattern: /bg-.+/, 11 | }, 12 | 'mocha', 13 | 'macchiato', 14 | 'frappe', 15 | 'latte', 16 | ], 17 | plugins: [ 18 | require('@tailwindcss/forms'), 19 | require('@catppuccin/tailwindcss')({ 20 | prefix: 'ctp', 21 | defaultFlavour: 'frappe', 22 | }), 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "types": [ 5 | "bun-types", // add Bun type definitions 6 | "./types.d.ts", // add your own type definitions 7 | "@types/chrome" 8 | ], 9 | 10 | // enable latest features 11 | "lib": ["esnext"], 12 | "module": "esnext", 13 | "target": "esnext", 14 | 15 | // if TS 5.x+ 16 | "moduleResolution": "bundler", 17 | "noEmit": true, 18 | "allowImportingTsExtensions": true, 19 | "moduleDetection": "force", 20 | // if TS 4.x or earlier 21 | // "moduleResolution": "nodenext", 22 | 23 | "jsx": "react-jsx", // support JSX 24 | "allowJs": true, // allow importing `.js` from `.ts` 25 | "esModuleInterop": true, // allow default imports for CommonJS modules 26 | 27 | // best practices 28 | "strict": true, 29 | "forceConsistentCasingInFileNames": true, 30 | "skipLibCheck": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/oven-sh/bun/issues/358#issuecomment-1715648224 2 | 3 | /// 4 | -------------------------------------------------------------------------------- /update-version.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { readFile, writeFile } from 'fs/promises'; 3 | import chalk from 'chalk'; 4 | 5 | async function updateManifestVersion() { 6 | try { 7 | const arg = process.argv[2]; 8 | const browser = arg && arg === 'firefox' ? 'firefox' : 'chrome'; 9 | 10 | const packageJsonPath = join(import.meta.dir, 'package.json'); 11 | const { version } = await import(packageJsonPath); 12 | 13 | const manifestPath = join(import.meta.dir, 'public', 'manifest.json'); 14 | const manifestData = await readFile(manifestPath, 'utf-8'); 15 | const manifest = JSON.parse(manifestData); 16 | 17 | manifest.version = version; 18 | 19 | if (browser && browser === 'firefox') { 20 | manifest.background = { 21 | scripts: ['background.js'], 22 | }; 23 | 24 | manifest.browser_specific_settings = { 25 | gecko: { 26 | id: 'github-repo-size@gmail.com', 27 | strict_min_version: '42.0', 28 | }, 29 | }; 30 | } else { 31 | manifest.background = { 32 | service_worker: 'background.js', 33 | }; 34 | delete manifest.browser_specific_settings; 35 | } 36 | await writeFile(manifestPath, JSON.stringify(manifest, null, 2)); 37 | 38 | console.log( 39 | chalk.green( 40 | 'Version updated successfully in manifest.json for ' + 41 | browser + 42 | ' Browser!' 43 | ) 44 | ); 45 | } catch (error) { 46 | console.error(chalk.red('Error updating version: ' + error)); 47 | process.exit(1); 48 | } 49 | } 50 | 51 | updateManifestVersion(); 52 | --------------------------------------------------------------------------------