├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ └── config.yml ├── dependabot.yml └── workflows │ ├── submit.yml │ └── test.yml ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .prettierignore ├── .prettierrc.json ├── .puppeteerrc.cjs ├── LICENSE ├── README.md ├── SECURITY.md ├── assets └── icon.png ├── dev ├── build.sh └── http-file-sync │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── pkg │ ├── manifest.go │ ├── server-sync.go │ └── watch-sync.go ├── docs ├── github-injected.png └── safari-confirm.png ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── src ├── background.ts ├── button │ ├── CaretForProvider.tsx │ ├── button-contributions.ts │ ├── button.css │ ├── button.tsx │ └── logo-mark.svg ├── components │ └── forms │ │ ├── Button.tsx │ │ ├── CheckboxInputField.tsx │ │ ├── InputField.tsx │ │ ├── InputFieldHint.tsx │ │ └── TextInputField.tsx ├── constants.ts ├── contents │ ├── button.tsx │ └── gitpod-dashboard.ts ├── hooks │ └── use-temporary-state.ts ├── popup.css ├── popup.tsx ├── storage.ts ├── style.css └── utils │ ├── parse-endpoint.spec.ts │ ├── parse-endpoint.ts │ └── permissions.ts ├── tailwind.config.js ├── test ├── package.json ├── pnpm-lock.yaml ├── src │ ├── button-contributions-copy.ts │ └── button-contributions.spec.ts └── tsconfig.json └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint", "react"], 13 | "rules": { 14 | "react/react-in-jsx-scope": "off" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Security 3 | url: https://www.gitpod.io/security 4 | about: Please report security vulnerabilities here. 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "npm" 6 | directory: "/" 7 | schedule: 8 | interval: "monthly" 9 | 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /.github/workflows/submit.yml: -------------------------------------------------------------------------------- 1 | name: "Submit to Web Stores" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | channel: 6 | description: "Release channel to publish to" 7 | required: true 8 | type: choice 9 | options: 10 | - staging 11 | - production 12 | default: staging 13 | 14 | env: 15 | CHANNEL: ${{ github.event.inputs.channel || 'staging' }} 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Cache pnpm modules 23 | uses: actions/cache@v4 24 | with: 25 | path: ~/.pnpm-store 26 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 27 | restore-keys: | 28 | ${{ runner.os }}- 29 | - uses: pnpm/action-setup@v4 30 | with: 31 | version: latest 32 | run_install: true 33 | - name: Use Node.js 20.x 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: 20.x 37 | cache: "pnpm" 38 | - name: Change version (Staging) 39 | if: env.CHANNEL == 'staging' 40 | run: | 41 | DATE=$(date +'%Y%m%d') 42 | BASE_VERSION=$(jq -r .version package.json | cut -d'-' -f1) 43 | 44 | NEW_VERSION="${BASE_VERSION}-${DATE}" 45 | jq --arg version "$NEW_VERSION" '.version = $version' package.json > package_tmp.json && mv package_tmp.json package.json 46 | 47 | - name: Build the extension (Chrome) 48 | run: pnpm plasmo build --target=chrome-mv3 49 | - name: Build the extension (Firefox) 50 | run: | 51 | pnpm plasmo build --target=firefox-mv3 52 | - name: Package the extension into zip artifacts 53 | run: | 54 | pnpm package --target=chrome-mv3 55 | pnpm package --target=firefox-mv3 56 | - name: Browser Platform Publish (staging) 57 | uses: PlasmoHQ/bpp@v3 58 | if: env.CHANNEL == 'staging' 59 | with: 60 | keys: ${{ secrets.SUBMIT_KEYS_STAGING }} 61 | verbose: true 62 | chrome-file: build/chrome-mv3-prod.zip 63 | firefox-file: build/firefox-mv3-prod.zip 64 | - name: Browser Platform Publish (production) 65 | uses: PlasmoHQ/bpp@v3 66 | if: env.CHANNEL == 'production' 67 | with: 68 | keys: ${{ secrets.SUBMIT_KEYS_PRODUCTION }} 69 | verbose: true 70 | chrome-file: build/chrome-mv3-prod.zip 71 | firefox-file: build/firefox-mv3-prod.zip 72 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | on: 3 | push: 4 | schedule: 5 | - cron: "0 0 * * MON-FRI" 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: pnpm/action-setup@v4 13 | with: 14 | version: latest 15 | - name: Use Node.js 20.x 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 20.x 19 | cache: "pnpm" 20 | - name: Install dependencies 21 | run: | 22 | pnpm install 23 | npx puppeteer browsers install 24 | cd test && pnpm install 25 | - name: Build the extension 26 | run: pnpm build 27 | - name: Run tests 28 | run: cd test && pnpm test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules/ 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | #cache 13 | .turbo 14 | 15 | # dev tools 16 | .bin 17 | .cache 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | .pnpm-debug.log* 28 | 29 | # local env files 30 | .env* 31 | 32 | out/ 33 | build/ 34 | dist/ 35 | 36 | # plasmo - https://www.plasmo.com 37 | .plasmo 38 | 39 | # bpp - http://bpp.browser.market/ 40 | key*.json 41 | 42 | # typescript 43 | .tsbuildinfo 44 | -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full-vnc 2 | 3 | RUN sudo apt-get update \ 4 | && wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \ 5 | && sudo apt-get install -y ./google-chrome-stable_current_amd64.deb \ 6 | && rm google-chrome-stable_current_amd64.deb \ 7 | && sudo rm -rf /var/lib/apt/lists/* -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | 4 | tasks: 5 | - name: install node version 6 | init: | 7 | sudo apt update 8 | sudo apt install -y nodejs 9 | nvm install 20 10 | nvm use 20 11 | nvm uninstall 16 12 | npm install -g pnpm 13 | gp sync-done node 14 | 15 | # Without nodeJS 20 installed, the "sharp" package won't bundle properly, 16 | # so wait until the node update is finished in the prebuild. 17 | - name: build package 18 | init: | 19 | gp sync-await node 20 | pnpm install 21 | pnpm build 22 | pnpm build-dev-tools 23 | pnpm test 24 | command: | 25 | pnpm build 26 | 27 | ports: 28 | - name: sync 29 | port: 8080 30 | onOpen: ignore 31 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | 3 | **/*.yaml 4 | **/*.yml 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "endOfLine": "auto", 5 | "trailingComma": "all", 6 | "plugins": ["@ianvs/prettier-plugin-sort-imports"], 7 | "importOrder": [ 8 | "", 9 | "", 10 | "", 11 | "^@plasmo/(.*)$", 12 | "", 13 | "^@plasmohq/(.*)$", 14 | "", 15 | "^~(.*)$", 16 | "", 17 | "^[./]" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.puppeteerrc.cjs: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | 3 | /** 4 | * @type {import("puppeteer").Configuration} 5 | */ 6 | module.exports = { 7 | cacheDirectory: join(__dirname, ".cache", "puppeteer"), 8 | browserRevision: "130.0.6723.58", 9 | defaultLaunchOptions: { 10 | headless: "new", 11 | args: [ 12 | "--no-sandbox", 13 | "--disable-setuid-sandbox", 14 | "--disable-dev-shm-usage", 15 | "--disable-gpu" 16 | ] 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 TypeFox GmbH 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 | # Gitpod Browser extension 2 | 3 | [![Setup Automated](https://img.shields.io/badge/setup-automated-blue?logo=gitpod)](https://gitpod.io/#https://github.com/gitpod-io/browser-extension) 4 | 5 | This is the browser extension for Gitpod. It supports Chrome (see [Chrome Web Store](https://chrome.google.com/webstore/detail/dodmmooeoklaejobgleioelladacbeki/)), Firefox (see [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/gitpod/)) and Edge (see [how to install Chrome extensions](https://support.microsoft.com/help/4538971/microsoft-edge-add-or-remove-extensions)), and adds a **Gitpod** button to the configured GitLab, GitHub and Bitbucket installations (defaults to `gitlab.com`, `github.com` and `bitbucket.org`) which immediately creates a Gitpod workspace for the current git context: 6 | 7 | ![Gitpodify](./docs/github-injected.png "Gitpodify") 8 | 9 | ### Issues 10 | 11 | We are currently tracking all issues related to the browser extension in the [`gitpod-io/gitpod`](https://github.com/gitpod-io/gitpod) repository. 12 | You can use the [`component: browser-extension`](https://github.com/gitpod-io/gitpod/issues?q=is%3Aissue+is%3Aopen+extension+label%3A%22component%3A+browser-extension%22) label to search for relevant issues including feature proposals and bug reports. 13 | 14 | ### Development 15 | 16 | To make changes and test them using Gitpod itself: 17 | 18 | - add test cases to the [unit test](https://github.com/gitpod-io/browser-extension/blob/se/plasmo/test/src/button-contributions.spec.ts#L39) 19 | - try out changes like this: 20 | 1. run `pnpm build` 21 | 1. run `pnpm watch-prod` and download the built binary for your system (local machine) 22 | 1. run the binary anywhere on your local machine to sync the extension folder locally. 23 | 1. open Chrome and go to `chrome://extensions/` 24 | 1. enable `Developer mode` (top right) 25 | 1. click `Load unpacked` (top left) and select the folder you just downloaded 26 | 1. now you can test your changes 27 | 1. repeat step 1 and 2 and [reload the extension](chrome://extensions/) whenever you want to test new changes 28 | 29 | Or, when developing locally, you can execute the following to enable hot reloading with the extension in Chrome: 30 | 31 | ``` 32 | pnpm dev 33 | ``` 34 | 35 | Then, `Load unpacked` the `build/chrome-mv3-dev` folder in Chrome and after making changes, pages will reload automatically and the changes will be reflected immediately. 36 | 37 | #### Build 38 | 39 | The build happens automatically when you start a workspace but if you want to build explicitly, use these commands: 40 | 41 | ``` 42 | pnpm install 43 | pnpm build --target=chrome-mv3 # or --target=firefox-mv3 44 | pnpm package --target=chrome-mv3 # or --target=firefox-mv3 45 | ``` 46 | 47 | ### Testing 48 | 49 | You can test the extension without publishing to the store. Before uploading the bundle to the browser, make sure to [build](#build) the code, then follow these steps: 50 | 51 | #### For Chrome 52 | 53 | 1. Open Chrome 54 | 2. Click Settings -> Extensions -> Load unpacked 55 | 3. Select the `chrome-mv3-prod` folder inside of `build/` 56 | 57 | #### For Firefox 58 | 59 | 1. Open Firefox 60 | 1. Go to `about:debugging#/runtime/this-firefox` 61 | 1. Click Load Temporary Add-on -> Select the `firefox-mv3-prod.zip` file. Please note, that some features (like extension settings) will not work. 62 | 63 | ## Release 64 | 65 | We currently publish the extension for **Chrome** and **Firefox**. 66 | 67 | To release a new version, follow these steps: 68 | 69 | 1. Bump up the version value inside `package.json` (`yarn version --patch` or `yarn version --minor`) 70 | 1. Push your changes to `main` 71 | 1. Compose a list of changes using the list of commits that were pushed since last version 72 | 1. [Create a new release](https://github.com/gitpod-io/browser-extension/releases/new), listing changes: 73 | 74 | ```yaml 75 | ### Changes 76 | 77 | - Change/Fix A 78 | - Change/Fix B 79 | - Change/Fix C 80 | 81 | ### Credits 82 | 83 | Thanks to @{EXTERNAL_CONTRIBUTOR_USERNAME} for helping! 🍊 84 | ``` 85 | 86 | For Firefox: 87 | 88 | 1. Sign in to https://addons.mozilla.org/en-US/developers/addon/gitpod/edit with the credentials from 1Password 89 | 2. Click Upload new version 90 | 3. Upload the zip file (`firefox-mv3-prod.zip`) 91 | 4. Go through the steps and submit 92 | 93 | For Chrome: 94 | 95 | 1. Using your Google account, open the [`gitpod-browser-extension Google Group`](https://groups.google.com/g/gitpod-browser-extension) 96 | 2. If you don't have access, reach out for [help in Slack](https://gitpod.slack.com/archives/C04QC1ZMPV4) 97 | 3. Once you are in the Google Group, make sure to "Switch to Gitpod" in the top navbar 98 | 4. Click "Upload new package" 99 | 5. Upload the zip file (`chrome-mv3-prod.zip`) and submit 100 | 6. Wait a few hours for the review to happen! 101 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. 4 | 5 | To report a security issue please visit https://gitpod.io/security 6 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitpod-io/browser-extension/4d824dac9913e7eb2897fc64f4e89e96681963d5/assets/icon.png -------------------------------------------------------------------------------- /dev/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ROOT=/workspace/browser-extension 4 | cd $ROOT/dev/http-file-sync 5 | 6 | echo "building binaries ..." 7 | go build -o $ROOT/.bin/watch-sync 8 | GOARCH=amd64 GOOS=darwin go build -o $ROOT/.bin/watch-sync-osx-x86_64 9 | GOARCH=arm64 GOOS=darwin go build -o $ROOT/.bin/watch-sync-osx-arm64 -------------------------------------------------------------------------------- /dev/http-file-sync/go.mod: -------------------------------------------------------------------------------- 1 | module gitpod.io/http-file-sync 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.6.0 7 | github.com/spf13/cobra v1.7.0 8 | ) 9 | 10 | require ( 11 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 12 | github.com/spf13/pflag v1.0.5 // indirect 13 | golang.org/x/sys v0.12.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /dev/http-file-sync/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 3 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 4 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 5 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 6 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 7 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 8 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 9 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 10 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 11 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 12 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 13 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | -------------------------------------------------------------------------------- /dev/http-file-sync/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "gitpod.io/http-file-sync/pkg" 8 | ) 9 | 10 | const ( 11 | serverPort = "8080" 12 | remoteURL = "http://localhost:" + serverPort 13 | localDir = "./gitpod-extension-dev" 14 | manifestFilename = "file-manifest.json" 15 | ) 16 | 17 | func main() { 18 | // Root command (default/client-side logic) 19 | var rootCmd = &cobra.Command{Use: "watch-sync", Short: "Run a client that watches remote changes and syncs them locally"} 20 | rootCmd.Run = func(cmd *cobra.Command, args []string) { 21 | if len(args) > 1 { 22 | fmt.Println("Usage: watch-sync ?") 23 | } 24 | commandOnUpdate := "" 25 | if len(args) == 1 { 26 | commandOnUpdate = args[0] 27 | } 28 | pkg.WatchSync(remoteURL, localDir, manifestFilename, commandOnUpdate) 29 | } 30 | 31 | var servCmd = &cobra.Command{ 32 | Use: "serve ", 33 | Short: "Run the server logic", 34 | Run: func(cmd *cobra.Command, args []string) { 35 | if len(args) != 1 { 36 | fmt.Println("Please provide a folder to watch") 37 | fmt.Println("Usage: watch-sync serve ") 38 | return 39 | } 40 | pkg.Serve(args[0], serverPort, manifestFilename) 41 | }, 42 | } 43 | 44 | // Add "serv" command as a subcommand of the root 45 | rootCmd.AddCommand(servCmd) 46 | 47 | // Execute the root command 48 | if err := rootCmd.Execute(); err != nil { 49 | fmt.Println(err) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /dev/http-file-sync/pkg/manifest.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | type FileManifest struct { 4 | Files []FileEntry `json:"files"` 5 | } 6 | 7 | type FileEntry struct { 8 | Name string `json:"name"` 9 | LastModified string `json:"lastModified"` 10 | } 11 | -------------------------------------------------------------------------------- /dev/http-file-sync/pkg/server-sync.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | 11 | "github.com/fsnotify/fsnotify" 12 | ) 13 | 14 | func generateManifest(dir string, manifestFile string) { 15 | 16 | fmt.Println("Generating manifest...") 17 | 18 | manifest := readManifestRec([]FileEntry{}, manifestFile, dir, "") 19 | 20 | manifestJSON, err := json.MarshalIndent(map[string][]FileEntry{"files": manifest}, "", " ") 21 | if err != nil { 22 | fmt.Println("Error marshaling JSON:", err) 23 | return 24 | } 25 | 26 | err = os.WriteFile(filepath.Join(dir, manifestFile), manifestJSON, 0644) 27 | if err != nil { 28 | fmt.Println("Error writing manifest file:", err) 29 | return 30 | } 31 | } 32 | 33 | func readManifestRec(manifest []FileEntry, manifestFile string, baseDir string, relativePath string) []FileEntry { 34 | dir := filepath.Join(baseDir, relativePath) 35 | files, err := os.ReadDir(dir) 36 | if err != nil { 37 | fmt.Println("Error reading directory:", err) 38 | return manifest 39 | } 40 | 41 | for _, file := range files { 42 | if file.Name() == manifestFile { 43 | continue 44 | } 45 | 46 | info, err := file.Info() 47 | if err != nil { 48 | fmt.Println("Error getting file info:", err) 49 | continue 50 | } 51 | lastModified := info.ModTime().UTC().Format(time.RFC3339) 52 | if info.IsDir() { 53 | manifest = readManifestRec(manifest, manifestFile, baseDir, filepath.Join(relativePath, file.Name())) 54 | } else { 55 | manifest = append(manifest, FileEntry{Name: filepath.Join(relativePath, file.Name()), LastModified: lastModified}) 56 | } 57 | } 58 | return manifest 59 | } 60 | 61 | func Serve(watchDir string, port string, manifestFile string) { 62 | 63 | // Create directory if it doesn't exist 64 | _ = os.MkdirAll(watchDir, 0755) 65 | 66 | // Initialize a new file watcher 67 | watcher, err := fsnotify.NewWatcher() 68 | if err != nil { 69 | fmt.Println("Error creating watcher:", err) 70 | return 71 | } 72 | defer watcher.Close() 73 | 74 | // Generate initial manifest 75 | generateManifest(watchDir, manifestFile) 76 | 77 | // Watch directory 78 | err = watcher.Add(watchDir) 79 | if err != nil { 80 | fmt.Println("Error adding directory to watcher:", err) 81 | return 82 | } 83 | 84 | // File server 85 | go func() { 86 | http.Handle("/", http.FileServer(http.Dir(watchDir))) 87 | http.ListenAndServe(":"+port, nil) 88 | }() 89 | 90 | fmt.Printf("Serving files at http://localhost:%s\n", port) 91 | fmt.Printf("Please download the binary for your client OS from `.bin/` and run it in a local directory. It will auto sync and update the extension on changes\n") 92 | 93 | // File watcher loop 94 | for { 95 | select { 96 | case event, ok := <-watcher.Events: 97 | if !ok { 98 | return 99 | } 100 | 101 | if event.Op&fsnotify.Write == fsnotify.Write || 102 | event.Op&fsnotify.Create == fsnotify.Create || 103 | event.Op&fsnotify.Remove == fsnotify.Remove { 104 | if filepath.Base(event.Name) != manifestFile { 105 | generateManifest(watchDir, manifestFile) 106 | } 107 | } 108 | case err, ok := <-watcher.Errors: 109 | if !ok { 110 | return 111 | } 112 | fmt.Println("Error:", err) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /dev/http-file-sync/pkg/watch-sync.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "time" 11 | ) 12 | 13 | func WatchSync(remoteUrl string, localDir string, manifestFilename string, commandOnUpdate string) { 14 | fmt.Printf("Watching %s/%s and sync changes to %s\n", remoteUrl, manifestFilename, localDir) 15 | // Create directory if it doesn't exist 16 | _ = os.MkdirAll(localDir, 0755) 17 | 18 | // Generate initial manifest 19 | update(remoteUrl, localDir, manifestFilename, commandOnUpdate) 20 | 21 | // Watch for changes 22 | for { 23 | // sleep for 2 seconds 24 | time.Sleep(2 * time.Second) 25 | update(remoteUrl, localDir, manifestFilename, commandOnUpdate) 26 | } 27 | } 28 | 29 | func update(remoteUrl string, localDir string, manifestFilename string, commandOnUpdate string) { 30 | localManifest := filepath.Join(localDir, "local_"+manifestFilename) 31 | 32 | // Download remote manifest 33 | resp, err := http.Get(fmt.Sprintf("%s/%s", remoteUrl, manifestFilename)) 34 | if err != nil { 35 | fmt.Println("Error downloading manifest:", err) 36 | return 37 | } 38 | defer resp.Body.Close() 39 | 40 | remoteManifestData, err := ioutil.ReadAll(resp.Body) 41 | if err != nil { 42 | fmt.Println("Error reading manifest:", err) 43 | return 44 | } 45 | 46 | var remoteManifest FileManifest 47 | json.Unmarshal(remoteManifestData, &remoteManifest) 48 | 49 | // Read local manifest if exists 50 | localManifestData, err := os.ReadFile(localManifest) 51 | var localManifestMap map[string]string 52 | if err == nil { 53 | var localManifest FileManifest 54 | json.Unmarshal(localManifestData, &localManifest) 55 | localManifestMap = make(map[string]string) 56 | for _, entry := range localManifest.Files { 57 | localManifestMap[entry.Name] = entry.LastModified 58 | } 59 | } 60 | 61 | // Compare and download files 62 | for _, remoteFile := range remoteManifest.Files { 63 | localLastModified, exists := localManifestMap[remoteFile.Name] 64 | if !exists || localLastModified != remoteFile.LastModified { 65 | fmt.Printf("Downloading %s...\n", remoteFile.Name) 66 | 67 | resp, err := http.Get(fmt.Sprintf("%s/%s", remoteUrl, remoteFile.Name)) 68 | if err != nil { 69 | fmt.Println("Error downloading file:", err) 70 | continue 71 | } 72 | defer resp.Body.Close() 73 | 74 | fileData, err := ioutil.ReadAll(resp.Body) 75 | if err != nil { 76 | fmt.Println("Error reading file:", err) 77 | continue 78 | } 79 | 80 | err = os.MkdirAll(filepath.Join(localDir, filepath.Dir(remoteFile.Name)), 0755) 81 | if err != nil { 82 | fmt.Println("Error creating directory:", err) 83 | } 84 | err = os.WriteFile(filepath.Join(localDir, remoteFile.Name), fileData, 0644) 85 | if err != nil { 86 | fmt.Println("Error writing file:", err) 87 | } 88 | } 89 | } 90 | 91 | // Save new local manifest 92 | err = os.WriteFile(localManifest, remoteManifestData, 0644) 93 | if err != nil { 94 | fmt.Println("Error writing local manifest:", err) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /docs/github-injected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitpod-io/browser-extension/4d824dac9913e7eb2897fc64f4e89e96681963d5/docs/github-injected.png -------------------------------------------------------------------------------- /docs/safari-confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitpod-io/browser-extension/4d824dac9913e7eb2897fc64f4e89e96681963d5/docs/safari-confirm.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitpod", 3 | "displayName": "Gitpod", 4 | "version": "2.4.0", 5 | "description": "The developer platform for on-demand cloud development environments. Create software faster and more securely.", 6 | "author": "Gitpod ", 7 | "homepage": "https://www.gitpod.io", 8 | "scripts": { 9 | "dev": "plasmo dev --source-maps --no-minify", 10 | "watch-dev": "pnpm build-dev-tools && .bin/watch-sync serve ./build/chrome-mv3-dev", 11 | "watch-prod": "pnpm build-dev-tools && .bin/watch-sync serve ./build/chrome-mv3-prod", 12 | "build": "plasmo build --source-maps --no-minify", 13 | "test": "cd test && pnpm test", 14 | "package": "plasmo package", 15 | "package:all": "pnpm build --target=chrome-mv3 && pnpm build --target=firefox-mv3 && pnpm package --target=chrome-mv3 && pnpm package --target=firefox-mv3", 16 | "format": "prettier --experimental-ternaries --write .", 17 | "build-dev-tools": "sh dev/build.sh" 18 | }, 19 | "dependencies": { 20 | "@plasmohq/storage": "^1.10.0", 21 | "@tailwindcss/forms": "^0.5.10", 22 | "classnames": "^2.5.1", 23 | "lucide-react": "^0.474.0", 24 | "plasmo": "^0.89.2", 25 | "react": "^18.3.1", 26 | "react-dom": "^18.3.1", 27 | "react-hotkeys-hook": "^4.6.1", 28 | "validator": "^13.12.0", 29 | "webext-dynamic-content-scripts": "^10.0.2", 30 | "webext-permission-toggle": "^5.0.2", 31 | "webextension-polyfill": "^0.12.0" 32 | }, 33 | "devDependencies": { 34 | "@ianvs/prettier-plugin-sort-imports": "4.3.1", 35 | "@types/chai": "^5.0.1", 36 | "@types/chrome": "0.0.287", 37 | "@types/mocha": "^10.0.7", 38 | "@types/node": "20.14.5", 39 | "@types/puppeteer": "^7.0.4", 40 | "@types/react": "18.3.3", 41 | "@types/react-dom": "18.3.1", 42 | "@types/validator": "^13.12.0", 43 | "@types/webextension-polyfill": "^0.12.1", 44 | "@typescript-eslint/eslint-plugin": "^6.21.0", 45 | "@typescript-eslint/parser": "^6.21.0", 46 | "autoprefixer": "^10.4.15", 47 | "chai": "^5.1.2", 48 | "eslint": "^8.49.0", 49 | "eslint-plugin-react": "^7.33.2", 50 | "htmlnano": "2.0.3", 51 | "mocha": "^11.1.0", 52 | "postcss": "^8.5.2", 53 | "prettier": "3.3.3", 54 | "puppeteer": "^23.6.0", 55 | "puppeteer-core": "^24.1.1", 56 | "source-map-support": "^0.5.21", 57 | "tailwindcss": "^3.4.15", 58 | "ts-node": "^10.9.2", 59 | "typescript": "5.5.4" 60 | }, 61 | "manifest": { 62 | "optional_host_permissions": [ 63 | "*://*/*" 64 | ], 65 | "permissions": [ 66 | "scripting", 67 | "contextMenus", 68 | "activeTab" 69 | ], 70 | "browser_specific_settings": { 71 | "gecko": { 72 | "id": "{dbcc42f9-c979-4f53-8a95-a102fbff3bbe}" 73 | } 74 | } 75 | }, 76 | "pnpm": { 77 | "supportedArchitectures": { 78 | "libc": [ 79 | "musl" 80 | ] 81 | }, 82 | "overrides": { 83 | "sharp": "0.33.4" 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('postcss').ProcessOptions} 3 | */ 4 | module.exports = { 5 | plugins: { 6 | tailwindcss: {}, 7 | autoprefixer: {}, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import browser from "webextension-polyfill"; 2 | 3 | import "webext-dynamic-content-scripts"; 4 | 5 | import addDomainPermissionToggle from "webext-permission-toggle"; 6 | 7 | (async () => { 8 | addDomainPermissionToggle(); 9 | })(); 10 | 11 | browser.runtime.onInstalled.addListener((details) => { 12 | if (details.reason === "install") { 13 | browser.tabs.create({ url: "https://www.gitpod.io/extension-activation?track=true" }); 14 | } 15 | }); 16 | browser.runtime.setUninstallURL("https://www.gitpod.io/extension-uninstall?track=true"); 17 | 18 | export {}; 19 | -------------------------------------------------------------------------------- /src/button/CaretForProvider.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | 3 | import type { SupportedApplication } from "./button-contributions"; 4 | 5 | const BitbucketCaret = () => { 6 | return ( 7 | 8 | 13 | 14 | ); 15 | }; 16 | 17 | const GitHubCaret = () => { 18 | return ( 19 | 30 | ); 31 | }; 32 | 33 | const GitLabCaret = () => { 34 | return ( 35 | 42 | 48 | 49 | ); 50 | }; 51 | 52 | const AzureDevOpsCaret = () => { 53 | return ( 54 | 55 | 59 | 60 | ); 61 | }; 62 | 63 | type Props = { 64 | provider: SupportedApplication; 65 | }; 66 | export const CaretForProvider = ({ provider }: Props) => { 67 | switch (provider) { 68 | case "github": 69 | return ; 70 | case "bitbucket": 71 | case "bitbucket-server": 72 | return ; 73 | case "gitlab": 74 | return ; 75 | case "azure-devops": 76 | return ; 77 | default: 78 | return ( 79 | 80 | 81 | 82 | ); 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /src/button/button-contributions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file needs to be clear from imports, because it is copied into the test project and used by mocha. 3 | * Happy about anyone who's able to make this work with imports (i.e. run the tests in this project), but I couldn't figure it out and gave up. 4 | */ 5 | 6 | export type SupportedApplication = "github" | "gitlab" | "bitbucket-server" | "bitbucket" | "azure-devops"; 7 | 8 | const resolveMetaAppName = (head: HTMLHeadElement): string | undefined => { 9 | const metaApplication = head.querySelector("meta[name=application-name]"); 10 | const ogApplication = head.querySelector("meta[property='og:site_name']"); 11 | 12 | if (metaApplication) { 13 | return metaApplication.getAttribute("content") || undefined; 14 | } else if (ogApplication) { 15 | return ogApplication.getAttribute("content") || undefined; 16 | } 17 | 18 | return undefined; 19 | }; 20 | 21 | export const DEFAULT_HOSTS = ["github.com", "gitlab.com", "bitbucket.org", "dev.azure.com"]; 22 | 23 | /** 24 | * Provides a fast check to see if the current URL is on a supported site. 25 | */ 26 | export const isSiteSuitable = (): boolean => { 27 | const isWhitelistedHost = DEFAULT_HOSTS.some((host) => location.host === host); 28 | if (isWhitelistedHost) { 29 | return true; 30 | } 31 | 32 | const appName = resolveMetaAppName(document.head); 33 | if (!appName) { 34 | return false; 35 | } 36 | const allowedApps = ["GitHub", "GitLab", "Bitbucket"]; 37 | 38 | return allowedApps.some((allowedApp) => appName.includes(allowedApp)); 39 | }; 40 | 41 | export interface ButtonContributionParams { 42 | /** 43 | * A unique id for the button contribution. Used to identify the button in the UI. 44 | */ 45 | id: string; 46 | 47 | /** 48 | * 49 | */ 50 | exampleUrls: string[]; 51 | 52 | /** 53 | * A CSS selector that matches the parent element in which the button should be inserted. 54 | * 55 | * Use the developer tools -> right click on the element -> "copy JS path" to get the selector. 56 | */ 57 | selector: string; 58 | 59 | /** 60 | * The element in which the button should be inserted. 61 | * 62 | * This element will be inserted into the main document and allows for styling within the original page. 63 | * 64 | * The structure looks like this: 65 | * 66 | * 67 | * .... 68 | * 69 | * 70 | * #shadow-root 71 | * 72 | * 73 | * 74 | */ 75 | containerElement: { 76 | type: "div" | "li"; 77 | props: { 78 | [key: string]: string; 79 | }; 80 | }; 81 | 82 | /** 83 | * Either a regular expression that is used to match the current URL or a function expected to return a boolean. This is making the selection faster and also can help to disambiguate. 84 | */ 85 | match?: RegExp | (() => boolean); 86 | 87 | /** 88 | * The application that is supported by this button contribution. 89 | */ 90 | application: SupportedApplication; 91 | 92 | /** 93 | * Additional class names that should be added to the elements. 94 | */ 95 | additionalClassNames?: ("secondary" | "medium" | "left-align-menu" | "tall")[]; 96 | 97 | /** 98 | * A selector that is used to insert the button before a specific element. 99 | */ 100 | insertBefore?: string; 101 | 102 | /** 103 | * A list of manipulations that should be applied to the document. 104 | * 105 | * Each manipulation contains a CSS selector (element) that is used to find the element to manipulate and optionally 106 | * the classnames to remove and add. 107 | */ 108 | manipulations?: { element: string; remove?: string; add?: string; style?: Partial }[]; 109 | 110 | /** 111 | * A function that can be used to transform the URL that should be opened when the Gitpod button is clicked. 112 | * @returns The transformed URL. 113 | */ 114 | urlTransformer?: (originalURL: string) => string; 115 | } 116 | 117 | function createElement( 118 | type: "div" | "li", 119 | props: { 120 | [key: string]: string; 121 | }, 122 | ) { 123 | return { 124 | type, 125 | props, 126 | }; 127 | } 128 | 129 | export const buttonContributions: ButtonContributionParams[] = [ 130 | // Azure DevOps 131 | { 132 | id: "ado-repo", 133 | exampleUrls: [ 134 | // "https://dev.azure.com/services-azure/_git/project2" 135 | ], 136 | selector: "div.repos-files-header-commandbar:nth-child(1)", 137 | containerElement: createElement("div", {}), 138 | application: "azure-devops", 139 | insertBefore: `div.bolt-header-command-item-button:has(button[id^="__bolt-header-command-bar-menu-button"])`, 140 | manipulations: [ 141 | { 142 | element: "div.repos-files-header-commandbar.scroll-hidden", 143 | remove: "scroll-hidden", 144 | }, 145 | ], 146 | urlTransformer(originalUrl) { 147 | const url = new URL(originalUrl); 148 | if (url.pathname.includes("version=GB")) { 149 | return originalUrl; 150 | } 151 | // version=GBdevelop 152 | const branchElement = document.evaluate( 153 | "//div[contains(@class, 'version-dropdown')]//span[contains(@class, 'text-ellipsis')]", 154 | document, 155 | null, 156 | XPathResult.FIRST_ORDERED_NODE_TYPE, 157 | null, 158 | ).singleNodeValue; 159 | if (branchElement) { 160 | const branch = branchElement.textContent?.trim(); 161 | url.searchParams.set("version", `GB${branch}`); 162 | } 163 | 164 | return url.toString(); 165 | }, 166 | }, 167 | { 168 | id: "ado-pr", 169 | exampleUrls: [ 170 | // "https://dev.azure.com/services-azure/test-project/_git/repo2/pullrequest/1" 171 | ], 172 | selector: ".repos-pr-header > div:nth-child(2) > div:nth-child(1)", 173 | containerElement: createElement("div", {}), 174 | application: "azure-devops", 175 | insertBefore: `div.bolt-header-command-item-button:has(button[id^="__bolt-menu-button-"])`, 176 | }, 177 | { 178 | id: "ado-repo-empty", 179 | exampleUrls: [], 180 | selector: "div.clone-with-application", 181 | application: "azure-devops", 182 | containerElement: createElement("div", { marginLeft: "4px", marginRight: "4px" }), 183 | }, 184 | 185 | // GitLab 186 | { 187 | id: "gl-repo", // also taking care of branches 188 | exampleUrls: [ 189 | "https://gitlab.com/svenefftinge/browser-extension-test", 190 | "https://gitlab.com/svenefftinge/browser-extension-test/-/tree/my-branch", 191 | ], 192 | // must not match /blob/ because that is a file 193 | match: /^(?!.*\/blob\/).*$/, 194 | selector: "#tree-holder .tree-controls", 195 | containerElement: { type: "div", props: { marginLeft: "8px" } }, 196 | application: "gitlab", 197 | manipulations: [ 198 | { 199 | // make the clone button secondary 200 | element: "#clone-dropdown", 201 | remove: "btn-confirm", 202 | }, 203 | ], 204 | }, 205 | { 206 | id: "gl-repo-empty", 207 | exampleUrls: ["https://gitlab.com/filiptronicek/empty"], 208 | selector: `xpath://*[@id="js-project-show-empty-page"]/div[1]/div[1]/div[2]`, 209 | containerElement: { type: "div", props: { marginLeft: "8px" } }, 210 | application: "gitlab", 211 | }, 212 | { 213 | id: "gl-file", 214 | exampleUrls: [ 215 | //TODO fix me "https://gitlab.com/svenefftinge/browser-extension-test/-/blob/my-branch/README.md", 216 | ], 217 | match: /\/blob\//, 218 | selector: 219 | "#fileHolder > div.js-file-title.file-title-flex-parent > div.gl-display-flex.gl-flex-wrap.file-actions", 220 | containerElement: createElement("div", { display: "inline-flex", marginLeft: "8px" }), 221 | application: "gitlab", 222 | manipulations: [ 223 | { 224 | // make the clone button secondary 225 | element: 226 | "#fileHolder > div.js-file-title.file-title-flex-parent > div.gl-display-flex.gl-flex-wrap.file-actions > div.gl-sm-ml-3.gl-mr-3 > div > button", 227 | remove: "btn-confirm", 228 | }, 229 | ], 230 | }, 231 | { 232 | id: "gl-merge-request", 233 | exampleUrls: ["https://gitlab.com/svenefftinge/browser-extension-test/-/merge_requests/1"], 234 | match: /\/merge_requests\//, 235 | selector: "body[data-project-id] div.detail-page-header-actions.is-merge-request > div", 236 | containerElement: createElement("div", { marginLeft: "8px", marginRight: "-8px" }), 237 | application: "gitlab", 238 | insertBefore: "body[data-project-id] div.detail-page-header-actions.is-merge-request > div > div", 239 | manipulations: [ 240 | { 241 | // make the clone button secondary 242 | element: 243 | "#content-body > div.merge-request > div.detail-page-header.border-bottom-0.gl-display-block.gl-pt-5.gl-sm-display-flex\\!.is-merge-request > div.detail-page-header-actions.gl-align-self-start.is-merge-request.js-issuable-actions.gl-display-flex > div > div.gl-sm-ml-3.dropdown.gl-dropdown > button", 244 | remove: "btn-confirm", 245 | }, 246 | ], 247 | }, 248 | { 249 | id: "gl-issue", 250 | exampleUrls: ["https://gitlab.com/svenefftinge/browser-extension-test/-/issues/1"], 251 | match: /\/issues\//, 252 | selector: 253 | "#content-body > div.issue-details.issuable-details.js-issue-details > div.detail-page-description.content-block.js-detail-page-description.gl-pt-3.gl-pb-0.gl-border-none > div:nth-child(1) > div > div.gl-flex.gl-items-start.gl-flex-col.md\\:gl-flex-row.gl-gap-3.gl-pt-3 > div", 254 | containerElement: createElement("div", { marginLeft: "0", marginRight: "0px" }), 255 | application: "gitlab", 256 | insertBefore: "#new-actions-header-dropdown", 257 | manipulations: [ 258 | { 259 | element: 260 | "#content-body > div.issue-details.issuable-details.js-issue-details > div.detail-page-description.content-block.js-detail-page-description.gl-pt-3.gl-pb-0.gl-border-none > div.js-issue-widgets > div > div > div.new-branch-col.gl-font-size-0.gl-my-2 > div > div.btn-group.available > button.gl-button.btn.btn-md.btn-confirm.js-create-merge-request", 261 | remove: "btn-confirm", 262 | }, 263 | { 264 | element: 265 | "#content-body > div.issue-details.issuable-details.js-issue-details > div.detail-page-description.content-block.js-detail-page-description.gl-pt-3.gl-pb-0.gl-border-none > div.js-issue-widgets > div > div > div.new-branch-col.gl-font-size-0.gl-my-2 > div > div.btn-group.available > button.gl-button.btn.btn-icon.btn-md.btn-confirm.js-dropdown-toggle.dropdown-toggle.create-merge-request-dropdown-toggle", 266 | remove: "btn-confirm", 267 | }, 268 | ], 269 | }, 270 | 271 | // GitHub 272 | { 273 | id: "gh-repo", 274 | exampleUrls: [ 275 | // disabled testing, because the new layout doesn't show as an anonymous user 276 | "https://github.com/svenefftinge/browser-extension-test", 277 | "https://github.com/svenefftinge/browser-extension-test/tree/my-branch", 278 | ], 279 | selector: `xpath://*[contains(@id, 'repo-content-')]/div/div/div/div[1]/react-partial/div/div/div[2]/div[2]`, 280 | containerElement: createElement("div", {}), 281 | additionalClassNames: ["medium"], 282 | application: "github", 283 | match: () => { 284 | const regex = /^https?:\/\/([^/]+)\/([^/]+)\/([^/]+)(\/(tree\/.*)?)?$/; 285 | return document.querySelector("div.file-navigation") === null && regex.test(window.location.href); 286 | }, 287 | }, 288 | { 289 | id: "gh-commit", 290 | exampleUrls: [ 291 | "https://github.com/svenefftinge/browser-extension-test/commit/82d701a9ac26ea25da9b24c5b3722b7a89e43b16", 292 | ], 293 | selector: "#repo-content-pjax-container > div > div.commit.full-commit.mt-0.px-2.pt-2", 294 | insertBefore: "#browse-at-time-link", 295 | containerElement: createElement("div", { 296 | float: "right", 297 | marginLeft: "8px", 298 | }), 299 | application: "github", 300 | additionalClassNames: ["medium"], 301 | }, 302 | 303 | { 304 | id: "gh-issue", 305 | exampleUrls: ["https://github.com/svenefftinge/browser-extension-test/issues/1"], 306 | selector: "#partial-discussion-header > div.gh-header-show > div > div", 307 | containerElement: createElement("div", { 308 | order: "2", 309 | }), 310 | match: /\/issues\//, 311 | application: "github", 312 | manipulations: [ 313 | { 314 | // make the code button secondary 315 | element: "#partial-discussion-header > div.gh-header-show > div > div > a", 316 | remove: "Button--primary", 317 | add: "Button--secondary", 318 | }, 319 | ], 320 | }, 321 | { 322 | id: "gh-issue-new", // this isn't referring to "new issue", but to new "issue" 323 | exampleUrls: ["https://github.com/svenefftinge/browser-extension-test/issues/1"], 324 | selector: `xpath://*[@id="js-repo-pjax-container"]/react-app/div/div/div/div/div[1]/div/div/div[3]/div`, 325 | containerElement: createElement("div", {}), 326 | insertBefore: `xpath://*[@id="js-repo-pjax-container"]/react-app/div/div/div/div/div[1]/div/div/div[3]/div/div`, 327 | application: "github", 328 | // we need to make the button higher: the buttons here use 2rem instead of 1.75rem 329 | additionalClassNames: ["tall"], 330 | }, 331 | { 332 | id: "gh-pull", 333 | exampleUrls: ["https://github.com/svenefftinge/browser-extension-test/pull/2"], 334 | selector: "#partial-discussion-header > div.gh-header-show > div > div", 335 | containerElement: createElement("div", { 336 | order: "2", 337 | }), 338 | match: /\/pull\//, 339 | application: "github", 340 | }, 341 | { 342 | id: "gh-file", 343 | exampleUrls: ["https://github.com/svenefftinge/browser-extension-test/blob/my-branch/README.md"], 344 | selector: "#StickyHeader > div > div > div.Box-sc-g0xbh4-0.gtBUEp", 345 | containerElement: createElement("div", { 346 | marginLeft: "8px", 347 | }), 348 | application: "github", 349 | additionalClassNames: ["medium"], 350 | }, 351 | { 352 | id: "gh-empty-repo", 353 | exampleUrls: [ 354 | //TODO fixme "https://github.com/svenefftinge/empty-repo", 355 | ], 356 | selector: 357 | "#repo-content-pjax-container > div > div.d-md-flex.flex-items-stretch.gutter-md.mb-4 > div.col-md-6.mb-4.mb-md-0 > div,#repo-content-turbo-frame > div > div.d-md-flex.flex-items-stretch.gutter-md.mb-4 > div.col-md-6.mb-4.mb-md-0 > div", 358 | containerElement: createElement("div", {}), 359 | application: "github", 360 | manipulations: [ 361 | { 362 | element: 363 | "#repo-content-pjax-container > div > div.d-md-flex.flex-items-stretch.gutter-md.mb-4 > div.col-md-6.mb-4.mb-md-0 > div > a, #repo-content-turbo-frame > div > div.d-md-flex.flex-items-stretch.gutter-md.mb-4 > div.col-md-6.mb-4.mb-md-0 > div > a", 364 | style: { 365 | display: "none", 366 | }, 367 | }, 368 | { 369 | element: 370 | "#repo-content-pjax-container > div > div.d-md-flex.flex-items-stretch.gutter-md.mb-4 > div.col-md-6.mb-4.mb-md-0 > div > h3, #repo-content-turbo-frame > div > div.d-md-flex.flex-items-stretch.gutter-md.mb-4 > div.col-md-6.mb-4.mb-md-0 > div > h3", 371 | style: { 372 | display: "none", 373 | }, 374 | }, 375 | ], 376 | }, 377 | // Bitbucket Server 378 | { 379 | id: "bbs-repo", 380 | match: /\/(browse|commits)/, 381 | exampleUrls: [ 382 | "https://bitbucket.gitpod-dev.com/users/svenefftinge/repos/browser-extension-test/browse", 383 | "https://bitbucket.gitpod-dev.com/users/svenefftinge/repos/browser-extension-test/browse?at=refs%2Fheads%2Fmy-branch", 384 | ], 385 | selector: 386 | "#main > div.aui-toolbar2.branch-selector-toolbar > div > div.aui-toolbar2-primary > div > div:nth-child(1) > div", 387 | insertBefore: "#branch-actions", 388 | containerElement: createElement("div", { 389 | marginLeft: "2px", 390 | }), 391 | application: "bitbucket-server", 392 | additionalClassNames: ["secondary"], 393 | }, 394 | { 395 | id: "bbs-pull-request", 396 | exampleUrls: [ 397 | // disabled because it doesn't work anonymously 398 | // "https://bitbucket.gitpod-dev.com/users/svenefftinge/repos/browser-extension-test/pull-requests/1/overview", 399 | ], 400 | selector: "#pull-requests-container > header > div.pull-request-header-bar > div.pull-request-actions", 401 | insertBefore: 402 | "#pull-requests-container > header > div.pull-request-header-bar > div.pull-request-actions > div.pull-request-more-actions", 403 | containerElement: createElement("div", { 404 | marginLeft: "2px", 405 | }), 406 | application: "bitbucket-server", 407 | }, 408 | 409 | // bitbucket.org 410 | // we use xpath expressions, because the CSS selectors are not stable enough 411 | // tests are disabled because the URLs are not reachable without a session 412 | { 413 | id: "bb-repo", 414 | exampleUrls: [ 415 | // "https://bitbucket.org/svenefftinge/browser-extension-test/src/master/" 416 | ], 417 | selector: 'xpath://*[@id="main"]/div/div/div[1]/div/header/div/div/div/div[2]/div', 418 | insertBefore: 419 | "#main > div > div > div.css-1m2ufqk.efo6slf1 > div > header > div > div > div > div.css-1ianfu6 > div > div:nth-child(2)", 420 | containerElement: createElement("div", { 421 | marginLeft: "2px", 422 | }), 423 | application: "bitbucket", 424 | }, 425 | { 426 | id: "bb-pull-request", 427 | exampleUrls: [ 428 | // "https://bitbucket.org/efftinge/browser-extension-test/pull-requests/1" 429 | ], 430 | selector: 'xpath://*[@id="main"]/div/div/div[1]/div/div/div/div[1]/div/div[2]/div/div[2]/div/div', // grandparent div of the "Request changes" and "Approve" buttons 431 | containerElement: createElement("div", {}), 432 | insertBefore: 433 | 'xpath:(//*[@id="main"]/div/div/div[1]/div/div/div/div[1]/div/div[2]/div/div[2]/div/div/div)[last()]', // note the [last()] to insert before the last child (the kebab menu) 434 | application: "bitbucket", 435 | }, 436 | { 437 | id: "bb-branch", 438 | match: /\/branch\/(.+)/, 439 | exampleUrls: [ 440 | // "https://bitbucket.org/efftinge/browser-extension-test/branch/my-branch" 441 | ], 442 | selector: 'xpath://*[@id="main"]/div/div/div[1]/div/div/div[2]/div/div', // action bar section with the last action of "Settings" 443 | containerElement: createElement("div", { 444 | marginLeft: "2px", 445 | }), 446 | application: "bitbucket", 447 | }, 448 | { 449 | id: "bb-commits", 450 | match: /\/commits\/(.+)?/, 451 | exampleUrls: ["https://bitbucket.org/efftinge/browser-extension-test/commits/"], 452 | selector: 'xpath://*[@id="main"]/div/div/div[1]/div/div/div[1]/div[1]/div[2]/div', 453 | containerElement: createElement("div", { 454 | marginLeft: "2px", 455 | }), 456 | application: "bitbucket", 457 | }, 458 | ]; 459 | -------------------------------------------------------------------------------- /src/button/button.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-family: -apple-system, "system-ui", "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, 3 | "Apple Color Emoji", "Segoe UI Emoji"; 4 | --line-height: 20px; 5 | --font-size: 12px; 6 | --font-weight: 500; 7 | --border-radius: 6px; 8 | --border-width: 1px; 9 | 10 | --primary-bg-color: #1f75cb; 11 | --primary-hover-bg-color: #064787; 12 | --primary-border-color: #1f75cb; 13 | --primary-hover-border-color: #064787; 14 | --primary-color: #fff; 15 | --primary-hover-color: #fff; 16 | --primary-height: 20px; 17 | --primary-separator-color: var(--primary-color); 18 | 19 | --secondary-bg-color: #2684ff; 20 | --secondary-hover-bg-color: #2684ff; 21 | --secondary-border-color: #1f75cb; 22 | --secondary-hover-border-color: #064787; 23 | --secondary-color: #fff; 24 | --secondary-hover-color: #fff; 25 | --secondary-separator-color: var(--secondary-border-color); 26 | 27 | --dropdown-bg-color: #fff; 28 | --dropdown-hover-bg-color: #f6f8fa; 29 | --dropdown-border-width: var(--border-width); 30 | --dropdown-border-radius: var(--border-radius); 31 | --dropdown-border-color: rgba(31, 35, 40, 0.15); 32 | --dropdown-box-shadow: 5px 5px 10px 0px rgba(140, 149, 159, 0.2); 33 | } 34 | 35 | #plasmo-shadow-container { 36 | z-index: 30 !important; 37 | } 38 | 39 | #plasmo-inline { 40 | max-width: fit-content; 41 | } 42 | 43 | .gitpod-button { 44 | position: "relative"; 45 | display: "inline-block"; 46 | } 47 | 48 | .button { 49 | display: flex; 50 | } 51 | 52 | .button-part { 53 | display: flex; 54 | align-items: center; 55 | border-width: var(--border-width); 56 | border-style: solid; 57 | box-sizing: border-box; 58 | background-color: var(--primary-bg-color); 59 | border-color: var(--primary-border-color); 60 | padding: 0 12px; 61 | color: var(--primary-color); 62 | text-decoration: none; 63 | height: var(--primary-height); 64 | min-height: var(--primary-height); 65 | font-family: var(--font-family); 66 | font-size: var(--font-size); 67 | line-height: var(--line-height); 68 | font-weight: var(--font-weight); 69 | cursor: pointer; 70 | white-space: nowrap; 71 | } 72 | 73 | .secondary .button-part { 74 | background-color: var(--secondary-bg-color); 75 | border-color: var(--secondary-border-color); 76 | color: var(--secondary-color); 77 | } 78 | 79 | .secondary .button-part:hover { 80 | background-color: var(--secondary-hover-bg-color); 81 | border-color: var(--secondary-hover-border-color); 82 | color: var(--secondary-hover-color); 83 | } 84 | 85 | .button-part:hover { 86 | background-color: var(--primary-hover-bg-color); 87 | border-color: var(--primary-hover-border-color); 88 | color: var(--primary-hover-color); 89 | } 90 | 91 | .action { 92 | border-bottom-left-radius: var(--border-radius); 93 | border-top-left-radius: var(--border-radius); 94 | } 95 | 96 | .action-no-options { 97 | border-radius: var(--border-radius); 98 | } 99 | 100 | .action-logo { 101 | padding-right: 6px; 102 | fill: currentColor; 103 | } 104 | 105 | .action-label { 106 | display: flex; 107 | justify-content: center; 108 | align-items: center; 109 | } 110 | 111 | .action-chevron { 112 | padding-left: 3px; 113 | padding-right: 3px; 114 | border-left: 1px solid var(--primary-separator-color); 115 | border-bottom-right-radius: var(--border-radius); 116 | border-top-right-radius: var(--border-radius); 117 | } 118 | 119 | .action-chevron:hover { 120 | border-left: 1px solid var(--primary-separator-color); 121 | } 122 | 123 | .secondary .action-chevron { 124 | border-left: 1px solid var(--secondary-separator-color); 125 | } 126 | 127 | .secondary .action-chevron:hover { 128 | border-left: 1px solid var(--secondary-separator-color); 129 | } 130 | 131 | .chevron-icon { 132 | fill: currentColor; 133 | } 134 | 135 | .drop-down { 136 | z-index: 2147483647; 137 | position: absolute; 138 | top: 130%; 139 | right: 0; 140 | display: flex; 141 | flex-direction: column; 142 | padding: 6px 0; 143 | background-color: var(--dropdown-bg-color); 144 | border-width: var(--dropdown-border-width); 145 | border-radius: var(--dropdown-border-radius); 146 | border-style: solid; 147 | border-color: var(--dropdown-border-color); 148 | box-shadow: var(--dropdown-box-shadow); 149 | } 150 | 151 | .left-align-menu .drop-down { 152 | right: auto; 153 | left: 0; 154 | } 155 | 156 | .drop-down::before { 157 | content: ""; 158 | position: absolute; 159 | top: -8px; 160 | right: 4px; 161 | border-left: 8px solid transparent; 162 | border-right: 8px solid transparent; 163 | border-bottom: 8px solid inherit; 164 | border-bottom: 8px solid var(--borderColor-default, var(--color-border-default)); 165 | } 166 | 167 | .drop-down .button-part { 168 | border: none !important; 169 | color: var(--dropdown-color) !important; 170 | background-color: var(--dropdown-bg-color) !important; 171 | min-width: 160px; 172 | } 173 | 174 | .drop-down-action { 175 | cursor: pointer; 176 | font-weight: 400; 177 | } 178 | 179 | .drop-down-action:hover { 180 | background-color: var(--dropdown-hover-bg-color) !important; 181 | } 182 | 183 | /* github */ 184 | 185 | .github { 186 | --font-family: -apple-system, "system-ui", "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, 187 | "Apple Color Emoji", "Segoe UI Emoji"; 188 | --line-height: 1.5; 189 | --font-size: var(--text-body-size-small, 0.75rem); 190 | --font-weight: var(--base-text-weight-medium, 500); 191 | --border-radius: var(--borderRadius-medium, 0.375rem); 192 | --border-width: var(--borderWidth-thin, max(1px, 0.0625rem)); 193 | 194 | --primary-height: var(--control-small-size, 1.75rem); 195 | --primary-bg-color: var(--button-primary-bgColor-rest, var(--color-btn-primary-bg)); 196 | --primary-hover-bg-color: var(--button-primary-bgColor-hover, var(--color-btn-primary-hover-bg)); 197 | --primary-border-color: var(--button-primary-borderColor-rest, var(--color-btn-primary-border)); 198 | --primary-hover-border-color: var(--button-primary-borderColor-hover, var(--color-btn-primary-hover-border)); 199 | --primary-color: var(--button-primary-fgColor-rest, var(--color-btn-primary-text)); 200 | --primary-hover-color: var(--button-primary-fgColor-rest, var(--color-btn-primary-text)); 201 | --primary-box-shadow: var(--shadow-resting-small, var(--color-btn-primary-shadow)), 202 | var(--shadow-highlight, var(--color-btn-primary-inset-shadow)); 203 | 204 | --dropdown-bg-color: var(--overlay-bgColor, var(--color-canvas-overlay)); 205 | --dropdown-hover-bg-color: var(--bgColor-neutral-muted, var(--color-neutral-subtle)); 206 | --dropdown-border-radius: 6px; 207 | --dropdown-border-width: 1px; 208 | --dropdown-border-color: var(--borderColor-default, var(--color-border-default)); 209 | --dropdown-box-shadow: var(--shadow-floating-large, var(--color-shadow-large)); 210 | } 211 | 212 | .github.tall { 213 | --primary-height: var(--control-medium-size, 2rem); 214 | } 215 | 216 | .github .chevron-icon { 217 | padding: 3px; 218 | } 219 | 220 | .github.medium { 221 | --line-height: 1.5; 222 | --primary-height: var(--control-medium-size, 2rem); 223 | --font-size: var(--text-body-size-medium, 0.75rem); 224 | } 225 | 226 | .gitlab { 227 | --font-family: -apple-system, "system-ui", "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, 228 | "Apple Color Emoji", "Segoe UI Emoji"; 229 | --line-height: 1rem; 230 | --font-size: 0.875rem; 231 | --font-weight: 400; 232 | --border-radius: 0.25rem; 233 | --border-width: 0px; 234 | 235 | --primary-bg-color: #1f75cb; 236 | --primary-hover-bg-color: #1068bf; 237 | --primary-border-color: var(--button-primary-borderColor-rest, var(--color-btn-primary-border)); 238 | --primary-hover-border-color: #dedee3; 239 | --primary-color: #fff; 240 | --primary-hover-color: #fff; 241 | --primary-box-shadow: inset 0 0 0 1px #1068bf; 242 | --primary-hover-box-shadow: inset 0 0 0 2px #064787, 0 2px 2px 0 rgba(0, 0, 0, 0.08); 243 | --primary-height: 32px; 244 | --primary-separator-color: #dcdcde; 245 | 246 | --dropdown-color: #333238; 247 | --dropdown-bg-color: #fff; 248 | --dropdown-hover-bg-color: #ececef; 249 | --dropdown-border-width: 1px; 250 | --dropdown-border-radius: 0.25rem; 251 | --dropdown-border-color: #dcdcde; 252 | --dropdown-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 253 | } 254 | 255 | .bitbucket-server { 256 | --font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Fira Sans, Droid Sans, 257 | Helvetica Neue, sans-serif; 258 | --line-height: 20px; 259 | --font-size: 14px; 260 | --font-weight: 500; 261 | --border-radius: 3px; 262 | --border-width: 0; 263 | 264 | --primary-bg-color: var(--ds-background-brand-bold, #0052cc); 265 | --primary-hover-bg-color: var(--ds-background-brand-bold-hovered, #0065ff); 266 | --primary-border-color: var(--aui-button-default-border-color, transparent); 267 | --primary-hover-border-color: var(--aui-button-default-border-color, transparent); 268 | --primary-color: var(--ds-text-inverse, #ffffff) !important; 269 | --primary-hover-color: var(--ds-text-inverse, #ffffff) !important; 270 | --primary-box-shadow: none; 271 | --primary-hover-box-shadow: none; 272 | --primary-height: 30px; 273 | --primary-separator-color: var(--aui-body-background); 274 | 275 | --secondary-bg-color: var(--aui-button-default-bg-color); 276 | --secondary-hover-bg-color: var(--aui-button-default-hover-bg-color); 277 | --secondary-color: var(--aui-button-default-text-color); 278 | --secondary-hover-color: var(--aui-button-default-text-color); 279 | --secondary-separator-color: var(--aui-body-background); 280 | 281 | --dropdown-color: #172b4d; 282 | --dropdown-bg-color: var(--aui-body-background); 283 | --dropdown-hover-bg-color: #ebecf0; 284 | --dropdown-border-width: var(--aui-inline-dialog-border-width); 285 | --dropdown-border-radius: 3px; 286 | --dropdown-border-color: var(--aui-inline-dialog-border-color); 287 | --dropdown-box-shadow: 0 4px 8px -2px var(--aui-shadow2), 0 0 1px var(--aui-shadow2); 288 | } 289 | 290 | .bitbucket { 291 | --font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Fira Sans, Droid Sans, 292 | Helvetica Neue, sans-serif; 293 | --line-height: 20px; 294 | --font-size: 14px; 295 | --font-weight: 500; 296 | --border-radius: 3px; 297 | --border-width: 0; 298 | 299 | --primary-bg-color: rgb(0, 82, 204); 300 | --primary-hover-bg-color: rgba(0, 82, 204, 0.9); 301 | --primary-border-color: var(--aui-button-default-border-color, transparent); 302 | --primary-hover-border-color: var(--aui-button-default-border-color, transparent); 303 | --primary-color: var(--ds-text-inverse, #ffffff) !important; 304 | --primary-hover-color: var(--ds-text-inverse, #ffffff) !important; 305 | --primary-box-shadow: none; 306 | --primary-hover-box-shadow: none; 307 | --primary-height: 32px; 308 | --primary-separator-color: var(--ds-text-inverse, #ffffff) !important; 309 | 310 | --dropdown-color: #172b4d; 311 | --dropdown-bg-color: var(--ds-surface-overlay, #ffffff); 312 | --dropdown-hover-bg-color: #ebecf0; 313 | --dropdown-border-width: 0; 314 | --dropdown-border-radius: var(--ds-border-radius, 3px); 315 | --dropdown-border-color: 0; 316 | --dropdown-box-shadow: var( 317 | --ds-shadow-overlay, 318 | 0 4px 8px -2px rgba(9, 30, 66, 0.25), 319 | 0 0 1px rgba(9, 30, 66, 0.31) 320 | ); 321 | } 322 | 323 | .azure-devops { 324 | --font-family: "Segoe UI", "-apple-system", BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Ubuntu, Arial, 325 | sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 326 | --primary-bg-color: var(--communication-background, rgba(0, 120, 212, 1)); 327 | --primary-hover-bg-color: rgba(var(--palette-primary-darkened-6, 0, 103, 181), 1); 328 | --primary-color: var(--text-on-communication-background, rgba(255, 255, 255, 1)); 329 | --primary-hover-color: var(--text-on-communication-background, rgba(255, 255, 255, 1)); 330 | --primary-separator-color: rgba(var(--palette-primary-darkened-10, 0, 91, 161), 1); 331 | --font-weight: 600; 332 | --primary-height: 32px; 333 | 334 | --dropdown-color: var(--text-primary-color, rgba(0, 0, 0, 0.9)); 335 | --dropdown-bg-color: var(--callout-background-color, rgba(255, 255, 255, 1)); 336 | --dropdown-hover-bg-color: var(--palette-black-alpha-4, rgba(0, 0, 0, 0.04)); 337 | --dropdown-box-shadow: 0 3.2px 7.2px 0 var(--callout-shadow-color, rgba(0, 0, 0, 0.132)), 338 | 0 0.6px 1.8px 0 var(--callout-shadow-secondary-color, rgba(0, 0, 0, 0.108)); 339 | --dropdown-border-width: 0; 340 | --dropdown-border-radius: 4px; 341 | 342 | --border-radius: 2px; 343 | --border-width: 0px; 344 | } 345 | -------------------------------------------------------------------------------- /src/button/button.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import { useEffect, useMemo, useRef, useState } from "react"; 3 | import { useHotkeys } from "react-hotkeys-hook"; 4 | import Logo from "react:./logo-mark.svg"; 5 | 6 | import { useStorage } from "@plasmohq/storage/hook"; 7 | import { Storage } from "@plasmohq/storage"; 8 | 9 | import { DEFAULT_GITPOD_ENDPOINT, EVENT_CURRENT_URL_CHANGED } from "~constants"; 10 | import { STORAGE_KEY_ADDRESS, STORAGE_KEY_ALWAYS_OPTIONS, STORAGE_KEY_NEW_TAB } from "~storage"; 11 | 12 | import type { SupportedApplication } from "./button-contributions"; 13 | import { CaretForProvider } from "./CaretForProvider"; 14 | 15 | type Props = { 16 | application: SupportedApplication; 17 | additionalClassNames?: string[]; 18 | urlTransformer?: (url: string) => string; 19 | }; 20 | export const GitpodButton = ({ application, additionalClassNames, urlTransformer }: Props) => { 21 | const [address] = useStorage(STORAGE_KEY_ADDRESS); 22 | const [openInNewTab] = useStorage(STORAGE_KEY_NEW_TAB, true); 23 | const [disableAutostart] = useStorage(STORAGE_KEY_ALWAYS_OPTIONS, false); 24 | const [showDropdown, setShowDropdown] = useState(false); 25 | const [currentHref, setCurrentHref] = useState(window.location.href); 26 | 27 | const linkRef = useRef(null); 28 | 29 | useEffect(() => { 30 | const handleUrlChange = () => { 31 | setCurrentHref(window.location.href); 32 | }; 33 | 34 | document.addEventListener(EVENT_CURRENT_URL_CHANGED, handleUrlChange); 35 | 36 | return () => { 37 | document.removeEventListener(EVENT_CURRENT_URL_CHANGED, handleUrlChange); 38 | }; 39 | }, []); 40 | 41 | // if the user has no address configured, set it to the default endpoint 42 | useEffect(() => { 43 | (async () => { 44 | // we don't use the useStorage hook because it does not offer a way see if the value is set (it could just be loading), meaning we could end up setting the default endpoint even if it's already set 45 | const storage = new Storage(); 46 | const persistedAddress = await storage.get(STORAGE_KEY_ADDRESS); 47 | if (!persistedAddress) { 48 | await storage.set(STORAGE_KEY_ADDRESS, DEFAULT_GITPOD_ENDPOINT); 49 | } 50 | })(); 51 | }, []); 52 | 53 | const actions = useMemo(() => { 54 | const parsedHref = !urlTransformer ? currentHref : urlTransformer(currentHref); 55 | return [ 56 | { 57 | href: `${address}/?autostart=${!disableAutostart}#${parsedHref}`, 58 | label: "Open", 59 | }, 60 | { 61 | href: `${address}/?autostart=false#${parsedHref}`, 62 | label: "Open with options...", 63 | }, 64 | ]; 65 | }, [address, disableAutostart, currentHref, urlTransformer]); 66 | const dropdownRef = useRef(null); 67 | const firstActionRef = useRef(null); 68 | 69 | const toggleDropdown = () => { 70 | setShowDropdown(!showDropdown); 71 | }; 72 | 73 | const handleDocumentClick = (event: Event) => { 74 | if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { 75 | setShowDropdown(false); 76 | } 77 | }; 78 | 79 | useEffect(() => { 80 | document.addEventListener("click", handleDocumentClick); 81 | return () => { 82 | document.removeEventListener("click", handleDocumentClick); 83 | }; 84 | }, []); 85 | 86 | useEffect(() => { 87 | if (showDropdown && firstActionRef.current) { 88 | firstActionRef.current.focus(); 89 | } 90 | }, [showDropdown]); 91 | 92 | const target = openInNewTab ? "_blank" : "_self"; 93 | 94 | useHotkeys("alt+g", () => linkRef.current?.click(), [linkRef.current]); 95 | 96 | return ( 97 |
102 |
103 | 110 | 111 | 112 | {actions[0].label} 113 | 114 | 115 | {!disableAutostart && ( 116 | 125 | )} 126 |
127 | 128 | {showDropdown && ( 129 |
{ 133 | if (e.key === "Escape") { 134 | setShowDropdown(false); 135 | } 136 | }} 137 | > 138 | {actions.slice(1).map((action) => ( 139 | 147 | {action.label} 148 | 149 | ))} 150 |
151 | )} 152 |
153 | ); 154 | }; 155 | -------------------------------------------------------------------------------- /src/button/logo-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/forms/Button.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import React, { forwardRef, type FC, type ForwardedRef, type ReactNode } from "react"; 3 | 4 | export type ButtonProps = { 5 | type?: "primary" | "secondary" | "danger" | "danger.secondary" | "transparent"; 6 | size?: "small" | "medium" | "block"; 7 | disabled?: boolean; 8 | className?: string; 9 | autoFocus?: boolean; 10 | htmlType?: "button" | "submit" | "reset"; 11 | icon?: ReactNode; 12 | children?: ReactNode; 13 | onClick?: ButtonOnClickHandler; 14 | }; 15 | 16 | // Allow w/ or w/o handling event argument 17 | type ButtonOnClickHandler = React.DOMAttributes["onClick"] | (() => void); 18 | 19 | // eslint-disable-next-line react/display-name 20 | export const Button = forwardRef( 21 | ( 22 | { 23 | type = "primary", 24 | className, 25 | htmlType = "button", 26 | disabled = false, 27 | autoFocus = false, 28 | size, 29 | icon, 30 | children, 31 | onClick, 32 | }, 33 | ref: ForwardedRef, 34 | ) => { 35 | return ( 36 | 77 | ); 78 | }, 79 | ); 80 | 81 | // TODO: Consider making this a LoadingButton variant instead 82 | type ButtonContentProps = { 83 | icon?: ReactNode; 84 | children?: ReactNode; 85 | }; 86 | const ButtonContent: FC = ({ icon, children }) => { 87 | if (!icon) { 88 | return {children}; 89 | } 90 | 91 | return ( 92 |
93 | {icon ? icon : null} 94 | {children && {children}} 95 |
96 | ); 97 | }; 98 | -------------------------------------------------------------------------------- /src/components/forms/CheckboxInputField.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import React, { useCallback, useId, type FC, type PropsWithChildren, type ReactNode } from "react"; 3 | 4 | import { InputField } from "./InputField"; 5 | import { InputFieldHint } from "./InputFieldHint"; 6 | 7 | type CheckboxListFieldProps = { 8 | label: string; 9 | error?: ReactNode; 10 | className?: string; 11 | topMargin?: boolean; 12 | }; 13 | 14 | // CheckboxListField is a wrapper for a list of related CheckboxInputField components. 15 | export const CheckboxListField: FC> = ({ 16 | label, 17 | error, 18 | className, 19 | topMargin, 20 | children, 21 | }) => { 22 | return ( 23 | 24 |
{children}
25 |
26 | ); 27 | }; 28 | 29 | type CheckboxInputFieldProps = { 30 | id?: string; 31 | value?: string; 32 | checked: boolean; 33 | disabled?: boolean; 34 | label: ReactNode; 35 | hint?: ReactNode; 36 | error?: ReactNode; 37 | topMargin?: boolean; 38 | containerClassName?: string; 39 | onChange: (checked: boolean) => void; 40 | }; 41 | export const CheckboxInputField: FC = ({ 42 | id, 43 | value, 44 | label, 45 | hint, 46 | error, 47 | checked, 48 | disabled = false, 49 | topMargin = true, 50 | containerClassName, 51 | onChange, 52 | }) => { 53 | const maybeId = useId(); 54 | const elementId = id || maybeId; 55 | 56 | const handleChange = useCallback( 57 | (e) => { 58 | onChange(e.target.checked); 59 | }, 60 | [onChange], 61 | ); 62 | 63 | return ( 64 | // Intentionally not passing label and hint to InputField because we want to render them differently for checkboxes. 65 | 66 | 94 | 95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /src/components/forms/InputField.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import React, { memo, type PropsWithChildren, type ReactNode } from "react"; 3 | 4 | import { InputFieldHint } from "./InputFieldHint"; 5 | 6 | type Props = { 7 | label?: ReactNode; 8 | id?: string; 9 | hint?: ReactNode; 10 | error?: ReactNode; 11 | topMargin?: boolean; 12 | className?: string; 13 | disabled?: boolean; 14 | }; 15 | 16 | // eslint-disable-next-line react/display-name 17 | export const InputField = memo( 18 | ({ label, id, hint, error, topMargin = true, className, children, disabled = false }: PropsWithChildren) => { 19 | return ( 20 |
21 | {label && ( 22 | 33 | )} 34 | {children} 35 | {error && {error}} 36 | {hint && {hint}} 37 |
38 | ); 39 | }, 40 | ); 41 | -------------------------------------------------------------------------------- /src/components/forms/InputFieldHint.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import React, { type FC, type PropsWithChildren } from "react"; 3 | 4 | type Props = { 5 | disabled?: boolean; 6 | }; 7 | export const InputFieldHint: FC> = ({ disabled = false, children }) => { 8 | return ( 9 | 15 | {children} 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/forms/TextInputField.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2023 Gitpod GmbH. All rights reserved. 3 | * Licensed under the GNU Affero General Public License (AGPL). 4 | * See License.AGPL.txt in the project root for license information. 5 | */ 6 | 7 | import classNames from "classnames"; 8 | import React, { memo, useCallback, useId, type PropsWithChildren, type ReactNode } from "react"; 9 | 10 | import { InputField } from "./InputField"; 11 | 12 | type TextInputFieldTypes = "text" | "password" | "email" | "url"; 13 | 14 | type Props = { 15 | type?: TextInputFieldTypes; 16 | label?: ReactNode; 17 | value: string; 18 | id?: string; 19 | hint?: ReactNode; 20 | error?: ReactNode; 21 | placeholder?: string; 22 | disabled?: boolean; 23 | required?: boolean; 24 | topMargin?: boolean; 25 | containerClassName?: string; 26 | onChange: (newValue: string) => void; 27 | onBlur?: () => void; 28 | }; 29 | 30 | // eslint-disable-next-line react/display-name 31 | export const TextInputField = memo( 32 | ({ 33 | type = "text", 34 | label, 35 | value, 36 | id, 37 | placeholder, 38 | hint, 39 | error, 40 | disabled = false, 41 | required = false, 42 | topMargin, 43 | containerClassName, 44 | onChange, 45 | onBlur, 46 | }: PropsWithChildren) => { 47 | const maybeId = useId(); 48 | const elementId = id || maybeId; 49 | 50 | return ( 51 | 59 | 70 | 71 | ); 72 | }, 73 | ); 74 | 75 | type TextInputProps = { 76 | type?: TextInputFieldTypes; 77 | value: string; 78 | className?: string; 79 | id?: string; 80 | placeholder?: string; 81 | disabled?: boolean; 82 | required?: boolean; 83 | onChange?: (newValue: string) => void; 84 | onBlur?: () => void; 85 | }; 86 | 87 | // eslint-disable-next-line react/display-name 88 | export const TextInput = memo( 89 | ({ 90 | type = "text", 91 | value, 92 | className, 93 | id, 94 | placeholder, 95 | disabled = false, 96 | required = false, 97 | onChange, 98 | onBlur, 99 | }: PropsWithChildren) => { 100 | const handleChange = useCallback( 101 | (e) => { 102 | onChange && onChange(e.target.value); 103 | }, 104 | [onChange], 105 | ); 106 | 107 | const handleBlur = useCallback(() => onBlur && onBlur(), [onBlur]); 108 | 109 | return ( 110 | 128 | ); 129 | }, 130 | ); 131 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_GITPOD_ENDPOINT = "https://gitpod.io"; 2 | export const ALL_ORIGINS_WILDCARD = "*://*/*"; 3 | export const EVENT_CURRENT_URL_CHANGED = "current-url-changed"; 4 | -------------------------------------------------------------------------------- /src/contents/button.tsx: -------------------------------------------------------------------------------- 1 | import cssText from "data-text:../button/button.css"; 2 | import type { PlasmoCSConfig, PlasmoGetInlineAnchor } from "plasmo"; 3 | import React, { type ReactElement } from "react"; 4 | 5 | import { EVENT_CURRENT_URL_CHANGED } from "~constants"; 6 | 7 | import { GitpodButton } from "../button/button"; 8 | import { buttonContributions, isSiteSuitable, type ButtonContributionParams } from "../button/button-contributions"; 9 | 10 | // keep in sync with DEFAULT_HOSTS in src/button/button-contributions.ts 11 | export const config: PlasmoCSConfig = { 12 | matches: ["https://github.com/*", "https://gitlab.com/*", "https://bitbucket.org/*", "https://dev.azure.com/*"], 13 | }; 14 | 15 | export const getStyle = () => { 16 | const style = document.createElement("style"); 17 | style.textContent = cssText; 18 | return style; 19 | }; 20 | 21 | class ButtonContributionManager { 22 | private buttons = new Map(); 23 | private active: { 24 | contribution: ButtonContributionParams; 25 | anchor: HTMLElement; 26 | }; 27 | 28 | private currentHref = window.location.href; 29 | 30 | _disabled = false; 31 | 32 | constructor(private contributions: ButtonContributionParams[]) { 33 | if (!this._disabled) { 34 | const isSuitable = isSiteSuitable(); 35 | if (!isSuitable) { 36 | this._disabled = true; 37 | } 38 | } 39 | 40 | for (const contribution of this.contributions) { 41 | const containerId = this.getContainerId(contribution); 42 | if (!this.buttons.has(containerId)) { 43 | this.buttons.set( 44 | containerId, 45 | , 51 | ); 52 | } 53 | } 54 | } 55 | 56 | private getContainerId(contribution: ButtonContributionParams) { 57 | return `gp-btn-cnt-${contribution.application}${contribution.additionalClassNames?.map((c) => "-" + c)?.join("")}`; 58 | } 59 | 60 | private updateActive(active?: { contribution: ButtonContributionParams; anchor: HTMLElement }) { 61 | if (this.active && this.active.contribution.id !== active?.contribution.id) { 62 | const element = document.getElementById(this.active.contribution.id); 63 | if (element) { 64 | element.remove(); 65 | } 66 | } 67 | this.active = active; 68 | } 69 | 70 | public getInlineAnchor(): HTMLElement | null { 71 | if (this._disabled) { 72 | return null; 73 | } 74 | 75 | if (this.currentHref !== window.location.href) { 76 | this.currentHref = window.location.href; 77 | const event = new CustomEvent(EVENT_CURRENT_URL_CHANGED); 78 | document.dispatchEvent(event); 79 | } 80 | 81 | for (const contribution of this.contributions) { 82 | const isActive = this.isActive(contribution); 83 | if (isActive) { 84 | if (this.active?.contribution.id === contribution.id && this.active?.anchor?.isConnected) { 85 | // do nothing 86 | return null; 87 | } else { 88 | // update 89 | const anchor = this.installAnchor(contribution); 90 | if (anchor) { 91 | this.updateActive({ 92 | contribution, 93 | anchor, 94 | }); 95 | return anchor; 96 | } 97 | } 98 | } 99 | } 100 | this.updateActive(undefined); 101 | return null; 102 | } 103 | 104 | public getElement(): ReactElement | null { 105 | if (!this.active) { 106 | return null; 107 | } 108 | return this.buttons.get(this.getContainerId(this.active.contribution)); 109 | } 110 | 111 | /** 112 | * Checks if the contribution applies to the current page. 113 | */ 114 | private isActive(contrib: ButtonContributionParams) { 115 | if (typeof contrib.match === "function" && !contrib.match()) { 116 | return false; 117 | } else if (typeof contrib.match === "object" && !contrib.match.test(window.location.href)) { 118 | return false; 119 | } 120 | const parent = this.lookupElement(contrib.selector); 121 | if (parent === null) { 122 | return false; 123 | } 124 | return true; 125 | } 126 | 127 | private lookupElement(path: string): HTMLElement | null { 128 | if (!path) { 129 | return null; 130 | } 131 | if (path.startsWith("xpath:")) { 132 | const xpath = path.substring("xpath:".length); 133 | const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); 134 | return result.singleNodeValue as HTMLElement; 135 | } else { 136 | return document.querySelector(path); 137 | } 138 | } 139 | 140 | private installAnchor(contrib: ButtonContributionParams) { 141 | const parent = this.lookupElement(contrib.selector); 142 | if (parent === null) { 143 | return null; 144 | } 145 | const containerConfig = contrib.containerElement; 146 | let container: HTMLElement; 147 | if (containerConfig === null) { 148 | container = createElement("div"); 149 | } else { 150 | container = createElement(containerConfig.type, containerConfig.props); 151 | } 152 | container.id = contrib.id; 153 | const before = this.lookupElement(contrib.insertBefore); 154 | 155 | if (contrib.manipulations) { 156 | for (const manipulation of contrib.manipulations) { 157 | const element = this.lookupElement(manipulation.element); 158 | if (element) { 159 | if (manipulation.remove) { 160 | element.classList.remove(manipulation.remove); 161 | } 162 | if (manipulation.add) { 163 | element.classList.add(manipulation.add); 164 | } 165 | if (element instanceof HTMLElement && manipulation.style) { 166 | for (const key in manipulation.style) { 167 | element.style[key] = manipulation.style[key]; 168 | } 169 | } 170 | } 171 | } 172 | } 173 | // make sure we manipulate the dom after applying any selectors. 174 | if (before) { 175 | parent.insertBefore(container, before); 176 | } else { 177 | parent.appendChild(container); 178 | } 179 | 180 | // plasmo adds the element as a sibling, so we create a dummy element within the container and point to that. 181 | const dummy = createElement("div"); 182 | dummy.style.display = "none"; 183 | container.appendChild(dummy); 184 | 185 | return dummy; 186 | } 187 | 188 | public getContributions() { 189 | return this.contributions; 190 | } 191 | } 192 | 193 | const manager = new ButtonContributionManager(buttonContributions); 194 | 195 | export const getInlineAnchor: PlasmoGetInlineAnchor = () => { 196 | return manager.getInlineAnchor(); 197 | }; 198 | 199 | export default () => { 200 | return manager.getElement(); 201 | }; 202 | 203 | function createElement(name: keyof HTMLElementTagNameMap, styles?: Partial) { 204 | const element = document.createElement(name); 205 | if (styles) { 206 | for (const key in styles) { 207 | element.style[key] = styles[key]; 208 | } 209 | } 210 | return element; 211 | } 212 | -------------------------------------------------------------------------------- /src/contents/gitpod-dashboard.ts: -------------------------------------------------------------------------------- 1 | import { Storage } from "@plasmohq/storage"; 2 | import type { PlasmoCSConfig } from "plasmo"; 3 | import { STORAGE_AUTOMATICALLY_DETECT_GITPOD, STORAGE_KEY_ADDRESS } from "~storage"; 4 | import { parseEndpoint } from "~utils/parse-endpoint"; 5 | 6 | /** 7 | * Checks if the current site is a Gitpod instance. 8 | */ 9 | const isSiteGitpod = (): boolean => { 10 | return !!document.head.querySelector("meta[name=Gitpod]"); 11 | }; 12 | 13 | export const config: PlasmoCSConfig = { 14 | matches: ["https://gitpod.io/*", "https://app.gitpod.io/*", "https://*.gitpod.cloud/*"], 15 | }; 16 | 17 | const storage = new Storage(); 18 | 19 | const automaticallyUpdateEndpoint = async () => { 20 | if ((await storage.get(STORAGE_AUTOMATICALLY_DETECT_GITPOD)) === false) { 21 | return; 22 | } 23 | 24 | const currentHost = window.location.host; 25 | const currentlyStoredEndpoint = await storage.get(STORAGE_KEY_ADDRESS); 26 | if (!currentlyStoredEndpoint || new URL(currentlyStoredEndpoint).host !== currentHost) { 27 | console.log(`Gitpod extension: switching default endpoint to ${currentHost}.`); 28 | await storage.set(STORAGE_KEY_ADDRESS, parseEndpoint(window.location.origin)); 29 | } 30 | }; 31 | 32 | if (isSiteGitpod()) { 33 | localStorage.setItem("extension-last-seen-active", new Date().toISOString()); 34 | const targetElement = document.querySelector(`meta[name="extension-active"]`); 35 | if (targetElement) { 36 | targetElement.setAttribute("content", "true"); 37 | } 38 | 39 | automaticallyUpdateEndpoint(); 40 | } 41 | -------------------------------------------------------------------------------- /src/hooks/use-temporary-state.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from "react"; 2 | 3 | type UseTemporaryStateReturnType = [value: ValueType, set: (value: ValueType) => void]; 4 | 5 | /** 6 | * @description Hook to have state that reverts to a default value after a timeout when you update it. Useful for temporarily showing messages or disabling buttons. 7 | * 8 | * @param defaultValue Default value 9 | * @param timeout Milliseconds to revert to default value after setting a temporary value 10 | * @returns [value, setTemporaryValue] 11 | */ 12 | export const useTemporaryState = ( 13 | defaultValue: ValueType, 14 | timeout: number, 15 | ): UseTemporaryStateReturnType => { 16 | const [value, setValue] = useState(defaultValue); 17 | const timeoutRef = useRef>(); 18 | 19 | const setTemporaryValue = useCallback( 20 | (tempValue: ValueType, revertValue?: ValueType) => { 21 | timeoutRef.current && clearTimeout(timeoutRef.current); 22 | 23 | setValue(tempValue); 24 | 25 | timeoutRef.current = setTimeout(() => { 26 | setValue(revertValue !== undefined ? revertValue : defaultValue); 27 | }, timeout); 28 | }, 29 | [defaultValue, timeout], 30 | ); 31 | 32 | useEffect(() => { 33 | if (timeoutRef.current) { 34 | clearTimeout(timeoutRef.current); 35 | } 36 | }, []); 37 | 38 | return [value, setTemporaryValue]; 39 | }; 40 | -------------------------------------------------------------------------------- /src/popup.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | html, 7 | body { 8 | @apply h-full; 9 | } 10 | body { 11 | @apply bg-white dark:bg-gitpod-black text-black dark:text-white; 12 | } 13 | h1 { 14 | @apply text-gray-900 dark:text-gray-100 font-bold; 15 | line-height: 64px; 16 | font-size: 48px; 17 | } 18 | h2 { 19 | @apply text-base text-gray-500 dark:text-gray-400; 20 | } 21 | h3 { 22 | @apply text-2xl text-gray-800 dark:text-gray-100 leading-9 font-semibold; 23 | } 24 | h4 { 25 | @apply pb-2 text-sm font-semibold text-gray-600 dark:text-gray-400; 26 | } 27 | p { 28 | @apply text-sm text-gray-400 dark:text-gray-600; 29 | } 30 | } 31 | 32 | @keyframes fadeIn { 33 | from { 34 | opacity: 0; 35 | } 36 | to { 37 | opacity: 1; 38 | } 39 | } 40 | 41 | .fade-in { 42 | animation: fadeIn 220ms ease-in-out; 43 | } 44 | -------------------------------------------------------------------------------- /src/popup.tsx: -------------------------------------------------------------------------------- 1 | import { CheckIcon } from "lucide-react"; 2 | import React, { useCallback, useEffect, useState, type FormEvent, type PropsWithChildren } from "react"; 3 | import browser from "webextension-polyfill"; 4 | 5 | import "./popup.css"; 6 | 7 | import { Storage } from "@plasmohq/storage"; 8 | import { useStorage } from "@plasmohq/storage/hook"; 9 | 10 | import { Button } from "~components/forms/Button"; 11 | import { CheckboxInputField } from "~components/forms/CheckboxInputField"; 12 | import { InputField } from "~components/forms/InputField"; 13 | import { TextInput } from "~components/forms/TextInputField"; 14 | import { ALL_ORIGINS_WILDCARD, DEFAULT_GITPOD_ENDPOINT } from "~constants"; 15 | import { useTemporaryState } from "~hooks/use-temporary-state"; 16 | import { 17 | STORAGE_AUTOMATICALLY_DETECT_GITPOD, 18 | STORAGE_KEY_ADDRESS, 19 | STORAGE_KEY_ALWAYS_OPTIONS, 20 | STORAGE_KEY_NEW_TAB, 21 | } from "~storage"; 22 | import { hostToOrigin, parseEndpoint } from "~utils/parse-endpoint"; 23 | import { canAccessAllSites } from "~utils/permissions"; 24 | 25 | const storage = new Storage(); 26 | 27 | const Animate = ({ children, on }: PropsWithChildren<{ on?: string }>) => { 28 | return on === undefined ? 29 |
{children}
30 | // see popup.css for transition styles 31 | :
32 | {children} 33 |
; 34 | }; 35 | 36 | function IndexPopup() { 37 | const [error, setError] = useState(); 38 | 39 | const [storedAddress] = useStorage(STORAGE_KEY_ADDRESS, DEFAULT_GITPOD_ENDPOINT); 40 | const [address, setAddress] = useState(storedAddress); 41 | const [justSaved, setJustSaved] = useTemporaryState(false, 2000); 42 | 43 | const updateAddress = useCallback( 44 | (e: FormEvent) => { 45 | e.preventDefault(); 46 | 47 | try { 48 | const parsedAddress = parseEndpoint(address); 49 | const origin = hostToOrigin(parsedAddress); 50 | 51 | storage 52 | .setItem(STORAGE_KEY_ADDRESS, parsedAddress) 53 | .catch((e) => { 54 | setError(e.message); 55 | }) 56 | .then(() => { 57 | setJustSaved(true); 58 | }); 59 | 60 | if (origin) { 61 | browser.permissions.request({ origins: [origin] }).catch((e) => { 62 | setError(e.message); 63 | }); 64 | } 65 | } catch (e) { 66 | setError(e.message); 67 | } 68 | }, 69 | [address, setError], 70 | ); 71 | 72 | // Need to update address when storage changes. This also applies for the initial load. 73 | useEffect(() => { 74 | setAddress(storedAddress); 75 | }, [storedAddress]); 76 | 77 | const [openInNewTab, setOpenInNewTab] = useStorage(STORAGE_KEY_NEW_TAB, true); 78 | const [allSites, setAllSites] = useState(false); 79 | 80 | useEffect(() => { 81 | (async () => { 82 | setAllSites(await canAccessAllSites()); 83 | })(); 84 | }, []); 85 | const [disableAutostart, setDisableAutostart] = useStorage(STORAGE_KEY_ALWAYS_OPTIONS, false); 86 | 87 | const [enableInstanceHopping, setEnableInstanceHopping] = useStorage( 88 | STORAGE_AUTOMATICALLY_DETECT_GITPOD, 89 | true, 90 | ); 91 | 92 | return ( 93 |
101 |
102 | 107 |
108 | 109 | 118 |
119 |
120 | 125 | { 130 | if (checked) { 131 | const granted = await browser.permissions.request({ 132 | origins: [ALL_ORIGINS_WILDCARD], 133 | }); 134 | setAllSites(granted); 135 | } else { 136 | const success = await browser.permissions.remove({ 137 | origins: [ALL_ORIGINS_WILDCARD], 138 | }); 139 | setAllSites(!success); 140 | } 141 | }} 142 | /> 143 | 149 | 155 | 156 | 157 | {/* show error if set */} 158 |
171 | {error} 172 |
173 |
174 | ); 175 | } 176 | 177 | export default IndexPopup; 178 | -------------------------------------------------------------------------------- /src/storage.ts: -------------------------------------------------------------------------------- 1 | export const STORAGE_KEY_ADDRESS = "gitpod-installation-address"; 2 | export const STORAGE_KEY_NEW_TAB = "gitpod-installation-new-tab"; 3 | export const STORAGE_AUTOMATICALLY_DETECT_GITPOD = "gitpod-installation-automatically-detect-gitpod"; 4 | export const STORAGE_KEY_ALWAYS_OPTIONS = "gitpod-installation-always-options"; 5 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .new-tab { 2 | background-color: #f0f0f0; 3 | border: 1px solid #ccc; 4 | border-radius: 4px; 5 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); 6 | color: #333; 7 | display: block; 8 | font-size: 12px; 9 | line-height: 16px; 10 | margin: 0 0 10px; 11 | padding: 10px; 12 | position: relative; 13 | text-decoration: none; 14 | width: 100%; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/parse-endpoint.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import { hostToOrigin, parseEndpoint } from "./parse-endpoint"; 4 | 5 | describe("parseEndpoint", () => { 6 | it("parses valid hosts", () => { 7 | expect(parseEndpoint("https://gitpod.io/new")).to.equal("https://gitpod.io"); 8 | expect(parseEndpoint("gitpod.io")).to.equal("https://gitpod.io"); 9 | expect(parseEndpoint("gitpod.io/new")).to.equal("https://gitpod.io"); 10 | expect(parseEndpoint("gitpod://")).to.equal("gitpod://"); 11 | expect(parseEndpoint("http://localhost:3000")).to.equal("http://localhost:3000"); 12 | }); 13 | 14 | it("does not parse invalid hosts", () => { 15 | expect(() => parseEndpoint("gitpod")).to.throw(TypeError); 16 | expect(() => parseEndpoint("ftp://gitpod.io")).to.throw(TypeError); 17 | expect(() => parseEndpoint("https://")).to.throw(TypeError); 18 | }); 19 | }); 20 | 21 | describe("hostToOrigin", () => { 22 | it("converts hosts to origins", () => { 23 | expect(hostToOrigin("https://gitpod.io")).to.equal("https://gitpod.io/*"); 24 | expect(hostToOrigin("http://localhost:3000")).to.equal("http://localhost:3000/*"); 25 | }); 26 | 27 | it("does not convert invalid hosts", () => { 28 | expect(hostToOrigin("ftp://gitpod.io")).to.be.undefined; 29 | expect(hostToOrigin("gitpod://")).to.be.undefined; 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/utils/parse-endpoint.ts: -------------------------------------------------------------------------------- 1 | import isUrl from "validator/es/lib/isURL"; 2 | 3 | const allowedProtocols = ["http:", "https:", "gitpod:"]; 4 | 5 | export const parseEndpoint = (input: string): string => { 6 | let url: URL; 7 | 8 | if (URL.canParse(input)) { 9 | url = new URL(input); 10 | 11 | if (!allowedProtocols.includes(url.protocol)) { 12 | throw new TypeError(`Invalid protocol in URL: ${input}`); 13 | } 14 | } else if (isUrl(input, { require_protocol: false, protocols: ["http", "https", "gitpod"] })) { 15 | url = new URL(`https://${input}`); 16 | } else { 17 | throw new TypeError(`Invalid URL: ${input}`); 18 | } 19 | 20 | return `${url.protocol}//${url.host}`; 21 | }; 22 | 23 | export const hostToOrigin = (host: string): string | undefined => { 24 | const { origin, protocol } = new URL(host); 25 | if (origin === "null" || !["http:", "https:"].includes(protocol)) { 26 | return undefined; 27 | } 28 | 29 | return `${origin}/*`; 30 | }; 31 | -------------------------------------------------------------------------------- /src/utils/permissions.ts: -------------------------------------------------------------------------------- 1 | import browser from "webextension-polyfill"; 2 | 3 | import { ALL_ORIGINS_WILDCARD } from "~constants"; 4 | 5 | export const canAccessAllSites = async () => { 6 | return await browser.permissions.contains({ origins: [ALL_ORIGINS_WILDCARD] }); 7 | }; 8 | 9 | export const canAccessOrigin = async (origin: string) => { 10 | return await browser.permissions.contains({ origins: [origin] }); 11 | }; 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.tsx"], 4 | plugins: [require("@tailwindcss/forms")], 5 | important: true, 6 | theme: { 7 | extend: { 8 | colors: { 9 | "gitpod-black": "#161616", 10 | "kumquat-base": "#FFAE33", 11 | "kumquat-ripe": "#FFB45B", 12 | "gitpod-red": "#CE4A3E", 13 | }, 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitpod-test", 3 | "displayName": "Gitpod Tests", 4 | "type": "module", 5 | "version": "2.0.0", 6 | "scripts": { 7 | "test": "cp ../src/button/button-contributions.ts ./src/button-contributions-copy.ts && tsc && mocha dist/**/*.spec.js" 8 | }, 9 | "devDependencies": { 10 | "@types/chai": "^4.3.6", 11 | "@types/chrome": "0.0.243", 12 | "@types/mocha": "^10.0.1", 13 | "@types/node": "20.5.0", 14 | "chai": "^4.3.8", 15 | "htmlnano": "^2.0.4", 16 | "mocha": "^10.2.0", 17 | "prettier": "^3.0.3", 18 | "puppeteer": "^21.1.1", 19 | "ts-node": "^10.9.1", 20 | "source-map-support": "^0.5.21", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | devDependencies: 8 | '@types/chai': 9 | specifier: ^4.3.6 10 | version: 4.3.6 11 | '@types/chrome': 12 | specifier: 0.0.243 13 | version: 0.0.243 14 | '@types/mocha': 15 | specifier: ^10.0.1 16 | version: 10.0.1 17 | '@types/node': 18 | specifier: 20.5.0 19 | version: 20.5.0 20 | chai: 21 | specifier: ^4.3.8 22 | version: 4.3.8 23 | htmlnano: 24 | specifier: ^2.0.4 25 | version: 2.0.4 26 | mocha: 27 | specifier: ^10.2.0 28 | version: 10.2.0 29 | prettier: 30 | specifier: ^3.0.3 31 | version: 3.0.3 32 | puppeteer: 33 | specifier: ^21.1.1 34 | version: 21.1.1 35 | source-map-support: 36 | specifier: ^0.5.21 37 | version: 0.5.21 38 | ts-node: 39 | specifier: ^10.9.1 40 | version: 10.9.1(@types/node@20.5.0)(typescript@5.2.2) 41 | typescript: 42 | specifier: ^5.2.2 43 | version: 5.2.2 44 | 45 | packages: 46 | 47 | /@babel/code-frame@7.22.13: 48 | resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} 49 | engines: {node: '>=6.9.0'} 50 | dependencies: 51 | '@babel/highlight': 7.22.13 52 | chalk: 2.4.2 53 | dev: true 54 | 55 | /@babel/helper-validator-identifier@7.22.15: 56 | resolution: {integrity: sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==} 57 | engines: {node: '>=6.9.0'} 58 | dev: true 59 | 60 | /@babel/highlight@7.22.13: 61 | resolution: {integrity: sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==} 62 | engines: {node: '>=6.9.0'} 63 | dependencies: 64 | '@babel/helper-validator-identifier': 7.22.15 65 | chalk: 2.4.2 66 | js-tokens: 4.0.0 67 | dev: true 68 | 69 | /@cspotcode/source-map-support@0.8.1: 70 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 71 | engines: {node: '>=12'} 72 | dependencies: 73 | '@jridgewell/trace-mapping': 0.3.9 74 | dev: true 75 | 76 | /@jridgewell/resolve-uri@3.1.1: 77 | resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} 78 | engines: {node: '>=6.0.0'} 79 | dev: true 80 | 81 | /@jridgewell/sourcemap-codec@1.4.15: 82 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 83 | dev: true 84 | 85 | /@jridgewell/trace-mapping@0.3.9: 86 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 87 | dependencies: 88 | '@jridgewell/resolve-uri': 3.1.1 89 | '@jridgewell/sourcemap-codec': 1.4.15 90 | dev: true 91 | 92 | /@puppeteer/browsers@1.7.0: 93 | resolution: {integrity: sha512-sl7zI0IkbQGak/+IE3VEEZab5SSOlI5F6558WvzWGC1n3+C722rfewC1ZIkcF9dsoGSsxhsONoseVlNQG4wWvQ==} 94 | engines: {node: '>=16.3.0'} 95 | hasBin: true 96 | dependencies: 97 | debug: 4.3.4(supports-color@8.1.1) 98 | extract-zip: 2.0.1 99 | progress: 2.0.3 100 | proxy-agent: 6.3.0 101 | tar-fs: 3.0.4 102 | unbzip2-stream: 1.4.3 103 | yargs: 17.7.1 104 | transitivePeerDependencies: 105 | - supports-color 106 | dev: true 107 | 108 | /@tootallnate/quickjs-emscripten@0.23.0: 109 | resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} 110 | dev: true 111 | 112 | /@tsconfig/node10@1.0.9: 113 | resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} 114 | dev: true 115 | 116 | /@tsconfig/node12@1.0.11: 117 | resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} 118 | dev: true 119 | 120 | /@tsconfig/node14@1.0.3: 121 | resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} 122 | dev: true 123 | 124 | /@tsconfig/node16@1.0.4: 125 | resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} 126 | dev: true 127 | 128 | /@types/chai@4.3.6: 129 | resolution: {integrity: sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==} 130 | dev: true 131 | 132 | /@types/chrome@0.0.243: 133 | resolution: {integrity: sha512-4PHv0kxxxpZFHWPBiJJ9TWH8kbx0567j1b2djnhpJjpiSGNI7UKkz7dSEECBtQ0B3N5nQTMwSB/5IopkWGAbEA==} 134 | dependencies: 135 | '@types/filesystem': 0.0.32 136 | '@types/har-format': 1.2.12 137 | dev: true 138 | 139 | /@types/filesystem@0.0.32: 140 | resolution: {integrity: sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==} 141 | dependencies: 142 | '@types/filewriter': 0.0.29 143 | dev: true 144 | 145 | /@types/filewriter@0.0.29: 146 | resolution: {integrity: sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==} 147 | dev: true 148 | 149 | /@types/har-format@1.2.12: 150 | resolution: {integrity: sha512-P20p/YBrqUBmzD6KhIQ8EiY4/RRzlekL4eCvfQnulFPfjmiGxKIoyCeI7qam5I7oKH3P8EU4ptEi0EfyGoLysw==} 151 | dev: true 152 | 153 | /@types/mocha@10.0.1: 154 | resolution: {integrity: sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==} 155 | dev: true 156 | 157 | /@types/node@20.5.0: 158 | resolution: {integrity: sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==} 159 | dev: true 160 | 161 | /@types/yauzl@2.10.0: 162 | resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} 163 | requiresBuild: true 164 | dependencies: 165 | '@types/node': 20.5.0 166 | dev: true 167 | optional: true 168 | 169 | /acorn-walk@8.2.0: 170 | resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} 171 | engines: {node: '>=0.4.0'} 172 | dev: true 173 | 174 | /acorn@8.10.0: 175 | resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} 176 | engines: {node: '>=0.4.0'} 177 | hasBin: true 178 | dev: true 179 | 180 | /agent-base@7.1.0: 181 | resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} 182 | engines: {node: '>= 14'} 183 | dependencies: 184 | debug: 4.3.4(supports-color@8.1.1) 185 | transitivePeerDependencies: 186 | - supports-color 187 | dev: true 188 | 189 | /ansi-colors@4.1.1: 190 | resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} 191 | engines: {node: '>=6'} 192 | dev: true 193 | 194 | /ansi-regex@5.0.1: 195 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 196 | engines: {node: '>=8'} 197 | dev: true 198 | 199 | /ansi-styles@3.2.1: 200 | resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 201 | engines: {node: '>=4'} 202 | dependencies: 203 | color-convert: 1.9.3 204 | dev: true 205 | 206 | /ansi-styles@4.3.0: 207 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 208 | engines: {node: '>=8'} 209 | dependencies: 210 | color-convert: 2.0.1 211 | dev: true 212 | 213 | /anymatch@3.1.3: 214 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 215 | engines: {node: '>= 8'} 216 | dependencies: 217 | normalize-path: 3.0.0 218 | picomatch: 2.3.1 219 | dev: true 220 | 221 | /arg@4.1.3: 222 | resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} 223 | dev: true 224 | 225 | /argparse@2.0.1: 226 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 227 | dev: true 228 | 229 | /assertion-error@1.1.0: 230 | resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} 231 | dev: true 232 | 233 | /ast-types@0.13.4: 234 | resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} 235 | engines: {node: '>=4'} 236 | dependencies: 237 | tslib: 2.6.2 238 | dev: true 239 | 240 | /b4a@1.6.4: 241 | resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} 242 | dev: true 243 | 244 | /balanced-match@1.0.2: 245 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 246 | dev: true 247 | 248 | /base64-js@1.5.1: 249 | resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} 250 | dev: true 251 | 252 | /basic-ftp@5.0.3: 253 | resolution: {integrity: sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==} 254 | engines: {node: '>=10.0.0'} 255 | dev: true 256 | 257 | /binary-extensions@2.2.0: 258 | resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} 259 | engines: {node: '>=8'} 260 | dev: true 261 | 262 | /brace-expansion@1.1.11: 263 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 264 | dependencies: 265 | balanced-match: 1.0.2 266 | concat-map: 0.0.1 267 | dev: true 268 | 269 | /brace-expansion@2.0.1: 270 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 271 | dependencies: 272 | balanced-match: 1.0.2 273 | dev: true 274 | 275 | /braces@3.0.3: 276 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 277 | engines: {node: '>=8'} 278 | dependencies: 279 | fill-range: 7.1.1 280 | dev: true 281 | 282 | /browser-stdout@1.3.1: 283 | resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} 284 | dev: true 285 | 286 | /buffer-crc32@0.2.13: 287 | resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} 288 | dev: true 289 | 290 | /buffer-from@1.1.2: 291 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 292 | dev: true 293 | 294 | /buffer@5.7.1: 295 | resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} 296 | dependencies: 297 | base64-js: 1.5.1 298 | ieee754: 1.2.1 299 | dev: true 300 | 301 | /callsites@3.1.0: 302 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 303 | engines: {node: '>=6'} 304 | dev: true 305 | 306 | /camelcase@6.3.0: 307 | resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} 308 | engines: {node: '>=10'} 309 | dev: true 310 | 311 | /chai@4.3.8: 312 | resolution: {integrity: sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==} 313 | engines: {node: '>=4'} 314 | dependencies: 315 | assertion-error: 1.1.0 316 | check-error: 1.0.2 317 | deep-eql: 4.1.3 318 | get-func-name: 2.0.0 319 | loupe: 2.3.6 320 | pathval: 1.1.1 321 | type-detect: 4.0.8 322 | dev: true 323 | 324 | /chalk@2.4.2: 325 | resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 326 | engines: {node: '>=4'} 327 | dependencies: 328 | ansi-styles: 3.2.1 329 | escape-string-regexp: 1.0.5 330 | supports-color: 5.5.0 331 | dev: true 332 | 333 | /chalk@4.1.2: 334 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 335 | engines: {node: '>=10'} 336 | dependencies: 337 | ansi-styles: 4.3.0 338 | supports-color: 7.2.0 339 | dev: true 340 | 341 | /check-error@1.0.2: 342 | resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} 343 | dev: true 344 | 345 | /chokidar@3.5.3: 346 | resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} 347 | engines: {node: '>= 8.10.0'} 348 | dependencies: 349 | anymatch: 3.1.3 350 | braces: 3.0.3 351 | glob-parent: 5.1.2 352 | is-binary-path: 2.1.0 353 | is-glob: 4.0.3 354 | normalize-path: 3.0.0 355 | readdirp: 3.6.0 356 | optionalDependencies: 357 | fsevents: 2.3.3 358 | dev: true 359 | 360 | /chromium-bidi@0.4.22(devtools-protocol@0.0.1159816): 361 | resolution: {integrity: sha512-wR7Y9Ioez+cNXT4ZP7VNM1HRTljpNnMSLw4/RnwhhZUP4yCU7kIQND00YiktuHekch68jklGPK1q9Jkb29+fQg==} 362 | peerDependencies: 363 | devtools-protocol: '*' 364 | dependencies: 365 | devtools-protocol: 0.0.1159816 366 | mitt: 3.0.1 367 | dev: true 368 | 369 | /cliui@7.0.4: 370 | resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} 371 | dependencies: 372 | string-width: 4.2.3 373 | strip-ansi: 6.0.1 374 | wrap-ansi: 7.0.0 375 | dev: true 376 | 377 | /cliui@8.0.1: 378 | resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} 379 | engines: {node: '>=12'} 380 | dependencies: 381 | string-width: 4.2.3 382 | strip-ansi: 6.0.1 383 | wrap-ansi: 7.0.0 384 | dev: true 385 | 386 | /color-convert@1.9.3: 387 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 388 | dependencies: 389 | color-name: 1.1.3 390 | dev: true 391 | 392 | /color-convert@2.0.1: 393 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 394 | engines: {node: '>=7.0.0'} 395 | dependencies: 396 | color-name: 1.1.4 397 | dev: true 398 | 399 | /color-name@1.1.3: 400 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 401 | dev: true 402 | 403 | /color-name@1.1.4: 404 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 405 | dev: true 406 | 407 | /concat-map@0.0.1: 408 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 409 | dev: true 410 | 411 | /cosmiconfig@8.2.0: 412 | resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} 413 | engines: {node: '>=14'} 414 | dependencies: 415 | import-fresh: 3.3.0 416 | js-yaml: 4.1.0 417 | parse-json: 5.2.0 418 | path-type: 4.0.0 419 | dev: true 420 | 421 | /create-require@1.1.1: 422 | resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} 423 | dev: true 424 | 425 | /cross-fetch@4.0.0: 426 | resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} 427 | dependencies: 428 | node-fetch: 2.7.0 429 | transitivePeerDependencies: 430 | - encoding 431 | dev: true 432 | 433 | /data-uri-to-buffer@5.0.1: 434 | resolution: {integrity: sha512-a9l6T1qqDogvvnw0nKlfZzqsyikEBZBClF39V3TFoKhDtGBqHu2HkuomJc02j5zft8zrUaXEuoicLeW54RkzPg==} 435 | engines: {node: '>= 14'} 436 | dev: true 437 | 438 | /debug@4.3.4(supports-color@8.1.1): 439 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 440 | engines: {node: '>=6.0'} 441 | peerDependencies: 442 | supports-color: '*' 443 | peerDependenciesMeta: 444 | supports-color: 445 | optional: true 446 | dependencies: 447 | ms: 2.1.2 448 | supports-color: 8.1.1 449 | dev: true 450 | 451 | /decamelize@4.0.0: 452 | resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} 453 | engines: {node: '>=10'} 454 | dev: true 455 | 456 | /deep-eql@4.1.3: 457 | resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} 458 | engines: {node: '>=6'} 459 | dependencies: 460 | type-detect: 4.0.8 461 | dev: true 462 | 463 | /degenerator@5.0.1: 464 | resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} 465 | engines: {node: '>= 14'} 466 | dependencies: 467 | ast-types: 0.13.4 468 | escodegen: 2.1.0 469 | esprima: 4.0.1 470 | dev: true 471 | 472 | /devtools-protocol@0.0.1159816: 473 | resolution: {integrity: sha512-2cZlHxC5IlgkIWe2pSDmCrDiTzbSJWywjbDDnupOImEBcG31CQgBLV8wWE+5t+C4rimcjHsbzy7CBzf9oFjboA==} 474 | dev: true 475 | 476 | /diff@4.0.2: 477 | resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} 478 | engines: {node: '>=0.3.1'} 479 | dev: true 480 | 481 | /diff@5.0.0: 482 | resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} 483 | engines: {node: '>=0.3.1'} 484 | dev: true 485 | 486 | /dom-serializer@1.4.1: 487 | resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} 488 | dependencies: 489 | domelementtype: 2.3.0 490 | domhandler: 4.3.1 491 | entities: 2.2.0 492 | dev: true 493 | 494 | /domelementtype@2.3.0: 495 | resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} 496 | dev: true 497 | 498 | /domhandler@4.3.1: 499 | resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} 500 | engines: {node: '>= 4'} 501 | dependencies: 502 | domelementtype: 2.3.0 503 | dev: true 504 | 505 | /domutils@2.8.0: 506 | resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} 507 | dependencies: 508 | dom-serializer: 1.4.1 509 | domelementtype: 2.3.0 510 | domhandler: 4.3.1 511 | dev: true 512 | 513 | /emoji-regex@8.0.0: 514 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 515 | dev: true 516 | 517 | /end-of-stream@1.4.4: 518 | resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} 519 | dependencies: 520 | once: 1.4.0 521 | dev: true 522 | 523 | /entities@2.2.0: 524 | resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} 525 | dev: true 526 | 527 | /entities@3.0.1: 528 | resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} 529 | engines: {node: '>=0.12'} 530 | dev: true 531 | 532 | /error-ex@1.3.2: 533 | resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} 534 | dependencies: 535 | is-arrayish: 0.2.1 536 | dev: true 537 | 538 | /escalade@3.1.1: 539 | resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} 540 | engines: {node: '>=6'} 541 | dev: true 542 | 543 | /escape-string-regexp@1.0.5: 544 | resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 545 | engines: {node: '>=0.8.0'} 546 | dev: true 547 | 548 | /escape-string-regexp@4.0.0: 549 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 550 | engines: {node: '>=10'} 551 | dev: true 552 | 553 | /escodegen@2.1.0: 554 | resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} 555 | engines: {node: '>=6.0'} 556 | hasBin: true 557 | dependencies: 558 | esprima: 4.0.1 559 | estraverse: 5.3.0 560 | esutils: 2.0.3 561 | optionalDependencies: 562 | source-map: 0.6.1 563 | dev: true 564 | 565 | /esprima@4.0.1: 566 | resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} 567 | engines: {node: '>=4'} 568 | hasBin: true 569 | dev: true 570 | 571 | /estraverse@5.3.0: 572 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 573 | engines: {node: '>=4.0'} 574 | dev: true 575 | 576 | /esutils@2.0.3: 577 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 578 | engines: {node: '>=0.10.0'} 579 | dev: true 580 | 581 | /extract-zip@2.0.1: 582 | resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} 583 | engines: {node: '>= 10.17.0'} 584 | hasBin: true 585 | dependencies: 586 | debug: 4.3.4(supports-color@8.1.1) 587 | get-stream: 5.2.0 588 | yauzl: 2.10.0 589 | optionalDependencies: 590 | '@types/yauzl': 2.10.0 591 | transitivePeerDependencies: 592 | - supports-color 593 | dev: true 594 | 595 | /fast-fifo@1.3.2: 596 | resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} 597 | dev: true 598 | 599 | /fd-slicer@1.1.0: 600 | resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} 601 | dependencies: 602 | pend: 1.2.0 603 | dev: true 604 | 605 | /fill-range@7.1.1: 606 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 607 | engines: {node: '>=8'} 608 | dependencies: 609 | to-regex-range: 5.0.1 610 | dev: true 611 | 612 | /find-up@5.0.0: 613 | resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 614 | engines: {node: '>=10'} 615 | dependencies: 616 | locate-path: 6.0.0 617 | path-exists: 4.0.0 618 | dev: true 619 | 620 | /flat@5.0.2: 621 | resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} 622 | hasBin: true 623 | dev: true 624 | 625 | /fs-extra@8.1.0: 626 | resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} 627 | engines: {node: '>=6 <7 || >=8'} 628 | dependencies: 629 | graceful-fs: 4.2.11 630 | jsonfile: 4.0.0 631 | universalify: 0.1.2 632 | dev: true 633 | 634 | /fs.realpath@1.0.0: 635 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 636 | dev: true 637 | 638 | /fsevents@2.3.3: 639 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 640 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 641 | os: [darwin] 642 | requiresBuild: true 643 | dev: true 644 | optional: true 645 | 646 | /get-caller-file@2.0.5: 647 | resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 648 | engines: {node: 6.* || 8.* || >= 10.*} 649 | dev: true 650 | 651 | /get-func-name@2.0.0: 652 | resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} 653 | dev: true 654 | 655 | /get-stream@5.2.0: 656 | resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} 657 | engines: {node: '>=8'} 658 | dependencies: 659 | pump: 3.0.0 660 | dev: true 661 | 662 | /get-uri@6.0.1: 663 | resolution: {integrity: sha512-7ZqONUVqaabogsYNWlYj0t3YZaL6dhuEueZXGF+/YVmf6dHmaFg8/6psJKqhx9QykIDKzpGcy2cn4oV4YC7V/Q==} 664 | engines: {node: '>= 14'} 665 | dependencies: 666 | basic-ftp: 5.0.3 667 | data-uri-to-buffer: 5.0.1 668 | debug: 4.3.4(supports-color@8.1.1) 669 | fs-extra: 8.1.0 670 | transitivePeerDependencies: 671 | - supports-color 672 | dev: true 673 | 674 | /glob-parent@5.1.2: 675 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 676 | engines: {node: '>= 6'} 677 | dependencies: 678 | is-glob: 4.0.3 679 | dev: true 680 | 681 | /glob@7.2.0: 682 | resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} 683 | dependencies: 684 | fs.realpath: 1.0.0 685 | inflight: 1.0.6 686 | inherits: 2.0.4 687 | minimatch: 3.1.2 688 | once: 1.4.0 689 | path-is-absolute: 1.0.1 690 | dev: true 691 | 692 | /graceful-fs@4.2.11: 693 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 694 | dev: true 695 | 696 | /has-flag@3.0.0: 697 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 698 | engines: {node: '>=4'} 699 | dev: true 700 | 701 | /has-flag@4.0.0: 702 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 703 | engines: {node: '>=8'} 704 | dev: true 705 | 706 | /he@1.2.0: 707 | resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} 708 | hasBin: true 709 | dev: true 710 | 711 | /htmlnano@2.0.4: 712 | resolution: {integrity: sha512-WGCkyGFwjKW1GeCBsPYacMvaMnZtFJ0zIRnC2NCddkA+IOEhTqskXrS7lep+3yYZw/nQ3dW1UAX4yA/GJyR8BA==} 713 | peerDependencies: 714 | cssnano: ^6.0.0 715 | postcss: ^8.3.11 716 | purgecss: ^5.0.0 717 | relateurl: ^0.2.7 718 | srcset: 4.0.0 719 | svgo: ^3.0.2 720 | terser: ^5.10.0 721 | uncss: ^0.17.3 722 | peerDependenciesMeta: 723 | cssnano: 724 | optional: true 725 | postcss: 726 | optional: true 727 | purgecss: 728 | optional: true 729 | relateurl: 730 | optional: true 731 | srcset: 732 | optional: true 733 | svgo: 734 | optional: true 735 | terser: 736 | optional: true 737 | uncss: 738 | optional: true 739 | dependencies: 740 | cosmiconfig: 8.2.0 741 | posthtml: 0.16.6 742 | timsort: 0.3.0 743 | dev: true 744 | 745 | /htmlparser2@7.2.0: 746 | resolution: {integrity: sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==} 747 | dependencies: 748 | domelementtype: 2.3.0 749 | domhandler: 4.3.1 750 | domutils: 2.8.0 751 | entities: 3.0.1 752 | dev: true 753 | 754 | /http-proxy-agent@7.0.0: 755 | resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} 756 | engines: {node: '>= 14'} 757 | dependencies: 758 | agent-base: 7.1.0 759 | debug: 4.3.4(supports-color@8.1.1) 760 | transitivePeerDependencies: 761 | - supports-color 762 | dev: true 763 | 764 | /https-proxy-agent@7.0.2: 765 | resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==} 766 | engines: {node: '>= 14'} 767 | dependencies: 768 | agent-base: 7.1.0 769 | debug: 4.3.4(supports-color@8.1.1) 770 | transitivePeerDependencies: 771 | - supports-color 772 | dev: true 773 | 774 | /ieee754@1.2.1: 775 | resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} 776 | dev: true 777 | 778 | /import-fresh@3.3.0: 779 | resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} 780 | engines: {node: '>=6'} 781 | dependencies: 782 | parent-module: 1.0.1 783 | resolve-from: 4.0.0 784 | dev: true 785 | 786 | /inflight@1.0.6: 787 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 788 | dependencies: 789 | once: 1.4.0 790 | wrappy: 1.0.2 791 | dev: true 792 | 793 | /inherits@2.0.4: 794 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 795 | dev: true 796 | 797 | /ip@1.1.8: 798 | resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} 799 | dev: true 800 | 801 | /ip@2.0.0: 802 | resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} 803 | dev: true 804 | 805 | /is-arrayish@0.2.1: 806 | resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} 807 | dev: true 808 | 809 | /is-binary-path@2.1.0: 810 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 811 | engines: {node: '>=8'} 812 | dependencies: 813 | binary-extensions: 2.2.0 814 | dev: true 815 | 816 | /is-extglob@2.1.1: 817 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 818 | engines: {node: '>=0.10.0'} 819 | dev: true 820 | 821 | /is-fullwidth-code-point@3.0.0: 822 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 823 | engines: {node: '>=8'} 824 | dev: true 825 | 826 | /is-glob@4.0.3: 827 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 828 | engines: {node: '>=0.10.0'} 829 | dependencies: 830 | is-extglob: 2.1.1 831 | dev: true 832 | 833 | /is-json@2.0.1: 834 | resolution: {integrity: sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==} 835 | dev: true 836 | 837 | /is-number@7.0.0: 838 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 839 | engines: {node: '>=0.12.0'} 840 | dev: true 841 | 842 | /is-plain-obj@2.1.0: 843 | resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} 844 | engines: {node: '>=8'} 845 | dev: true 846 | 847 | /is-unicode-supported@0.1.0: 848 | resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} 849 | engines: {node: '>=10'} 850 | dev: true 851 | 852 | /js-tokens@4.0.0: 853 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 854 | dev: true 855 | 856 | /js-yaml@4.1.0: 857 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 858 | hasBin: true 859 | dependencies: 860 | argparse: 2.0.1 861 | dev: true 862 | 863 | /json-parse-even-better-errors@2.3.1: 864 | resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} 865 | dev: true 866 | 867 | /jsonfile@4.0.0: 868 | resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} 869 | optionalDependencies: 870 | graceful-fs: 4.2.11 871 | dev: true 872 | 873 | /lines-and-columns@1.2.4: 874 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 875 | dev: true 876 | 877 | /locate-path@6.0.0: 878 | resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 879 | engines: {node: '>=10'} 880 | dependencies: 881 | p-locate: 5.0.0 882 | dev: true 883 | 884 | /log-symbols@4.1.0: 885 | resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} 886 | engines: {node: '>=10'} 887 | dependencies: 888 | chalk: 4.1.2 889 | is-unicode-supported: 0.1.0 890 | dev: true 891 | 892 | /loupe@2.3.6: 893 | resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} 894 | dependencies: 895 | get-func-name: 2.0.0 896 | dev: true 897 | 898 | /lru-cache@7.18.3: 899 | resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} 900 | engines: {node: '>=12'} 901 | dev: true 902 | 903 | /make-error@1.3.6: 904 | resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} 905 | dev: true 906 | 907 | /minimatch@3.1.2: 908 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 909 | dependencies: 910 | brace-expansion: 1.1.11 911 | dev: true 912 | 913 | /minimatch@5.0.1: 914 | resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==} 915 | engines: {node: '>=10'} 916 | dependencies: 917 | brace-expansion: 2.0.1 918 | dev: true 919 | 920 | /mitt@3.0.1: 921 | resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} 922 | dev: true 923 | 924 | /mkdirp-classic@0.5.3: 925 | resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} 926 | dev: true 927 | 928 | /mocha@10.2.0: 929 | resolution: {integrity: sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==} 930 | engines: {node: '>= 14.0.0'} 931 | hasBin: true 932 | dependencies: 933 | ansi-colors: 4.1.1 934 | browser-stdout: 1.3.1 935 | chokidar: 3.5.3 936 | debug: 4.3.4(supports-color@8.1.1) 937 | diff: 5.0.0 938 | escape-string-regexp: 4.0.0 939 | find-up: 5.0.0 940 | glob: 7.2.0 941 | he: 1.2.0 942 | js-yaml: 4.1.0 943 | log-symbols: 4.1.0 944 | minimatch: 5.0.1 945 | ms: 2.1.3 946 | nanoid: 3.3.3 947 | serialize-javascript: 6.0.0 948 | strip-json-comments: 3.1.1 949 | supports-color: 8.1.1 950 | workerpool: 6.2.1 951 | yargs: 16.2.0 952 | yargs-parser: 20.2.4 953 | yargs-unparser: 2.0.0 954 | dev: true 955 | 956 | /ms@2.1.2: 957 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 958 | dev: true 959 | 960 | /ms@2.1.3: 961 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 962 | dev: true 963 | 964 | /nanoid@3.3.3: 965 | resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} 966 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 967 | hasBin: true 968 | dev: true 969 | 970 | /netmask@2.0.2: 971 | resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} 972 | engines: {node: '>= 0.4.0'} 973 | dev: true 974 | 975 | /node-fetch@2.7.0: 976 | resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} 977 | engines: {node: 4.x || >=6.0.0} 978 | peerDependencies: 979 | encoding: ^0.1.0 980 | peerDependenciesMeta: 981 | encoding: 982 | optional: true 983 | dependencies: 984 | whatwg-url: 5.0.0 985 | dev: true 986 | 987 | /normalize-path@3.0.0: 988 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 989 | engines: {node: '>=0.10.0'} 990 | dev: true 991 | 992 | /once@1.4.0: 993 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 994 | dependencies: 995 | wrappy: 1.0.2 996 | dev: true 997 | 998 | /p-limit@3.1.0: 999 | resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 1000 | engines: {node: '>=10'} 1001 | dependencies: 1002 | yocto-queue: 0.1.0 1003 | dev: true 1004 | 1005 | /p-locate@5.0.0: 1006 | resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 1007 | engines: {node: '>=10'} 1008 | dependencies: 1009 | p-limit: 3.1.0 1010 | dev: true 1011 | 1012 | /pac-proxy-agent@7.0.1: 1013 | resolution: {integrity: sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==} 1014 | engines: {node: '>= 14'} 1015 | dependencies: 1016 | '@tootallnate/quickjs-emscripten': 0.23.0 1017 | agent-base: 7.1.0 1018 | debug: 4.3.4(supports-color@8.1.1) 1019 | get-uri: 6.0.1 1020 | http-proxy-agent: 7.0.0 1021 | https-proxy-agent: 7.0.2 1022 | pac-resolver: 7.0.0 1023 | socks-proxy-agent: 8.0.2 1024 | transitivePeerDependencies: 1025 | - supports-color 1026 | dev: true 1027 | 1028 | /pac-resolver@7.0.0: 1029 | resolution: {integrity: sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg==} 1030 | engines: {node: '>= 14'} 1031 | dependencies: 1032 | degenerator: 5.0.1 1033 | ip: 1.1.8 1034 | netmask: 2.0.2 1035 | dev: true 1036 | 1037 | /parent-module@1.0.1: 1038 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 1039 | engines: {node: '>=6'} 1040 | dependencies: 1041 | callsites: 3.1.0 1042 | dev: true 1043 | 1044 | /parse-json@5.2.0: 1045 | resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} 1046 | engines: {node: '>=8'} 1047 | dependencies: 1048 | '@babel/code-frame': 7.22.13 1049 | error-ex: 1.3.2 1050 | json-parse-even-better-errors: 2.3.1 1051 | lines-and-columns: 1.2.4 1052 | dev: true 1053 | 1054 | /path-exists@4.0.0: 1055 | resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 1056 | engines: {node: '>=8'} 1057 | dev: true 1058 | 1059 | /path-is-absolute@1.0.1: 1060 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 1061 | engines: {node: '>=0.10.0'} 1062 | dev: true 1063 | 1064 | /path-type@4.0.0: 1065 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 1066 | engines: {node: '>=8'} 1067 | dev: true 1068 | 1069 | /pathval@1.1.1: 1070 | resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} 1071 | dev: true 1072 | 1073 | /pend@1.2.0: 1074 | resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} 1075 | dev: true 1076 | 1077 | /picomatch@2.3.1: 1078 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 1079 | engines: {node: '>=8.6'} 1080 | dev: true 1081 | 1082 | /posthtml-parser@0.11.0: 1083 | resolution: {integrity: sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==} 1084 | engines: {node: '>=12'} 1085 | dependencies: 1086 | htmlparser2: 7.2.0 1087 | dev: true 1088 | 1089 | /posthtml-render@3.0.0: 1090 | resolution: {integrity: sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==} 1091 | engines: {node: '>=12'} 1092 | dependencies: 1093 | is-json: 2.0.1 1094 | dev: true 1095 | 1096 | /posthtml@0.16.6: 1097 | resolution: {integrity: sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==} 1098 | engines: {node: '>=12.0.0'} 1099 | dependencies: 1100 | posthtml-parser: 0.11.0 1101 | posthtml-render: 3.0.0 1102 | dev: true 1103 | 1104 | /prettier@3.0.3: 1105 | resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==} 1106 | engines: {node: '>=14'} 1107 | hasBin: true 1108 | dev: true 1109 | 1110 | /progress@2.0.3: 1111 | resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} 1112 | engines: {node: '>=0.4.0'} 1113 | dev: true 1114 | 1115 | /proxy-agent@6.3.0: 1116 | resolution: {integrity: sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==} 1117 | engines: {node: '>= 14'} 1118 | dependencies: 1119 | agent-base: 7.1.0 1120 | debug: 4.3.4(supports-color@8.1.1) 1121 | http-proxy-agent: 7.0.0 1122 | https-proxy-agent: 7.0.2 1123 | lru-cache: 7.18.3 1124 | pac-proxy-agent: 7.0.1 1125 | proxy-from-env: 1.1.0 1126 | socks-proxy-agent: 8.0.2 1127 | transitivePeerDependencies: 1128 | - supports-color 1129 | dev: true 1130 | 1131 | /proxy-from-env@1.1.0: 1132 | resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} 1133 | dev: true 1134 | 1135 | /pump@3.0.0: 1136 | resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} 1137 | dependencies: 1138 | end-of-stream: 1.4.4 1139 | once: 1.4.0 1140 | dev: true 1141 | 1142 | /puppeteer-core@21.1.1: 1143 | resolution: {integrity: sha512-Tlcajcf44zwfa9Sbwv3T8BtaNMJ69wtpHIxwl2NOBTyTK3D1wppQovXTjfw0TDOm3a16eCfQ+5BMi3vRQ4kuAQ==} 1144 | engines: {node: '>=16.3.0'} 1145 | dependencies: 1146 | '@puppeteer/browsers': 1.7.0 1147 | chromium-bidi: 0.4.22(devtools-protocol@0.0.1159816) 1148 | cross-fetch: 4.0.0 1149 | debug: 4.3.4(supports-color@8.1.1) 1150 | devtools-protocol: 0.0.1159816 1151 | ws: 8.13.0 1152 | transitivePeerDependencies: 1153 | - bufferutil 1154 | - encoding 1155 | - supports-color 1156 | - utf-8-validate 1157 | dev: true 1158 | 1159 | /puppeteer@21.1.1: 1160 | resolution: {integrity: sha512-2TLntjGA4qLrI9/8N0UK/5OoZJ2Ue7QgphN2SD+RsaHiha12AEiVyMGsB+i6LY1IoPAtEgYIjblQ7lw3kWDNRw==} 1161 | engines: {node: '>=16.3.0'} 1162 | requiresBuild: true 1163 | dependencies: 1164 | '@puppeteer/browsers': 1.7.0 1165 | cosmiconfig: 8.2.0 1166 | puppeteer-core: 21.1.1 1167 | transitivePeerDependencies: 1168 | - bufferutil 1169 | - encoding 1170 | - supports-color 1171 | - utf-8-validate 1172 | dev: true 1173 | 1174 | /queue-tick@1.0.1: 1175 | resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} 1176 | dev: true 1177 | 1178 | /randombytes@2.1.0: 1179 | resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} 1180 | dependencies: 1181 | safe-buffer: 5.2.1 1182 | dev: true 1183 | 1184 | /readdirp@3.6.0: 1185 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 1186 | engines: {node: '>=8.10.0'} 1187 | dependencies: 1188 | picomatch: 2.3.1 1189 | dev: true 1190 | 1191 | /require-directory@2.1.1: 1192 | resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} 1193 | engines: {node: '>=0.10.0'} 1194 | dev: true 1195 | 1196 | /resolve-from@4.0.0: 1197 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 1198 | engines: {node: '>=4'} 1199 | dev: true 1200 | 1201 | /safe-buffer@5.2.1: 1202 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 1203 | dev: true 1204 | 1205 | /serialize-javascript@6.0.0: 1206 | resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} 1207 | dependencies: 1208 | randombytes: 2.1.0 1209 | dev: true 1210 | 1211 | /smart-buffer@4.2.0: 1212 | resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} 1213 | engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} 1214 | dev: true 1215 | 1216 | /socks-proxy-agent@8.0.2: 1217 | resolution: {integrity: sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==} 1218 | engines: {node: '>= 14'} 1219 | dependencies: 1220 | agent-base: 7.1.0 1221 | debug: 4.3.4(supports-color@8.1.1) 1222 | socks: 2.7.1 1223 | transitivePeerDependencies: 1224 | - supports-color 1225 | dev: true 1226 | 1227 | /socks@2.7.1: 1228 | resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} 1229 | engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} 1230 | dependencies: 1231 | ip: 2.0.0 1232 | smart-buffer: 4.2.0 1233 | dev: true 1234 | 1235 | /source-map-support@0.5.21: 1236 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 1237 | dependencies: 1238 | buffer-from: 1.1.2 1239 | source-map: 0.6.1 1240 | dev: true 1241 | 1242 | /source-map@0.6.1: 1243 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 1244 | engines: {node: '>=0.10.0'} 1245 | dev: true 1246 | 1247 | /streamx@2.15.1: 1248 | resolution: {integrity: sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==} 1249 | dependencies: 1250 | fast-fifo: 1.3.2 1251 | queue-tick: 1.0.1 1252 | dev: true 1253 | 1254 | /string-width@4.2.3: 1255 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 1256 | engines: {node: '>=8'} 1257 | dependencies: 1258 | emoji-regex: 8.0.0 1259 | is-fullwidth-code-point: 3.0.0 1260 | strip-ansi: 6.0.1 1261 | dev: true 1262 | 1263 | /strip-ansi@6.0.1: 1264 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 1265 | engines: {node: '>=8'} 1266 | dependencies: 1267 | ansi-regex: 5.0.1 1268 | dev: true 1269 | 1270 | /strip-json-comments@3.1.1: 1271 | resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 1272 | engines: {node: '>=8'} 1273 | dev: true 1274 | 1275 | /supports-color@5.5.0: 1276 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 1277 | engines: {node: '>=4'} 1278 | dependencies: 1279 | has-flag: 3.0.0 1280 | dev: true 1281 | 1282 | /supports-color@7.2.0: 1283 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1284 | engines: {node: '>=8'} 1285 | dependencies: 1286 | has-flag: 4.0.0 1287 | dev: true 1288 | 1289 | /supports-color@8.1.1: 1290 | resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} 1291 | engines: {node: '>=10'} 1292 | dependencies: 1293 | has-flag: 4.0.0 1294 | dev: true 1295 | 1296 | /tar-fs@3.0.4: 1297 | resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} 1298 | dependencies: 1299 | mkdirp-classic: 0.5.3 1300 | pump: 3.0.0 1301 | tar-stream: 3.1.6 1302 | dev: true 1303 | 1304 | /tar-stream@3.1.6: 1305 | resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==} 1306 | dependencies: 1307 | b4a: 1.6.4 1308 | fast-fifo: 1.3.2 1309 | streamx: 2.15.1 1310 | dev: true 1311 | 1312 | /through@2.3.8: 1313 | resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} 1314 | dev: true 1315 | 1316 | /timsort@0.3.0: 1317 | resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==} 1318 | dev: true 1319 | 1320 | /to-regex-range@5.0.1: 1321 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1322 | engines: {node: '>=8.0'} 1323 | dependencies: 1324 | is-number: 7.0.0 1325 | dev: true 1326 | 1327 | /tr46@0.0.3: 1328 | resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} 1329 | dev: true 1330 | 1331 | /ts-node@10.9.1(@types/node@20.5.0)(typescript@5.2.2): 1332 | resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} 1333 | hasBin: true 1334 | peerDependencies: 1335 | '@swc/core': '>=1.2.50' 1336 | '@swc/wasm': '>=1.2.50' 1337 | '@types/node': '*' 1338 | typescript: '>=2.7' 1339 | peerDependenciesMeta: 1340 | '@swc/core': 1341 | optional: true 1342 | '@swc/wasm': 1343 | optional: true 1344 | dependencies: 1345 | '@cspotcode/source-map-support': 0.8.1 1346 | '@tsconfig/node10': 1.0.9 1347 | '@tsconfig/node12': 1.0.11 1348 | '@tsconfig/node14': 1.0.3 1349 | '@tsconfig/node16': 1.0.4 1350 | '@types/node': 20.5.0 1351 | acorn: 8.10.0 1352 | acorn-walk: 8.2.0 1353 | arg: 4.1.3 1354 | create-require: 1.1.1 1355 | diff: 4.0.2 1356 | make-error: 1.3.6 1357 | typescript: 5.2.2 1358 | v8-compile-cache-lib: 3.0.1 1359 | yn: 3.1.1 1360 | dev: true 1361 | 1362 | /tslib@2.6.2: 1363 | resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} 1364 | dev: true 1365 | 1366 | /type-detect@4.0.8: 1367 | resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} 1368 | engines: {node: '>=4'} 1369 | dev: true 1370 | 1371 | /typescript@5.2.2: 1372 | resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} 1373 | engines: {node: '>=14.17'} 1374 | hasBin: true 1375 | dev: true 1376 | 1377 | /unbzip2-stream@1.4.3: 1378 | resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} 1379 | dependencies: 1380 | buffer: 5.7.1 1381 | through: 2.3.8 1382 | dev: true 1383 | 1384 | /universalify@0.1.2: 1385 | resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} 1386 | engines: {node: '>= 4.0.0'} 1387 | dev: true 1388 | 1389 | /v8-compile-cache-lib@3.0.1: 1390 | resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} 1391 | dev: true 1392 | 1393 | /webidl-conversions@3.0.1: 1394 | resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} 1395 | dev: true 1396 | 1397 | /whatwg-url@5.0.0: 1398 | resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} 1399 | dependencies: 1400 | tr46: 0.0.3 1401 | webidl-conversions: 3.0.1 1402 | dev: true 1403 | 1404 | /workerpool@6.2.1: 1405 | resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==} 1406 | dev: true 1407 | 1408 | /wrap-ansi@7.0.0: 1409 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 1410 | engines: {node: '>=10'} 1411 | dependencies: 1412 | ansi-styles: 4.3.0 1413 | string-width: 4.2.3 1414 | strip-ansi: 6.0.1 1415 | dev: true 1416 | 1417 | /wrappy@1.0.2: 1418 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 1419 | dev: true 1420 | 1421 | /ws@8.13.0: 1422 | resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} 1423 | engines: {node: '>=10.0.0'} 1424 | peerDependencies: 1425 | bufferutil: ^4.0.1 1426 | utf-8-validate: '>=5.0.2' 1427 | peerDependenciesMeta: 1428 | bufferutil: 1429 | optional: true 1430 | utf-8-validate: 1431 | optional: true 1432 | dev: true 1433 | 1434 | /y18n@5.0.8: 1435 | resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} 1436 | engines: {node: '>=10'} 1437 | dev: true 1438 | 1439 | /yargs-parser@20.2.4: 1440 | resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} 1441 | engines: {node: '>=10'} 1442 | dev: true 1443 | 1444 | /yargs-parser@21.1.1: 1445 | resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} 1446 | engines: {node: '>=12'} 1447 | dev: true 1448 | 1449 | /yargs-unparser@2.0.0: 1450 | resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} 1451 | engines: {node: '>=10'} 1452 | dependencies: 1453 | camelcase: 6.3.0 1454 | decamelize: 4.0.0 1455 | flat: 5.0.2 1456 | is-plain-obj: 2.1.0 1457 | dev: true 1458 | 1459 | /yargs@16.2.0: 1460 | resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} 1461 | engines: {node: '>=10'} 1462 | dependencies: 1463 | cliui: 7.0.4 1464 | escalade: 3.1.1 1465 | get-caller-file: 2.0.5 1466 | require-directory: 2.1.1 1467 | string-width: 4.2.3 1468 | y18n: 5.0.8 1469 | yargs-parser: 20.2.4 1470 | dev: true 1471 | 1472 | /yargs@17.7.1: 1473 | resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==} 1474 | engines: {node: '>=12'} 1475 | dependencies: 1476 | cliui: 8.0.1 1477 | escalade: 3.1.1 1478 | get-caller-file: 2.0.5 1479 | require-directory: 2.1.1 1480 | string-width: 4.2.3 1481 | y18n: 5.0.8 1482 | yargs-parser: 21.1.1 1483 | dev: true 1484 | 1485 | /yauzl@2.10.0: 1486 | resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} 1487 | dependencies: 1488 | buffer-crc32: 0.2.13 1489 | fd-slicer: 1.1.0 1490 | dev: true 1491 | 1492 | /yn@3.1.1: 1493 | resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} 1494 | engines: {node: '>=6'} 1495 | dev: true 1496 | 1497 | /yocto-queue@0.1.0: 1498 | resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 1499 | engines: {node: '>=10'} 1500 | dev: true 1501 | -------------------------------------------------------------------------------- /test/src/button-contributions-copy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file needs to be clear from imports, because it is copied into the test project and used by mocha. 3 | * Happy about anyone who's able to make this work with imports (i.e. run the tests in this project), but I couldn't figure it out and gave up. 4 | */ 5 | 6 | export type SupportedApplication = "github" | "gitlab" | "bitbucket-server" | "bitbucket" | "azure-devops"; 7 | 8 | const resolveMetaAppName = (head: HTMLHeadElement): string | undefined => { 9 | const metaApplication = head.querySelector("meta[name=application-name]"); 10 | const ogApplication = head.querySelector("meta[property='og:site_name']"); 11 | 12 | if (metaApplication) { 13 | return metaApplication.getAttribute("content") || undefined; 14 | } else if (ogApplication) { 15 | return ogApplication.getAttribute("content") || undefined; 16 | } 17 | 18 | return undefined; 19 | }; 20 | 21 | export const DEFAULT_HOSTS = ["github.com", "gitlab.com", "bitbucket.org", "dev.azure.com"]; 22 | 23 | /** 24 | * Provides a fast check to see if the current URL is on a supported site. 25 | */ 26 | export const isSiteSuitable = (): boolean => { 27 | const isWhitelistedHost = DEFAULT_HOSTS.some((host) => location.host === host); 28 | if (isWhitelistedHost) { 29 | return true; 30 | } 31 | 32 | const appName = resolveMetaAppName(document.head); 33 | if (!appName) { 34 | return false; 35 | } 36 | const allowedApps = ["GitHub", "GitLab", "Bitbucket"]; 37 | 38 | return allowedApps.some((allowedApp) => appName.includes(allowedApp)); 39 | }; 40 | 41 | export interface ButtonContributionParams { 42 | /** 43 | * A unique id for the button contribution. Used to identify the button in the UI. 44 | */ 45 | id: string; 46 | 47 | /** 48 | * 49 | */ 50 | exampleUrls: string[]; 51 | 52 | /** 53 | * A CSS selector that matches the parent element in which the button should be inserted. 54 | * 55 | * Use the developer tools -> right click on the element -> "copy JS path" to get the selector. 56 | */ 57 | selector: string; 58 | 59 | /** 60 | * The element in which the button should be inserted. 61 | * 62 | * This element will be inserted into the main document and allows for styling within the original page. 63 | * 64 | * The structure looks like this: 65 | * 66 | * 67 | * .... 68 | * 69 | * 70 | * #shadow-root 71 | * 72 | * 73 | * 74 | */ 75 | containerElement: { 76 | type: "div" | "li"; 77 | props: { 78 | [key: string]: string; 79 | }; 80 | }; 81 | 82 | /** 83 | * Either a regular expression that is used to match the current URL or a function expected to return a boolean. This is making the selection faster and also can help to disambiguate. 84 | */ 85 | match?: RegExp | (() => boolean); 86 | 87 | /** 88 | * The application that is supported by this button contribution. 89 | */ 90 | application: SupportedApplication; 91 | 92 | /** 93 | * Additional class names that should be added to the elements. 94 | */ 95 | additionalClassNames?: ("secondary" | "medium" | "left-align-menu" | "tall")[]; 96 | 97 | /** 98 | * A selector that is used to insert the button before a specific element. 99 | */ 100 | insertBefore?: string; 101 | 102 | /** 103 | * A list of manipulations that should be applied to the document. 104 | * 105 | * Each manipulation contains a CSS selector (element) that is used to find the element to manipulate and optionally 106 | * the classnames to remove and add. 107 | */ 108 | manipulations?: { element: string; remove?: string; add?: string; style?: Partial }[]; 109 | 110 | /** 111 | * A function that can be used to transform the URL that should be opened when the Gitpod button is clicked. 112 | * @returns The transformed URL. 113 | */ 114 | urlTransformer?: (originalURL: string) => string; 115 | } 116 | 117 | function createElement( 118 | type: "div" | "li", 119 | props: { 120 | [key: string]: string; 121 | }, 122 | ) { 123 | return { 124 | type, 125 | props, 126 | }; 127 | } 128 | 129 | export const buttonContributions: ButtonContributionParams[] = [ 130 | // Azure DevOps 131 | { 132 | id: "ado-repo", 133 | exampleUrls: [ 134 | // "https://dev.azure.com/services-azure/_git/project2" 135 | ], 136 | selector: "div.repos-files-header-commandbar:nth-child(1)", 137 | containerElement: createElement("div", {}), 138 | application: "azure-devops", 139 | insertBefore: `div.bolt-header-command-item-button:has(button[id^="__bolt-header-command-bar-menu-button"])`, 140 | manipulations: [ 141 | { 142 | element: "div.repos-files-header-commandbar.scroll-hidden", 143 | remove: "scroll-hidden", 144 | }, 145 | ], 146 | urlTransformer(originalUrl) { 147 | const url = new URL(originalUrl); 148 | if (url.pathname.includes("version=GB")) { 149 | return originalUrl; 150 | } 151 | // version=GBdevelop 152 | const branchElement = document.evaluate( 153 | "//div[contains(@class, 'version-dropdown')]//span[contains(@class, 'text-ellipsis')]", 154 | document, 155 | null, 156 | XPathResult.FIRST_ORDERED_NODE_TYPE, 157 | null, 158 | ).singleNodeValue; 159 | if (branchElement) { 160 | const branch = branchElement.textContent?.trim(); 161 | url.searchParams.set("version", `GB${branch}`); 162 | } 163 | 164 | return url.toString(); 165 | }, 166 | }, 167 | { 168 | id: "ado-pr", 169 | exampleUrls: [ 170 | // "https://dev.azure.com/services-azure/test-project/_git/repo2/pullrequest/1" 171 | ], 172 | selector: ".repos-pr-header > div:nth-child(2) > div:nth-child(1)", 173 | containerElement: createElement("div", {}), 174 | application: "azure-devops", 175 | insertBefore: `div.bolt-header-command-item-button:has(button[id^="__bolt-menu-button-"])`, 176 | }, 177 | { 178 | id: "ado-repo-empty", 179 | exampleUrls: [], 180 | selector: "div.clone-with-application", 181 | application: "azure-devops", 182 | containerElement: createElement("div", { marginLeft: "4px", marginRight: "4px" }), 183 | }, 184 | 185 | // GitLab 186 | { 187 | id: "gl-repo", // also taking care of branches 188 | exampleUrls: [ 189 | "https://gitlab.com/svenefftinge/browser-extension-test", 190 | "https://gitlab.com/svenefftinge/browser-extension-test/-/tree/my-branch", 191 | ], 192 | // must not match /blob/ because that is a file 193 | match: /^(?!.*\/blob\/).*$/, 194 | selector: "#tree-holder .tree-controls", 195 | containerElement: { type: "div", props: { marginLeft: "8px" } }, 196 | application: "gitlab", 197 | manipulations: [ 198 | { 199 | // make the clone button secondary 200 | element: "#clone-dropdown", 201 | remove: "btn-confirm", 202 | }, 203 | ], 204 | }, 205 | { 206 | id: "gl-repo-empty", 207 | exampleUrls: ["https://gitlab.com/filiptronicek/empty"], 208 | selector: `xpath://*[@id="js-project-show-empty-page"]/div[1]/div[1]/div[2]`, 209 | containerElement: { type: "div", props: { marginLeft: "8px" } }, 210 | application: "gitlab", 211 | }, 212 | { 213 | id: "gl-file", 214 | exampleUrls: [ 215 | //TODO fix me "https://gitlab.com/svenefftinge/browser-extension-test/-/blob/my-branch/README.md", 216 | ], 217 | match: /\/blob\//, 218 | selector: 219 | "#fileHolder > div.js-file-title.file-title-flex-parent > div.gl-display-flex.gl-flex-wrap.file-actions", 220 | containerElement: createElement("div", { display: "inline-flex", marginLeft: "8px" }), 221 | application: "gitlab", 222 | manipulations: [ 223 | { 224 | // make the clone button secondary 225 | element: 226 | "#fileHolder > div.js-file-title.file-title-flex-parent > div.gl-display-flex.gl-flex-wrap.file-actions > div.gl-sm-ml-3.gl-mr-3 > div > button", 227 | remove: "btn-confirm", 228 | }, 229 | ], 230 | }, 231 | { 232 | id: "gl-merge-request", 233 | exampleUrls: ["https://gitlab.com/svenefftinge/browser-extension-test/-/merge_requests/1"], 234 | match: /\/merge_requests\//, 235 | selector: "body[data-project-id] div.detail-page-header-actions.is-merge-request > div", 236 | containerElement: createElement("div", { marginLeft: "8px", marginRight: "-8px" }), 237 | application: "gitlab", 238 | insertBefore: "body[data-project-id] div.detail-page-header-actions.is-merge-request > div > div", 239 | manipulations: [ 240 | { 241 | // make the clone button secondary 242 | element: 243 | "#content-body > div.merge-request > div.detail-page-header.border-bottom-0.gl-display-block.gl-pt-5.gl-sm-display-flex\\!.is-merge-request > div.detail-page-header-actions.gl-align-self-start.is-merge-request.js-issuable-actions.gl-display-flex > div > div.gl-sm-ml-3.dropdown.gl-dropdown > button", 244 | remove: "btn-confirm", 245 | }, 246 | ], 247 | }, 248 | { 249 | id: "gl-issue", 250 | exampleUrls: ["https://gitlab.com/svenefftinge/browser-extension-test/-/issues/1"], 251 | match: /\/issues\//, 252 | selector: 253 | "#content-body > div.issue-details.issuable-details.js-issue-details > div.detail-page-description.content-block.js-detail-page-description.gl-pt-3.gl-pb-0.gl-border-none > div:nth-child(1) > div > div.gl-flex.gl-items-start.gl-flex-col.md\\:gl-flex-row.gl-gap-3.gl-pt-3 > div", 254 | containerElement: createElement("div", { marginLeft: "0", marginRight: "0px" }), 255 | application: "gitlab", 256 | insertBefore: "#new-actions-header-dropdown", 257 | manipulations: [ 258 | { 259 | element: 260 | "#content-body > div.issue-details.issuable-details.js-issue-details > div.detail-page-description.content-block.js-detail-page-description.gl-pt-3.gl-pb-0.gl-border-none > div.js-issue-widgets > div > div > div.new-branch-col.gl-font-size-0.gl-my-2 > div > div.btn-group.available > button.gl-button.btn.btn-md.btn-confirm.js-create-merge-request", 261 | remove: "btn-confirm", 262 | }, 263 | { 264 | element: 265 | "#content-body > div.issue-details.issuable-details.js-issue-details > div.detail-page-description.content-block.js-detail-page-description.gl-pt-3.gl-pb-0.gl-border-none > div.js-issue-widgets > div > div > div.new-branch-col.gl-font-size-0.gl-my-2 > div > div.btn-group.available > button.gl-button.btn.btn-icon.btn-md.btn-confirm.js-dropdown-toggle.dropdown-toggle.create-merge-request-dropdown-toggle", 266 | remove: "btn-confirm", 267 | }, 268 | ], 269 | }, 270 | 271 | // GitHub 272 | { 273 | id: "gh-repo", 274 | exampleUrls: [ 275 | // disabled testing, because the new layout doesn't show as an anonymous user 276 | "https://github.com/svenefftinge/browser-extension-test", 277 | "https://github.com/svenefftinge/browser-extension-test/tree/my-branch", 278 | ], 279 | selector: `xpath://*[contains(@id, 'repo-content-')]/div/div/div/div[1]/react-partial/div/div/div[2]/div[2]`, 280 | containerElement: createElement("div", {}), 281 | additionalClassNames: ["medium"], 282 | application: "github", 283 | match: () => { 284 | const regex = /^https?:\/\/([^/]+)\/([^/]+)\/([^/]+)(\/(tree\/.*)?)?$/; 285 | return document.querySelector("div.file-navigation") === null && regex.test(window.location.href); 286 | }, 287 | }, 288 | { 289 | id: "gh-commit", 290 | exampleUrls: [ 291 | "https://github.com/svenefftinge/browser-extension-test/commit/82d701a9ac26ea25da9b24c5b3722b7a89e43b16", 292 | ], 293 | selector: "#repo-content-pjax-container > div > div.commit.full-commit.mt-0.px-2.pt-2", 294 | insertBefore: "#browse-at-time-link", 295 | containerElement: createElement("div", { 296 | float: "right", 297 | marginLeft: "8px", 298 | }), 299 | application: "github", 300 | additionalClassNames: ["medium"], 301 | }, 302 | 303 | { 304 | id: "gh-issue", 305 | exampleUrls: ["https://github.com/svenefftinge/browser-extension-test/issues/1"], 306 | selector: "#partial-discussion-header > div.gh-header-show > div > div", 307 | containerElement: createElement("div", { 308 | order: "2", 309 | }), 310 | match: /\/issues\//, 311 | application: "github", 312 | manipulations: [ 313 | { 314 | // make the code button secondary 315 | element: "#partial-discussion-header > div.gh-header-show > div > div > a", 316 | remove: "Button--primary", 317 | add: "Button--secondary", 318 | }, 319 | ], 320 | }, 321 | { 322 | id: "gh-issue-new", // this isn't referring to "new issue", but to new "issue" 323 | exampleUrls: ["https://github.com/svenefftinge/browser-extension-test/issues/1"], 324 | selector: `xpath://*[@id="js-repo-pjax-container"]/react-app/div/div/div/div/div[1]/div/div/div[3]/div`, 325 | containerElement: createElement("div", {}), 326 | insertBefore: `xpath://*[@id="js-repo-pjax-container"]/react-app/div/div/div/div/div[1]/div/div/div[3]/div/div`, 327 | application: "github", 328 | // we need to make the button higher: the buttons here use 2rem instead of 1.75rem 329 | additionalClassNames: ["tall"], 330 | }, 331 | { 332 | id: "gh-pull", 333 | exampleUrls: ["https://github.com/svenefftinge/browser-extension-test/pull/2"], 334 | selector: "#partial-discussion-header > div.gh-header-show > div > div", 335 | containerElement: createElement("div", { 336 | order: "2", 337 | }), 338 | match: /\/pull\//, 339 | application: "github", 340 | }, 341 | { 342 | id: "gh-file", 343 | exampleUrls: ["https://github.com/svenefftinge/browser-extension-test/blob/my-branch/README.md"], 344 | selector: "#StickyHeader > div > div > div.Box-sc-g0xbh4-0.gtBUEp", 345 | containerElement: createElement("div", { 346 | marginLeft: "8px", 347 | }), 348 | application: "github", 349 | additionalClassNames: ["medium"], 350 | }, 351 | { 352 | id: "gh-empty-repo", 353 | exampleUrls: [ 354 | //TODO fixme "https://github.com/svenefftinge/empty-repo", 355 | ], 356 | selector: 357 | "#repo-content-pjax-container > div > div.d-md-flex.flex-items-stretch.gutter-md.mb-4 > div.col-md-6.mb-4.mb-md-0 > div,#repo-content-turbo-frame > div > div.d-md-flex.flex-items-stretch.gutter-md.mb-4 > div.col-md-6.mb-4.mb-md-0 > div", 358 | containerElement: createElement("div", {}), 359 | application: "github", 360 | manipulations: [ 361 | { 362 | element: 363 | "#repo-content-pjax-container > div > div.d-md-flex.flex-items-stretch.gutter-md.mb-4 > div.col-md-6.mb-4.mb-md-0 > div > a, #repo-content-turbo-frame > div > div.d-md-flex.flex-items-stretch.gutter-md.mb-4 > div.col-md-6.mb-4.mb-md-0 > div > a", 364 | style: { 365 | display: "none", 366 | }, 367 | }, 368 | { 369 | element: 370 | "#repo-content-pjax-container > div > div.d-md-flex.flex-items-stretch.gutter-md.mb-4 > div.col-md-6.mb-4.mb-md-0 > div > h3, #repo-content-turbo-frame > div > div.d-md-flex.flex-items-stretch.gutter-md.mb-4 > div.col-md-6.mb-4.mb-md-0 > div > h3", 371 | style: { 372 | display: "none", 373 | }, 374 | }, 375 | ], 376 | }, 377 | // Bitbucket Server 378 | { 379 | id: "bbs-repo", 380 | match: /\/(browse|commits)/, 381 | exampleUrls: [ 382 | "https://bitbucket.gitpod-dev.com/users/svenefftinge/repos/browser-extension-test/browse", 383 | "https://bitbucket.gitpod-dev.com/users/svenefftinge/repos/browser-extension-test/browse?at=refs%2Fheads%2Fmy-branch", 384 | ], 385 | selector: 386 | "#main > div.aui-toolbar2.branch-selector-toolbar > div > div.aui-toolbar2-primary > div > div:nth-child(1) > div", 387 | insertBefore: "#branch-actions", 388 | containerElement: createElement("div", { 389 | marginLeft: "2px", 390 | }), 391 | application: "bitbucket-server", 392 | additionalClassNames: ["secondary"], 393 | }, 394 | { 395 | id: "bbs-pull-request", 396 | exampleUrls: [ 397 | // disabled because it doesn't work anonymously 398 | // "https://bitbucket.gitpod-dev.com/users/svenefftinge/repos/browser-extension-test/pull-requests/1/overview", 399 | ], 400 | selector: "#pull-requests-container > header > div.pull-request-header-bar > div.pull-request-actions", 401 | insertBefore: 402 | "#pull-requests-container > header > div.pull-request-header-bar > div.pull-request-actions > div.pull-request-more-actions", 403 | containerElement: createElement("div", { 404 | marginLeft: "2px", 405 | }), 406 | application: "bitbucket-server", 407 | }, 408 | 409 | // bitbucket.org 410 | // we use xpath expressions, because the CSS selectors are not stable enough 411 | // tests are disabled because the URLs are not reachable without a session 412 | { 413 | id: "bb-repo", 414 | exampleUrls: [ 415 | // "https://bitbucket.org/svenefftinge/browser-extension-test/src/master/" 416 | ], 417 | selector: 'xpath://*[@id="main"]/div/div/div[1]/div/header/div/div/div/div[2]/div', 418 | insertBefore: 419 | "#main > div > div > div.css-1m2ufqk.efo6slf1 > div > header > div > div > div > div.css-1ianfu6 > div > div:nth-child(2)", 420 | containerElement: createElement("div", { 421 | marginLeft: "2px", 422 | }), 423 | application: "bitbucket", 424 | }, 425 | { 426 | id: "bb-pull-request", 427 | exampleUrls: [ 428 | // "https://bitbucket.org/efftinge/browser-extension-test/pull-requests/1" 429 | ], 430 | selector: 'xpath://*[@id="main"]/div/div/div[1]/div/div/div/div[1]/div/div[2]/div/div[2]/div/div', // grandparent div of the "Request changes" and "Approve" buttons 431 | containerElement: createElement("div", {}), 432 | insertBefore: 433 | 'xpath:(//*[@id="main"]/div/div/div[1]/div/div/div/div[1]/div/div[2]/div/div[2]/div/div/div)[last()]', // note the [last()] to insert before the last child (the kebab menu) 434 | application: "bitbucket", 435 | }, 436 | { 437 | id: "bb-branch", 438 | match: /\/branch\/(.+)/, 439 | exampleUrls: [ 440 | // "https://bitbucket.org/efftinge/browser-extension-test/branch/my-branch" 441 | ], 442 | selector: 'xpath://*[@id="main"]/div/div/div[1]/div/div/div[2]/div/div', // action bar section with the last action of "Settings" 443 | containerElement: createElement("div", { 444 | marginLeft: "2px", 445 | }), 446 | application: "bitbucket", 447 | }, 448 | { 449 | id: "bb-commits", 450 | match: /\/commits\/(.+)?/, 451 | exampleUrls: ["https://bitbucket.org/efftinge/browser-extension-test/commits/"], 452 | selector: 'xpath://*[@id="main"]/div/div/div[1]/div/div/div[1]/div[1]/div[2]/div', 453 | containerElement: createElement("div", { 454 | marginLeft: "2px", 455 | }), 456 | application: "bitbucket", 457 | }, 458 | ]; 459 | -------------------------------------------------------------------------------- /test/src/button-contributions.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { after, before, describe, it } from "mocha"; 3 | import puppeteer, { Browser, Page } from "puppeteer"; 4 | 5 | import { buttonContributions } from "./button-contributions-copy.js"; 6 | 7 | describe("Platform match tests", function () { 8 | let browser: Browser; 9 | let page: Page; 10 | 11 | before(async function () { 12 | browser = await puppeteer.launch({ 13 | headless: "new", 14 | }); 15 | page = await browser.newPage(); 16 | }); 17 | 18 | after(async function () { 19 | await browser.close(); 20 | }); 21 | 22 | async function testHost() { 23 | const all = buttonContributions.flatMap((x) => x.exampleUrls); 24 | for (const url of all) { 25 | it(`should detect the platform for ${url}`, async function () { 26 | await page.goto(url); 27 | 28 | const foundMatch = await page.evaluate(() => { 29 | const resolveMetaAppName = (head: HTMLHeadElement): string | undefined => { 30 | const metaApplication = head.querySelector("meta[name=application-name]"); 31 | const ogApplication = head.querySelector("meta[property='og:site_name']"); 32 | 33 | if (metaApplication) { 34 | return metaApplication.getAttribute("content") || undefined; 35 | } else if (ogApplication) { 36 | return ogApplication.getAttribute("content") || undefined; 37 | } 38 | 39 | return undefined; 40 | }; 41 | 42 | const isSiteSuitable = (): boolean => { 43 | const appName = resolveMetaAppName(document.head); 44 | if (!appName) { 45 | return false; 46 | } 47 | const allowedApps = ["GitHub", "GitLab", "Bitbucket"]; 48 | 49 | return allowedApps.some((app) => appName.includes(app)); 50 | }; 51 | 52 | return isSiteSuitable(); 53 | }); 54 | expect(foundMatch, `Expected to find a match for '${url}'`).to.be.true; 55 | }).timeout(30_000); 56 | } 57 | } 58 | 59 | testHost(); 60 | }); 61 | 62 | describe("Query Selector Tests", function () { 63 | let browser: Browser; 64 | let page: Page; 65 | 66 | before(async function () { 67 | browser = await puppeteer.launch({ 68 | headless: "new", 69 | }); 70 | page = await browser.newPage(); 71 | }); 72 | 73 | after(async function () { 74 | await browser.close(); 75 | }); 76 | 77 | async function resolveSelector(page: Page, selector: string) { 78 | if (selector.startsWith("xpath:")) { 79 | return (await page.$x(selector.slice(6)))[0] || null; 80 | } else { 81 | return page.$(selector); 82 | } 83 | } 84 | 85 | async function testContribution(url: string, id: string) { 86 | await page.goto(url); 87 | let foundMatch = false; 88 | for (const contr of buttonContributions) { 89 | if (typeof contr.match === "object" && !contr.match.test(url)) { 90 | continue; 91 | } 92 | const element = await resolveSelector(page, contr.selector); 93 | if (contr.id === id) { 94 | expect(element, `Expected '${id}' to match on ${url}`).to.not.be.null; 95 | foundMatch = true; 96 | } else { 97 | if (contr.exampleUrls.length === 0) return true; 98 | expect(element, `Did not expect '${contr.id}' to match on ${url}`).to.be.null; 99 | } 100 | } 101 | expect(foundMatch, `Expected to find a match for '${id}' on ${url}`).to.be.true; 102 | } 103 | 104 | for (const contribs of buttonContributions) { 105 | for (const url of contribs.exampleUrls) { 106 | it(`url (${url}) should only match '${contribs.id}'`, async function () { 107 | await testContribution(url, contribs.id); 108 | }).timeout(5000); 109 | } 110 | } 111 | }); 112 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true, 7 | "outDir": "./dist", 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true 10 | }, 11 | 12 | "include": ["src/**/*.ts"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plasmo/templates/tsconfig.base", 3 | "exclude": ["node_modules"], 4 | "include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx"], 5 | "compilerOptions": { 6 | "paths": { 7 | "~*": ["./src/*"] 8 | }, 9 | "baseUrl": "." 10 | } 11 | } 12 | --------------------------------------------------------------------------------