using my boilerplate yuh
1
.env.development
Normal file
@@ -0,0 +1 @@
|
|||||||
|
MANIFEST_KEY=
|
||||||
0
.env.production
Normal file
95
.eslintignore
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories and management
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Webpack-built output
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# Extension archives
|
||||||
|
/build
|
||||||
|
|
||||||
|
# VsCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Terraform
|
||||||
|
.terraform
|
||||||
|
|
||||||
|
# development dependencies
|
||||||
|
.dev/vue-devtools
|
||||||
|
.dev/browser-profiles
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Sylelint IntelliJ Plugin Requirement
|
||||||
|
.stylelintrc.json
|
||||||
|
|
||||||
|
# Local environment settings
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
*.svg
|
||||||
|
|
||||||
|
config
|
||||||
|
.eslintrc.js
|
||||||
208
.eslintrc
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"node": true,
|
||||||
|
"webextensions": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"airbnb-base",
|
||||||
|
"airbnb/rules/react",
|
||||||
|
"airbnb-typescript",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"import",
|
||||||
|
"jsdoc",
|
||||||
|
"react-prefer-function-component"
|
||||||
|
],
|
||||||
|
"globals": {
|
||||||
|
"Atomics": "readonly",
|
||||||
|
"SharedArrayBuffer": "readonly",
|
||||||
|
"debugger": true,
|
||||||
|
"browser": true,
|
||||||
|
"context": true,
|
||||||
|
"JSX": true
|
||||||
|
},
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./tsconfig.json",
|
||||||
|
"ecmaVersion": 2022,
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true,
|
||||||
|
"modules": true,
|
||||||
|
"experimentalObjectRestSpread": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
},
|
||||||
|
"jsdoc": {
|
||||||
|
"mode": "typescript"
|
||||||
|
},
|
||||||
|
"import/parsers": {
|
||||||
|
"@typescript-eslint/parser": [
|
||||||
|
".ts",
|
||||||
|
".tsx"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"import/resolver": {
|
||||||
|
"typescript": {
|
||||||
|
"alwaysTryTypes": true,
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"prefer-const": [
|
||||||
|
"off",
|
||||||
|
{
|
||||||
|
"destructuring": "any",
|
||||||
|
"ignoreReadBeforeAssign": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-inner-declarations": "off",
|
||||||
|
"sort-imports": "off",
|
||||||
|
"no-case-declarations": "off",
|
||||||
|
"no-unreachable": "warn",
|
||||||
|
"no-constant-condition": "error",
|
||||||
|
"space-before-function-paren": "off",
|
||||||
|
"no-undef": "off",
|
||||||
|
"no-return-await": "off",
|
||||||
|
"@typescript-eslint/return-await": "off",
|
||||||
|
"@typescript-eslint/no-shadow": [
|
||||||
|
"off"
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-use-before-define": [
|
||||||
|
"off"
|
||||||
|
],
|
||||||
|
"class-methods-use-this": "off",
|
||||||
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
|
"@typescript-eslint/lines-between-class-members": "off",
|
||||||
|
"no-param-reassign": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"props": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-console": "off",
|
||||||
|
"consistent-return": "off",
|
||||||
|
"react/destructuring-assignment": "off",
|
||||||
|
"import/prefer-default-export": "off",
|
||||||
|
"no-promise-executor-return": "off",
|
||||||
|
"import/no-cycle": "off",
|
||||||
|
"import/no-extraneous-dependencies": "off",
|
||||||
|
"react/jsx-props-no-spreading": "off",
|
||||||
|
"keyword-spacing": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"before": true,
|
||||||
|
"after": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-continue": "off",
|
||||||
|
"space-before-blocks": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"functions": "always",
|
||||||
|
"keywords": "always",
|
||||||
|
"classes": "always"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/jsx-filename-extension": [
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
"extensions": [
|
||||||
|
".tsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/no-deprecated": "warn",
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"react-prefer-function-component/react-prefer-function-component": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"allowComponentDidCatch": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/function-component-definition": "off",
|
||||||
|
"react/button-has-type": "off",
|
||||||
|
"jsdoc/require-param-type": "off",
|
||||||
|
"jsdoc/require-returns-type": "off",
|
||||||
|
"jsdoc/newline-after-description": "off",
|
||||||
|
"react/require-default-props": "off",
|
||||||
|
"jsdoc/require-jsdoc": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"enableFixer": true,
|
||||||
|
"publicOnly": true,
|
||||||
|
"checkConstructors": false,
|
||||||
|
"require": {
|
||||||
|
"ArrowFunctionExpression": true,
|
||||||
|
"ClassDeclaration": true,
|
||||||
|
"ClassExpression": true,
|
||||||
|
"FunctionExpression": true
|
||||||
|
},
|
||||||
|
"contexts": [
|
||||||
|
"MethodDefinition:not([key.name=\"componentDidMount\"]):not([key.name=\"render\"])",
|
||||||
|
"ArrowFunctionExpression",
|
||||||
|
"ClassDeclaration",
|
||||||
|
"ClassExpression",
|
||||||
|
"ClassProperty:not([key.name=\"state\"]):not([key.name=\"componentDidMount\"])",
|
||||||
|
"FunctionDeclaration",
|
||||||
|
"FunctionExpression",
|
||||||
|
"TSDeclareFunction",
|
||||||
|
"TSEnumDeclaration",
|
||||||
|
"TSInterfaceDeclaration",
|
||||||
|
"TSMethodSignature",
|
||||||
|
"TSModuleDeclaration",
|
||||||
|
"TSTypeAliasDeclaration"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "warn",
|
||||||
|
"@typescript-eslint/naming-convention": "off",
|
||||||
|
"@typescript-eslint/space-before-function-paren": "off",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"@typescript-eslint/no-empty-interface": "warn",
|
||||||
|
"import/no-restricted-paths": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"zones": [
|
||||||
|
{
|
||||||
|
"target": "./src/background",
|
||||||
|
"from": "./src/views",
|
||||||
|
"message": "You cannot import into the `background` directory from the `views` directory (i.e. content script files) because it will break the build!"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "./src/views",
|
||||||
|
"from": "./src/background",
|
||||||
|
"message": "You cannot import into the `views` directory from the `background` directory (i.e. background script files) because it will break the build!"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "./src/shared",
|
||||||
|
"from": "./",
|
||||||
|
"except": [
|
||||||
|
"./src/shared",
|
||||||
|
"./node_modules"
|
||||||
|
],
|
||||||
|
"message": "You cannot import into `shared` from an external directory."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-restricted-syntax": [
|
||||||
|
"error",
|
||||||
|
"ForInStatement",
|
||||||
|
"LabeledStatement",
|
||||||
|
"WithStatement"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
25
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Create Release
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- production
|
||||||
|
- preview
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: build extension & create release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
- name: Get file permission
|
||||||
|
run: chmod -R 777 .
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Release with semantic-release
|
||||||
|
id: semantic-release
|
||||||
|
run: npx --no-install semantic-release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
43
.github/workflows/validate-pr.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: Validate PR Title
|
||||||
|
|
||||||
|
# thank you ben limmer for this workflow:
|
||||||
|
# https://github.com/blimmer/semantic-release-demo-2/blob/main/.github/workflows/lint-pr.yml
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: amannn/action-semantic-pull-request@v3.2.6
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Post Conventional Commit Comment (on failure)
|
||||||
|
uses: jungwinter/comment@v1
|
||||||
|
id: conventional-commit-help
|
||||||
|
with:
|
||||||
|
type: create
|
||||||
|
issue_number: ${{ github.event.pull_request.number }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
body: |
|
||||||
|
Your pull request title did not conform to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) standards. Our upcoming automated release pipeline will automatically determine
|
||||||
|
the proper release version based on your pull request title.
|
||||||
|
**Cheat Sheet**
|
||||||
|
- feat: A new feature
|
||||||
|
- fix: A bug fix
|
||||||
|
- docs: Documentation only changes
|
||||||
|
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||||
|
- refactor: A code change that neither fixes a bug nor adds a feature
|
||||||
|
- perf: A code change that improves performance
|
||||||
|
- test: Adding missing tests or correcting existing tests
|
||||||
|
- build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
||||||
|
- ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
|
||||||
|
- chore: Other changes that don't modify src or test files
|
||||||
|
- revert: Reverts a previous commit
|
||||||
|
if: ${{ failure() }}
|
||||||
214
.gitignore
vendored
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,node,react,storybookjs
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,node,react,storybookjs
|
||||||
|
|
||||||
|
### macOS ###
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
### macOS Patch ###
|
||||||
|
# iCloud generated files
|
||||||
|
*.icloud
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
### react ###
|
||||||
|
.DS_*
|
||||||
|
**/*.backup.*
|
||||||
|
**/*.back.*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
*.sublime*
|
||||||
|
|
||||||
|
psd
|
||||||
|
thumb
|
||||||
|
sketch
|
||||||
|
|
||||||
|
|
||||||
|
### VisualStudioCode ###
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Built Visual Studio Code Extensions
|
||||||
|
*.vsix
|
||||||
|
|
||||||
|
### VisualStudioCode Patch ###
|
||||||
|
# Ignore all local history of files
|
||||||
|
.history
|
||||||
|
.ionide
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,node,react,storybookjs
|
||||||
|
|
||||||
|
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
|
||||||
|
|
||||||
19
.prettierignore
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
*.css
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
# Webpack-built output
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# Extension archives
|
||||||
|
/build
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Dependency directories and management
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
12
.prettierrc.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"useTabs": false,
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"semi": true,
|
||||||
|
"jsxSingleQuote": true
|
||||||
|
}
|
||||||
37
.releaserc.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"branches": [
|
||||||
|
"production",
|
||||||
|
{
|
||||||
|
"name": "preview",
|
||||||
|
"channel": "alpha",
|
||||||
|
"prerelease": "alpha"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
{
|
||||||
|
"preset": "conventionalcommits"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
{
|
||||||
|
"preset": "conventionalcommits"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/exec",
|
||||||
|
{
|
||||||
|
"prepareCmd": "SEMANTIC_VERSION=${nextRelease.version} npm run build"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/github",
|
||||||
|
{
|
||||||
|
"assets": "build/**/artifacts/*.*",
|
||||||
|
"failComment": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
18
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Run current script",
|
||||||
|
"runtimeExecutable": "npx",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"tsx"
|
||||||
|
],
|
||||||
|
"program": "${file}",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
36
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[yaml]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[svg]": {
|
||||||
|
"editor.defaultFormatter": "jock.svg"
|
||||||
|
},
|
||||||
|
"material-icon-theme.activeIconPack": "react",
|
||||||
|
"material-icon-theme.folders.associations": {
|
||||||
|
"analytics": "Json",
|
||||||
|
"background": "Delta",
|
||||||
|
"navigation": "Routes",
|
||||||
|
"logging": "log",
|
||||||
|
"popup": "Layout",
|
||||||
|
"storage": "Database",
|
||||||
|
},
|
||||||
|
"material-icon-theme.files.associations": {
|
||||||
|
"tsconfig.extension.json": "tsconfig",
|
||||||
|
"tsconfig.build.json": "tsconfig",
|
||||||
|
"tsconfig.test.json": "tsconfig"
|
||||||
|
},
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
}
|
||||||
13
@types/css-imports.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
declare module "*.module.css" {
|
||||||
|
const classes: { [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.module.scss" {
|
||||||
|
const classes: { [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module "*.mp3" {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
22
@types/environment.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
declare global {
|
||||||
|
namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
NODE_ENV: 'development' | 'production';
|
||||||
|
CI?: string;
|
||||||
|
/** set this to make sure the extension id is the same for unpacked extensions
|
||||||
|
* @see https://developer.chrome.com/docs/apps/app_identity/#copy_key */
|
||||||
|
MANIFEST_KEY?: string;
|
||||||
|
/**
|
||||||
|
* The Node semantic versioning-compatible version of the extension. For preview-style releases, this variable
|
||||||
|
* converts versions like 1.0.0.100 to 1.0.0-beta.1.
|
||||||
|
*/
|
||||||
|
SEMANTIC_VERSION?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Environment = typeof process.env.NODE_ENV;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this file has no import/export statements (i.e. is a script)
|
||||||
|
// convert it into a module by adding an empty export statement.
|
||||||
|
export {};
|
||||||
6
@types/svg-import.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
declare module "*.svg" {
|
||||||
|
import { ReactElement, SVGProps } from "react";
|
||||||
|
|
||||||
|
const ReactComponent: (props: SVGProps<SVGElement>) => ReactElement;
|
||||||
|
export default ReactComponent;
|
||||||
|
}
|
||||||
19
README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# UT Registration Plus
|
||||||
|
|
||||||
|
## Built Using:
|
||||||
|
|
||||||
|
- React 18
|
||||||
|
- Typescript
|
||||||
|
- Webpack 5 (esbuild-loader)
|
||||||
|
- Eslint
|
||||||
|
- Prettier
|
||||||
|
- Semantic-Release
|
||||||
|
- Custom Messaging & Storage wrappers
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Clone this repo
|
||||||
|
2. Run `npm install`
|
||||||
|
3. Run `npm start` to start the development server
|
||||||
|
4. Run `npm run build` to build the extension for production
|
||||||
|
5. Run `npm run release` to release a new version of the extension in CI (either preview or production)
|
||||||
29944
package-lock.json
generated
Normal file
77
package.json
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"name": "ut-registration-plus",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "The UT Registration Plus extension is a Chrome extension that allows students to easily register for classes at The University of Texas at Austin.",
|
||||||
|
"private": true,
|
||||||
|
"homepage": "sriramhariharan.com",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsx webpack/development.ts",
|
||||||
|
"build": "tsx webpack/production.ts",
|
||||||
|
"release": "tsx webpack/release.ts",
|
||||||
|
"devtools": "react-devtools",
|
||||||
|
"lint": "eslint ./ --ext .ts,.tsx"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chrome-extension-toolkit": "^0.0.21",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"sass": "^1.57.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@semantic-release/exec": "^6.0.3",
|
||||||
|
"@svgr/webpack": "^6.5.1",
|
||||||
|
"@types/chrome": "^0.0.204",
|
||||||
|
"@types/node": "^18.11.17",
|
||||||
|
"@types/prompts": "^2.4.2",
|
||||||
|
"@types/react": "^18.0.26",
|
||||||
|
"@types/react-dom": "^18.0.9",
|
||||||
|
"@types/semver": "^7.3.13",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.47.0",
|
||||||
|
"@typescript-eslint/parser": "^5.47.0",
|
||||||
|
"archiver": "^5.3.1",
|
||||||
|
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
||||||
|
"chalk": "^5.2.0",
|
||||||
|
"conventional-changelog-conventionalcommits": "^5.0.0",
|
||||||
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
"create-file-webpack": "^1.0.2",
|
||||||
|
"css-loader": "^6.7.3",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"esbuild-loader": "^2.20.0",
|
||||||
|
"eslint": "^8.30.0",
|
||||||
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.5.2",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-jsdoc": "^39.6.4",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-react": "^7.31.11",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-prefer-function-component": "^3.1.0",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"fork-ts-checker-webpack-plugin": "^7.2.14",
|
||||||
|
"html-webpack-plugin": "^5.5.0",
|
||||||
|
"mini-css-extract-plugin": "^2.7.2",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"prettier": "^2.8.1",
|
||||||
|
"prompts": "^2.4.2",
|
||||||
|
"react-dev-utils": "^12.0.1",
|
||||||
|
"react-devtools": "^4.27.1",
|
||||||
|
"sass-loader": "^13.2.0",
|
||||||
|
"semantic-release": "^19.0.5",
|
||||||
|
"semver": "^7.3.8",
|
||||||
|
"simple-git": "^3.15.1",
|
||||||
|
"socket.io": "^4.5.4",
|
||||||
|
"socket.io-client": "^4.5.4",
|
||||||
|
"terser-webpack-plugin": "^5.3.6",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||||
|
"tsx": "^3.12.1",
|
||||||
|
"typescript": "^4.9.4",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
|
"webpack": "^5.75.0",
|
||||||
|
"webpack-build-notifier": "^2.3.0",
|
||||||
|
"webpack-dev-server": "^4.11.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
public/LICENSE.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Copyright © 2021 <Company>, All rights reserved
|
||||||
|
|
||||||
|
All information contained herein is, and remains the property of <COMPANY>. The intellectual and technical concepts contained
|
||||||
|
herein are proprietary to <Company> and may be covered by U.S. and Foreign Patents, patents in process, and are protected by trade secret or copyright law.
|
||||||
|
Dissemination of this information or reproduction of this material is strictly forbidden unless prior written permission is obtained from <Company>.
|
||||||
BIN
public/fonts/inter-100.woff2
Normal file
BIN
public/fonts/inter-200.woff2
Normal file
BIN
public/fonts/inter-300.woff2
Normal file
BIN
public/fonts/inter-400.woff2
Normal file
BIN
public/fonts/inter-500.woff2
Normal file
BIN
public/fonts/inter-600.woff2
Normal file
BIN
public/fonts/inter-700.woff2
Normal file
BIN
public/fonts/inter-800.woff2
Normal file
BIN
public/fonts/inter-900.woff2
Normal file
BIN
public/icons/icon_development_128.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/icons/icon_development_16.png
Normal file
|
After Width: | Height: | Size: 430 B |
BIN
public/icons/icon_development_32.png
Normal file
|
After Width: | Height: | Size: 849 B |
BIN
public/icons/icon_development_48.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/icons/icon_production_128.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/icons/icon_production_16.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/icons/icon_production_32.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/icons/icon_production_48.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
3
public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
50
src/background/background.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { MessageListener } from 'chrome-extension-toolkit';
|
||||||
|
import { BACKGROUND_MESSAGES } from 'src/shared/messages';
|
||||||
|
import { generateRandomId } from 'src/shared/util/random';
|
||||||
|
import onHistoryStateUpdated from './events/onHistoryStateUpdated';
|
||||||
|
import onInstall from './events/onInstall';
|
||||||
|
import onNewChromeSession from './events/onNewChromeSession';
|
||||||
|
import onServiceWorkerAlive from './events/onServiceWorkerAlive';
|
||||||
|
import onUpdate from './events/onUpdate';
|
||||||
|
import { sessionStore } from '../shared/storage/sessionStore';
|
||||||
|
import browserActionHandler from './handler/browserActionHandler';
|
||||||
|
import hotReloadingHandler from './handler/hotReloadingHandler';
|
||||||
|
import tabManagementHandler from './handler/tabManagementHandler';
|
||||||
|
|
||||||
|
onServiceWorkerAlive();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* will be triggered on either install or update
|
||||||
|
* (will also be triggered on a user's sync'd browsers (on other devices)))
|
||||||
|
*/
|
||||||
|
chrome.runtime.onInstalled.addListener(details => {
|
||||||
|
switch (details.reason) {
|
||||||
|
case 'install':
|
||||||
|
onInstall();
|
||||||
|
break;
|
||||||
|
case 'update':
|
||||||
|
onUpdate();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// This event is fired when any tab's url changes.
|
||||||
|
chrome.webNavigation.onHistoryStateUpdated.addListener(onHistoryStateUpdated);
|
||||||
|
|
||||||
|
// initialize the message listener that will listen for messages from the content script
|
||||||
|
const messageListener = new MessageListener<BACKGROUND_MESSAGES>({
|
||||||
|
...browserActionHandler,
|
||||||
|
...hotReloadingHandler,
|
||||||
|
...tabManagementHandler,
|
||||||
|
});
|
||||||
|
|
||||||
|
messageListener.listen();
|
||||||
|
|
||||||
|
sessionStore.getChromeSessionId().then(async chromeSessionId => {
|
||||||
|
if (!chromeSessionId) {
|
||||||
|
await sessionStore.setChromeSessionId(generateRandomId(10));
|
||||||
|
onNewChromeSession();
|
||||||
|
}
|
||||||
|
});
|
||||||
11
src/background/events/onHistoryStateUpdated.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* This event is fired when any tab's url changes.
|
||||||
|
* This is useful for content scripts to know when SPA navigations occur.
|
||||||
|
* @param details
|
||||||
|
*/
|
||||||
|
export default function onHistoryStateUpdated(
|
||||||
|
details: chrome.webNavigation.WebNavigationTransitionCallbackDetails
|
||||||
|
): void {
|
||||||
|
const { tabId, url } = details;
|
||||||
|
// TODO: send a message to tab with tabId to reanalyze the page
|
||||||
|
}
|
||||||
26
src/background/events/onInstall.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { SECOND } from 'src/shared/util/time';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the extension is first installed or synced onto a new machine
|
||||||
|
*/
|
||||||
|
export default async function onInstall() {
|
||||||
|
// set the uninstall url
|
||||||
|
chrome.runtime.setUninstallURL('https://www.google.com');
|
||||||
|
logOnInstallEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* making sure we are not sending duplicate install event for users that have synced browsers
|
||||||
|
* sync storage get's cleared on browser uninstall, so re-installing the browser will trigger the event
|
||||||
|
*/
|
||||||
|
function logOnInstallEvent() {
|
||||||
|
setTimeout(async () => {
|
||||||
|
const manifest = chrome.runtime.getManifest();
|
||||||
|
const INSTALL_KEY = `${manifest.short_name}-installed`;
|
||||||
|
const storage = await chrome.storage.sync.get(INSTALL_KEY);
|
||||||
|
if (!storage[INSTALL_KEY]) {
|
||||||
|
// TODO: send install event
|
||||||
|
await chrome.storage.sync.set({ [INSTALL_KEY]: true });
|
||||||
|
}
|
||||||
|
}, 5 * SECOND);
|
||||||
|
}
|
||||||
4
src/background/events/onNewChromeSession.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* This function is called when the user's browser opens for the first time
|
||||||
|
*/
|
||||||
|
export default function onNewChromeSession() {}
|
||||||
9
src/background/events/onServiceWorkerAlive.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { openDebugTab } from '../util/openDebugTab';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever the background service worker comes alive
|
||||||
|
* (usually around 30 seconds to 5 minutes after it was last alive)
|
||||||
|
*/
|
||||||
|
export default function onServiceWorkerAlive() {
|
||||||
|
openDebugTab();
|
||||||
|
}
|
||||||
10
src/background/events/onUpdate.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { hotReloadTab } from 'src/background/util/hotReloadTab';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the extension is updated (or when the extension is reloaded in development mode)
|
||||||
|
*/
|
||||||
|
export default function onUpdate() {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
hotReloadTab();
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/background/handler/browserActionHandler.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { MessageHandler } from 'chrome-extension-toolkit';
|
||||||
|
import BrowserActionMessages from 'src/shared/messages/BrowserActionMessages';
|
||||||
|
|
||||||
|
const browserActionHandler: MessageHandler<BrowserActionMessages> = {
|
||||||
|
disableBrowserAction({ sender, sendResponse }) {
|
||||||
|
// by setting the popup to an empty string, clicking the browser action will not open the popup.html.
|
||||||
|
// we can then add an onClickListener to it from the content script
|
||||||
|
chrome.action.setPopup({ tabId: sender.tab?.id, popup: '' }).then(sendResponse);
|
||||||
|
},
|
||||||
|
enableBrowserAction({ sender, sendResponse }) {
|
||||||
|
chrome.action
|
||||||
|
.setPopup({
|
||||||
|
tabId: sender.tab?.id,
|
||||||
|
popup: 'popup.html',
|
||||||
|
})
|
||||||
|
.then(sendResponse);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default browserActionHandler;
|
||||||
21
src/background/handler/hotReloadingHandler.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import HotReloadingMessages from 'src/shared/messages/HotReloadingMessages';
|
||||||
|
import { MessageHandler } from 'chrome-extension-toolkit';
|
||||||
|
import { devStore } from 'src/shared/storage/devStore';
|
||||||
|
|
||||||
|
const hotReloadingHandler: MessageHandler<HotReloadingMessages> = {
|
||||||
|
async reloadExtension({ sendResponse }) {
|
||||||
|
const isExtensionReloading = await devStore.getIsExtensionReloading();
|
||||||
|
if (!isExtensionReloading) return sendResponse();
|
||||||
|
|
||||||
|
const isTabReloading = await devStore.getIsExtensionReloading();
|
||||||
|
if (isTabReloading) {
|
||||||
|
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
const tabToReload = tabs[0];
|
||||||
|
|
||||||
|
await devStore.setReloadTabId(tabToReload?.id);
|
||||||
|
}
|
||||||
|
chrome.runtime.reload();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hotReloadingHandler;
|
||||||
18
src/background/handler/tabManagementHandler.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { MessageHandler } from 'chrome-extension-toolkit';
|
||||||
|
import TabManagementMessages from 'src/shared/messages/TabManagementMessages';
|
||||||
|
|
||||||
|
const tabManagementHandler: MessageHandler<TabManagementMessages> = {
|
||||||
|
getTabId({ sendResponse, sender }) {
|
||||||
|
sendResponse(sender.tab?.id ?? -1);
|
||||||
|
},
|
||||||
|
openNewTab({ data, sendResponse }) {
|
||||||
|
const { url } = data;
|
||||||
|
chrome.tabs.create({ url }).then(sendResponse);
|
||||||
|
},
|
||||||
|
removeTab({ data, sendResponse }) {
|
||||||
|
const { tabId } = data;
|
||||||
|
chrome.tabs.remove(tabId).then(sendResponse);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default tabManagementHandler;
|
||||||
39
src/background/util/hotReloadTab.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { devStore } from 'src/shared/storage/devStore';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of websites that we don't want to reload when the extension reloads (becuase it'd be hella annoying lmao)
|
||||||
|
*/
|
||||||
|
const HOT_RELOADING_WHITELIST = [
|
||||||
|
'youtube.com',
|
||||||
|
'twitch.tv',
|
||||||
|
'github.dev',
|
||||||
|
'figma.com',
|
||||||
|
'netflix.com',
|
||||||
|
'disneyplus.com',
|
||||||
|
'hbomax.com',
|
||||||
|
'spotify.com',
|
||||||
|
'localhost:6006',
|
||||||
|
'docs.google.com',
|
||||||
|
'reddit.com',
|
||||||
|
'gmail.com',
|
||||||
|
'photopea.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the tab that was open when the extension was reloaded
|
||||||
|
* @returns a promise that resolves when the tab is reloaded
|
||||||
|
*/
|
||||||
|
export async function hotReloadTab(): Promise<void> {
|
||||||
|
const { getIsTabReloading, getReloadTabId } = devStore;
|
||||||
|
|
||||||
|
const [isTabReloading, reloadTabId] = await Promise.all([getIsTabReloading(), getReloadTabId()]);
|
||||||
|
|
||||||
|
if (!isTabReloading || !reloadTabId) return;
|
||||||
|
|
||||||
|
chrome.tabs.get(reloadTabId, tab => {
|
||||||
|
if (!tab?.id) return;
|
||||||
|
if (!HOT_RELOADING_WHITELIST.find(url => tab.url?.includes(url))) {
|
||||||
|
chrome.tabs.reload(tab.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
24
src/background/util/openDebugTab.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { devStore } from 'src/shared/storage/devStore';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the debug tab as the first tab
|
||||||
|
*/
|
||||||
|
export async function openDebugTab() {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
const debugTabId = await devStore.getDebugTabId();
|
||||||
|
|
||||||
|
const isAlreadyOpen = await (await chrome.tabs.query({})).some(tab => tab.id === debugTabId);
|
||||||
|
if (isAlreadyOpen) return;
|
||||||
|
|
||||||
|
const wasVisible = await devStore.getWasDebugTabVisible();
|
||||||
|
|
||||||
|
const tab = await chrome.tabs.create({
|
||||||
|
url: chrome.runtime.getURL('debug.html'),
|
||||||
|
active: wasVisible,
|
||||||
|
pinned: true,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await devStore.setDebugTabId(tab.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/debug/hotReload.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import io from 'socket.io-client';
|
||||||
|
import { bMessenger } from 'src/shared/messages';
|
||||||
|
|
||||||
|
const socket = io('http://localhost:9090');
|
||||||
|
let reBuilding = false;
|
||||||
|
|
||||||
|
socket.on('disconnect', async reason => {
|
||||||
|
reBuilding = reason.includes('transport') && !reason.includes('client');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.onAny(args => {
|
||||||
|
console.log(args);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('connect', async () => {
|
||||||
|
if (!reBuilding) {
|
||||||
|
console.log('%c[hot-reloading] listening for changes...', 'color:white; background-color: orange;');
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
'%c[hot-reloading] changes detected, rebuilding and refreshing...',
|
||||||
|
'color:white; background-color: orange;'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('reload', async () => {
|
||||||
|
console.log('%c[hot-reloading] reloading...', 'color:white; background-color: orange;');
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||||
|
if (tabs?.[0]?.id) {
|
||||||
|
bMessenger.reloadExtension();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
129
src/debug/index.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import './hotReload';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { devStore } from 'src/shared/storage/devStore';
|
||||||
|
|
||||||
|
const manifest = chrome.runtime.getManifest();
|
||||||
|
|
||||||
|
interface JSONEditorProps {
|
||||||
|
data: any;
|
||||||
|
onChange: (updates: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function JSONEditor(props: JSONEditorProps) {
|
||||||
|
const { data, onChange } = props;
|
||||||
|
const [isEditing, setIsEditing] = React.useState(false);
|
||||||
|
const [json, setJson] = React.useState(JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setJson(JSON.stringify(data, null, 2));
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setJson(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
try {
|
||||||
|
const updates = JSON.parse(json);
|
||||||
|
onChange(updates);
|
||||||
|
setIsEditing(false);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert('Invalid JSON');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isEditing ? (
|
||||||
|
<div>
|
||||||
|
<div style={{ flex: 1, marginBottom: 10, gap: 10, display: 'flex' }}>
|
||||||
|
<button style={{ color: 'green' }} onClick={handleSave}>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button style={{ color: 'red' }} onClick={() => setIsEditing(false)}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea style={{ width: '100%', height: '300px' }} value={json} onChange={handleChange} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<pre onClick={() => setIsEditing(true)}>{json}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// const PrettyPrintJson = React.memo(({ data }: any) => (
|
||||||
|
// <div>
|
||||||
|
// <pre>{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
// </div>
|
||||||
|
// ));
|
||||||
|
|
||||||
|
function DevDashboard() {
|
||||||
|
const [localStorage, setLocalStorage] = React.useState<any>({});
|
||||||
|
const [syncStorage, setSyncStorage] = React.useState<any>({});
|
||||||
|
const [sessionStorage, setSessionStorage] = React.useState<any>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
devStore.setWasDebugTabVisible(true);
|
||||||
|
} else {
|
||||||
|
devStore.setWasDebugTabVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
chrome.storage.local.get(null, result => {
|
||||||
|
setLocalStorage(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.storage.sync.get(null, result => {
|
||||||
|
setSyncStorage(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.storage.session.get(null, result => {
|
||||||
|
setSessionStorage(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||||
|
if (areaName === 'local') {
|
||||||
|
setLocalStorage({ ...localStorage, ...changes });
|
||||||
|
} else if (areaName === 'sync') {
|
||||||
|
setSyncStorage({ ...syncStorage, ...changes });
|
||||||
|
} else if (areaName === 'session') {
|
||||||
|
setSessionStorage({ ...sessionStorage, ...changes });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEditStorage = (areaName: string) => (changes: Record<string, any>) => {
|
||||||
|
chrome.storage[areaName].set(changes);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>
|
||||||
|
{manifest.name} {manifest.version} - {process.env.NODE_ENV}
|
||||||
|
</h1>
|
||||||
|
<p>This tab is used for hot reloading and debugging. We will update this tab further in the future.</p>
|
||||||
|
<h2>Local Storage</h2>
|
||||||
|
<JSONEditor data={localStorage} onChange={handleEditStorage('local')} />
|
||||||
|
<h2>Sync Storage</h2>
|
||||||
|
<JSONEditor data={syncStorage} onChange={handleEditStorage('sync')} />
|
||||||
|
<h2>Session Storage</h2>
|
||||||
|
<JSONEditor data={sessionStorage} onChange={handleEditStorage('session')} />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<DevDashboard />, document.getElementById('root'));
|
||||||
6
src/shared/messages/BrowserActionMessages.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default interface BrowserActionMessages {
|
||||||
|
/** make it so that clicking the browser action will open the popup.html */
|
||||||
|
enableBrowserAction: () => void;
|
||||||
|
/** make it so that clicking the browser action will respond to interactions from the content script */
|
||||||
|
disableBrowserAction: () => void;
|
||||||
|
}
|
||||||
3
src/shared/messages/HotReloadingMessages.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default interface HotReloadingMessages {
|
||||||
|
reloadExtension: () => void;
|
||||||
|
}
|
||||||
21
src/shared/messages/TabManagementMessages.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Messages for managing the user's open tabs list
|
||||||
|
*/
|
||||||
|
export default interface TabManagementMessages {
|
||||||
|
/**
|
||||||
|
* Opens a new tab with the given URL
|
||||||
|
* @param data The URL to open
|
||||||
|
*/
|
||||||
|
openNewTab: (data: { url: string }) => chrome.tabs.Tab;
|
||||||
|
/**
|
||||||
|
* Gets the ID of the current tab (the tab that sent the message)
|
||||||
|
* @returns The ID of the current tab
|
||||||
|
*/
|
||||||
|
getTabId: () => number;
|
||||||
|
/**
|
||||||
|
* Removes the tab with the given ID
|
||||||
|
* @param data The ID of the tab to remove
|
||||||
|
* @returns The ID of the tab that was removed
|
||||||
|
*/
|
||||||
|
removeTab: (data: { tabId: number }) => void;
|
||||||
|
}
|
||||||
6
src/shared/messages/TabMessages.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* This is a type with all the message definitions that can be sent TO specific tabs
|
||||||
|
*/
|
||||||
|
export default interface TAB_MESSAGES {
|
||||||
|
reAnalyzePage: (data: { url: string }) => void;
|
||||||
|
}
|
||||||
21
src/shared/messages/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { createMessenger } from 'chrome-extension-toolkit';
|
||||||
|
import TAB_MESSAGES from './TabMessages';
|
||||||
|
import BrowserActionMessages from './BrowserActionMessages';
|
||||||
|
import HotReloadingMessages from './HotReloadingMessages';
|
||||||
|
import TabManagementMessages from './TabManagementMessages';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a type with all the message definitions that can be sent TO the background script
|
||||||
|
*/
|
||||||
|
export type BACKGROUND_MESSAGES = BrowserActionMessages & TabManagementMessages & HotReloadingMessages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility object that can be used to send type-safe messages to the background script
|
||||||
|
*/
|
||||||
|
export const bMessenger = createMessenger<BACKGROUND_MESSAGES>('background');
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility object that can be used to send type-safe messages to specific tabs
|
||||||
|
*/
|
||||||
|
export const tabMessenger = createMessenger<TAB_MESSAGES>('tab');
|
||||||
25
src/shared/storage/devStore.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { createStore } from 'chrome-extension-toolkit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that is used to store data that is only relevant during development
|
||||||
|
*/
|
||||||
|
interface IDevStore {
|
||||||
|
/** the tabId for the debug tab */
|
||||||
|
debugTabId?: number;
|
||||||
|
/** whether the debug tab is visible */
|
||||||
|
wasDebugTabVisible?: boolean;
|
||||||
|
/** whether we should enable extension reloading */
|
||||||
|
isExtensionReloading?: boolean;
|
||||||
|
/** whether we should enable tab reloading */
|
||||||
|
isTabReloading?: boolean;
|
||||||
|
/** The id of the tab that we want to reload (after the extension reloads itself ) */
|
||||||
|
reloadTabId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const devStore = createStore<IDevStore>('DEV_STORE', {
|
||||||
|
debugTabId: undefined,
|
||||||
|
isTabReloading: true,
|
||||||
|
wasDebugTabVisible: false,
|
||||||
|
isExtensionReloading: true,
|
||||||
|
reloadTabId: undefined,
|
||||||
|
});
|
||||||
15
src/shared/storage/sessionStore.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { createStore, Store } from 'chrome-extension-toolkit';
|
||||||
|
|
||||||
|
interface ISessionStore {
|
||||||
|
chromeSessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sessionStore = createStore<ISessionStore>(
|
||||||
|
'SESSION_STORE',
|
||||||
|
{
|
||||||
|
chromeSessionId: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
area: 'session',
|
||||||
|
}
|
||||||
|
);
|
||||||
25
src/shared/util/random.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Generate a random ID
|
||||||
|
*
|
||||||
|
* @returns string of size 10 made up of random numbers and letters
|
||||||
|
* @param length the length of the ID to generate
|
||||||
|
* @example "cdtl9l88pj"
|
||||||
|
*/
|
||||||
|
export function generateRandomId(length: number = 10): string {
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i += 1) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random number between min and max
|
||||||
|
* @param min the minimum number
|
||||||
|
* @param max the maximum number
|
||||||
|
* @returns a random number between min and max
|
||||||
|
*/
|
||||||
|
export function rangeRandom(min: number, max: number): number {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
34
src/shared/util/string.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Given a string, returns a string with the first letter capitalized.
|
||||||
|
* @input The string to capitalize.
|
||||||
|
*/
|
||||||
|
export function capitalize(input: string): string {
|
||||||
|
try {
|
||||||
|
return input.charAt(0).toUpperCase() + input.substring(1).toLowerCase();
|
||||||
|
} catch (err) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a string, returns a string with the first letter capitalized.
|
||||||
|
* @param input capitalize the first letter of this string
|
||||||
|
* @returns the string with the first letter capitalized
|
||||||
|
*/
|
||||||
|
export function capitalizeFirstLetter(input: string): string {
|
||||||
|
return input.charAt(0).toUpperCase() + input.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cuts the
|
||||||
|
* @param input The string to ellipsify.
|
||||||
|
* @param length The length of the string to return.
|
||||||
|
* @returns The ellipsified string.
|
||||||
|
*/
|
||||||
|
export const ellipsify = (input: string, chars: number): string => {
|
||||||
|
let ellipisifed = input;
|
||||||
|
if (input && input.length > chars) {
|
||||||
|
ellipisifed = `${input.substring(0, chars)}...`;
|
||||||
|
}
|
||||||
|
return ellipisifed;
|
||||||
|
};
|
||||||
19
src/shared/util/time.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export const MILLISECOND = 1;
|
||||||
|
export const SECOND = 1000 * MILLISECOND;
|
||||||
|
export const MINUTE = 60 * SECOND;
|
||||||
|
export const HOUR = 60 * MINUTE;
|
||||||
|
export const DAY = 24 * HOUR;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const sleep = (milliseconds: number): Promise<void> => new Promise(resolve => setTimeout(resolve, milliseconds));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks to see if expired by the time first stored and the time frame that it is stored for
|
||||||
|
*
|
||||||
|
* @param time time it was stored
|
||||||
|
* @param threshold time frame it can be stored for
|
||||||
|
* @return true if expired, false if the time frame is still in range
|
||||||
|
*/
|
||||||
|
export const didExpire = (time: number, threshold: number): boolean => time + threshold <= Date.now();
|
||||||
17
src/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"es2021"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"chrome",
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"../webpack"
|
||||||
|
]
|
||||||
|
}
|
||||||
19
src/views/content/components/Button/Button.module.scss
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
@import 'src/views/styles/base.module.scss';
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-family: 'Inter';
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/views/content/components/Button/Button.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { bMessenger } from 'src/shared/messages';
|
||||||
|
import styles from './Button.module.scss';
|
||||||
|
|
||||||
|
export function Button(): JSX.Element {
|
||||||
|
const handleOpenUrl = (url: string) => () => {
|
||||||
|
bMessenger.openNewTab({ url });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={styles.button} onClick={handleOpenUrl('https://www.google.com')}>
|
||||||
|
Click me
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
src/views/content/content.module.scss
Normal file
26
src/views/content/content.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { bMessenger } from 'src/shared/messages';
|
||||||
|
import { ContextInvalidated, createShadowDOM, onContextInvalidated } from 'chrome-extension-toolkit';
|
||||||
|
import { Button } from './components/Button/Button';
|
||||||
|
|
||||||
|
bMessenger.getTabId().then(tabId => {
|
||||||
|
console.log('tabId', tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
injectReact();
|
||||||
|
|
||||||
|
async function injectReact() {
|
||||||
|
const shadowDom = createShadowDOM('extension-dom-container');
|
||||||
|
render(<Button />, shadowDom.shadowRoot);
|
||||||
|
await shadowDom.addStyle('static/css/content.css');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
onContextInvalidated(() => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.id = 'context-invalidated-container';
|
||||||
|
document.body.appendChild(div);
|
||||||
|
render(<ContextInvalidated color='black' backgroundColor='orange' />, div);
|
||||||
|
});
|
||||||
|
}
|
||||||
4
src/views/hooks/useTabMessage.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { createUseMessage } from 'chrome-extension-toolkit';
|
||||||
|
import TAB_MESSAGES from 'src/shared/messages/TabMessages';
|
||||||
|
|
||||||
|
export const useTabMessage = createUseMessage<TAB_MESSAGES>();
|
||||||
17
src/views/popup/popup.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
|
||||||
|
console.log('test');
|
||||||
|
|
||||||
|
console.log('test2');
|
||||||
|
|
||||||
|
// Path: src/views/popup/popup.tsx
|
||||||
|
|
||||||
|
console.log('test3');
|
||||||
|
|
||||||
|
render(
|
||||||
|
<div>
|
||||||
|
<h1>Test</h1>
|
||||||
|
</div>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
24
src/views/reactDevtools.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// this is a custom wrapper around react-devtools
|
||||||
|
// that changes it so that we only send messages to the devtools when the current tab is active;
|
||||||
|
import { connectToDevTools } from 'react-devtools-core';
|
||||||
|
|
||||||
|
// connect to the devtools server
|
||||||
|
let ws = new WebSocket('ws://localhost:8097');
|
||||||
|
|
||||||
|
connectToDevTools({
|
||||||
|
websocket: ws,
|
||||||
|
});
|
||||||
|
|
||||||
|
// when the tab's visibile state changes, we connect or disconnect from the devtools
|
||||||
|
const onVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
ws = new WebSocket('ws://localhost:8097');
|
||||||
|
connectToDevTools({
|
||||||
|
websocket: ws,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||||
2
src/views/styles/base.module.scss
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@import './colors.module.scss';
|
||||||
|
@import './fonts.module.scss';
|
||||||
0
src/views/styles/colors.module.scss
Normal file
9
src/views/styles/fonts.module.scss
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@each $weights in '100' '200' '300' '400' '500' '600' '700' '800' '900' {
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('chrome-extension://__MSG_@@extension_id__/fonts/inter-#{$weights}.woff2') format('woff2');
|
||||||
|
font-display: auto;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: #{$weights};
|
||||||
|
}
|
||||||
|
}
|
||||||
55
tsconfig.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2021",
|
||||||
|
"outDir": "./",
|
||||||
|
"noEmit": true,
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types",
|
||||||
|
"./@types/"
|
||||||
|
],
|
||||||
|
"rootDir": "./",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowJs": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"incremental": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"es2021"
|
||||||
|
],
|
||||||
|
"jsx": "react",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"pretty": true,
|
||||||
|
"noImplicitReturns": false,
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"src/*": [
|
||||||
|
"./src/*"
|
||||||
|
],
|
||||||
|
"webpack/*": [
|
||||||
|
"./webpack/*"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"webpack/**/*",
|
||||||
|
"@types/**/*",
|
||||||
|
"./package.json",
|
||||||
|
"./release.config.js",
|
||||||
|
"webpack/plugins/custom/.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"**/.*/",
|
||||||
|
"build",
|
||||||
|
],
|
||||||
|
}
|
||||||
41
webpack/development.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import webpack from 'webpack';
|
||||||
|
import WebpackDevServer from 'webpack-dev-server';
|
||||||
|
import path from 'path';
|
||||||
|
import { Server } from 'socket.io';
|
||||||
|
import config from './webpack.config';
|
||||||
|
import { version } from '../package.json';
|
||||||
|
import { getManifest } from './manifest.config';
|
||||||
|
import { initializeHotReloading } from './plugins/custom/hotReloadServer';
|
||||||
|
|
||||||
|
const HOT_RELOAD_PORT = 9090;
|
||||||
|
const MODE: Environment = 'development';
|
||||||
|
|
||||||
|
const manifest = getManifest(MODE, version);
|
||||||
|
const compiler = webpack(config(MODE, manifest));
|
||||||
|
|
||||||
|
initializeHotReloading(HOT_RELOAD_PORT, compiler);
|
||||||
|
|
||||||
|
const server = new WebpackDevServer(
|
||||||
|
{
|
||||||
|
https: false,
|
||||||
|
hot: false,
|
||||||
|
client: false,
|
||||||
|
host: 'localhost',
|
||||||
|
static: {
|
||||||
|
directory: path.resolve('build'),
|
||||||
|
},
|
||||||
|
devMiddleware: {
|
||||||
|
writeToDisk: true,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
allowedHosts: 'all',
|
||||||
|
watchFiles: {
|
||||||
|
paths: ['src/**/*.{ts,tsx,js,jsx,html,css,scss,json,md,png,jpg,jpeg,gif,svg}', 'public/**/*'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compiler
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.start();
|
||||||
52
webpack/loaders/index.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { RuleSetRule } from 'webpack';
|
||||||
|
import * as styleLoaders from './styleLoaders';
|
||||||
|
|
||||||
|
/** using esbuild-loader for ⚡ fast builds */
|
||||||
|
const typescriptLoader: RuleSetRule = {
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
loader: 'esbuild-loader',
|
||||||
|
options: {
|
||||||
|
loader: 'tsx',
|
||||||
|
target: 'es2021',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** convert svgs to react components automatically */
|
||||||
|
const svgLoader: RuleSetRule = {
|
||||||
|
test: /\.svg$/,
|
||||||
|
issuer: /\.tsx?$/,
|
||||||
|
loader: '@svgr/webpack',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** these are files that we want to be able to be loaded into the extension folder instead of imported */
|
||||||
|
const urlLoader: RuleSetRule = {
|
||||||
|
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.mp3$/],
|
||||||
|
loader: 'url-loader',
|
||||||
|
options: {
|
||||||
|
limit: '10000',
|
||||||
|
name: 'static/media/[name].[ext]',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** these loaders will allow us to use raw css imports, css modules, raw sass imports, and sass modules */
|
||||||
|
const { cssLoader, cssModuleLoader, sassLoader, sassModuleLoader } = styleLoaders;
|
||||||
|
|
||||||
|
// this is the default file loader, it will be used for any file that doesn't match the other loaders
|
||||||
|
const fileLoader: RuleSetRule = {
|
||||||
|
loader: 'file-loader',
|
||||||
|
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/, /\.mp3$/],
|
||||||
|
options: {
|
||||||
|
name: 'static/media/[name].[ext]',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** the assembled list of loaders in the order that we want webpack to attempt to use them on modules */
|
||||||
|
const loaders: RuleSetRule[] = [
|
||||||
|
typescriptLoader,
|
||||||
|
{
|
||||||
|
// IMPORTANT: if you are adding a new loader, it must come before the file loader
|
||||||
|
oneOf: [svgLoader, urlLoader, cssLoader, cssModuleLoader, sassLoader, sassModuleLoader, fileLoader],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default loaders;
|
||||||
75
webpack/loaders/styleLoaders.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { RuleSetRule, RuleSetUseItem } from 'webpack';
|
||||||
|
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||||
|
import getCSSModuleLocalIdent from 'react-dev-utils/getCSSModuleLocalIdent';
|
||||||
|
|
||||||
|
const cssRegex = /\.css$/;
|
||||||
|
const cssModuleRegex = /\.module\.css$/;
|
||||||
|
|
||||||
|
const sassRegex = /\.(scss|sass)$/;
|
||||||
|
const sassModuleRegex = /\.module\.(scss|sass)$/;
|
||||||
|
|
||||||
|
function buildStyleLoaders(cssLoaderOptions: Record<string, any>): RuleSetUseItem[] {
|
||||||
|
const loaders = [
|
||||||
|
{
|
||||||
|
loader: MiniCssExtractPlugin.loader,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: { ...cssLoaderOptions, sourceMap: false },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return loaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cssLoader: RuleSetRule = {
|
||||||
|
test: cssRegex,
|
||||||
|
exclude: cssModuleRegex,
|
||||||
|
sideEffects: true,
|
||||||
|
use: [
|
||||||
|
...buildStyleLoaders({
|
||||||
|
importLoaders: 1,
|
||||||
|
esModule: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cssModuleLoader: RuleSetRule = {
|
||||||
|
test: cssModuleRegex,
|
||||||
|
use: [
|
||||||
|
...buildStyleLoaders({
|
||||||
|
importLoaders: 1,
|
||||||
|
modules: {
|
||||||
|
getLocalIdent: getCSSModuleLocalIdent,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sassLoader: RuleSetRule = {
|
||||||
|
test: sassRegex,
|
||||||
|
exclude: sassModuleRegex,
|
||||||
|
sideEffects: true,
|
||||||
|
use: [
|
||||||
|
...buildStyleLoaders({
|
||||||
|
importLoaders: 2,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loader: 'sass-loader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sassModuleLoader: RuleSetRule = {
|
||||||
|
test: sassModuleRegex,
|
||||||
|
use: [
|
||||||
|
...buildStyleLoaders({
|
||||||
|
importLoaders: 2,
|
||||||
|
modules: {
|
||||||
|
getLocalIdent: getCSSModuleLocalIdent,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loader: 'sass-loader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
57
webpack/manifest.config.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
const NAME = 'UT Registration Plus';
|
||||||
|
const SHORT_NAME = 'ut-registration-plus';
|
||||||
|
const DESCRIPTION = 'Improves the course registration process at the University of Texas at Austin!';
|
||||||
|
|
||||||
|
const HOST_PERMISSIONS: string[] = [
|
||||||
|
'*://*.utdirect.utexas.edu/apps/registrar/course_schedule/*',
|
||||||
|
'*://*.utexas.collegescheduler.com/*',
|
||||||
|
'*://*.catalog.utexas.edu/ribbit/',
|
||||||
|
'*://*.registrar.utexas.edu/schedules/*',
|
||||||
|
'*://*.login.utexas.edu/login/*',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a chrome extension manifest from the given version, mode, and
|
||||||
|
* @param mode the build mode (development or production)
|
||||||
|
* @param version a chrome extension version (not a semantic version)
|
||||||
|
* @returns a chrome extension manifest
|
||||||
|
*/
|
||||||
|
export function getManifest(mode: Environment, version: string): chrome.runtime.ManifestV3 {
|
||||||
|
let name = mode === 'development' ? `${NAME} (dev)` : NAME;
|
||||||
|
const manifest = {
|
||||||
|
name,
|
||||||
|
short_name: SHORT_NAME,
|
||||||
|
description: DESCRIPTION,
|
||||||
|
version,
|
||||||
|
manifest_version: 3,
|
||||||
|
// hardcode the key for development builds
|
||||||
|
key: process.env.MANIFEST_KEY,
|
||||||
|
host_permissions: HOST_PERMISSIONS,
|
||||||
|
permissions: ['storage', 'unlimitedStorage', 'background'],
|
||||||
|
background: {
|
||||||
|
service_worker: 'static/js/background.js',
|
||||||
|
},
|
||||||
|
content_scripts: [
|
||||||
|
{
|
||||||
|
matches: HOST_PERMISSIONS,
|
||||||
|
css: ['/static/css/content.css'],
|
||||||
|
js: ['/static/js/content.js'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
web_accessible_resources: [
|
||||||
|
{
|
||||||
|
resources: ['static/media/*', '*'],
|
||||||
|
matches: HOST_PERMISSIONS,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
icons: {
|
||||||
|
16: `icons/icon_${mode}_16.png`,
|
||||||
|
48: `icons/icon_${mode}_48.png`,
|
||||||
|
128: `icons/icon_${mode}_128.png`,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
default_popup: 'popup.html',
|
||||||
|
},
|
||||||
|
} satisfies chrome.runtime.ManifestV3;
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
116
webpack/plugins/buildProcessPlugins.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import webpack, { WebpackPluginInstance } from 'webpack';
|
||||||
|
import { EntryId } from 'webpack/webpack.config';
|
||||||
|
import CreateFileWebpack from 'create-file-webpack';
|
||||||
|
import CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||||
|
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
|
||||||
|
import WebpackBuildNotifierPlugin from 'webpack-build-notifier';
|
||||||
|
import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
|
||||||
|
import HTMLWebpackPlugin from 'html-webpack-plugin';
|
||||||
|
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||||
|
import TypeErrorNotifierPlugin from './custom/TypeErrorNotifierPlugin';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the plugins that are used in the build process
|
||||||
|
* @param mode the environment that the build is running in
|
||||||
|
* @param htmlEntries the entry points that need an html file
|
||||||
|
* @param manifest the manifest.json file
|
||||||
|
* @returns an array of webpack plugins
|
||||||
|
*/
|
||||||
|
export function getBuildPlugins(mode: Environment, htmlEntries: EntryId[], manifest: chrome.runtime.ManifestV3) {
|
||||||
|
let plugins: WebpackPluginInstance[] = [];
|
||||||
|
|
||||||
|
// show the progress of the build
|
||||||
|
plugins.push(new webpack.ProgressPlugin());
|
||||||
|
|
||||||
|
// make sure that the paths are case sensitive
|
||||||
|
plugins.push(new CaseSensitivePathsPlugin());
|
||||||
|
|
||||||
|
// specify how the outputed css files should be named
|
||||||
|
plugins.push(
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: 'static/css/[name].css',
|
||||||
|
chunkFilename: 'static/css/[name].chunk.css',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// create an html file for each entry point that needs one
|
||||||
|
for (const entryId of htmlEntries) {
|
||||||
|
// if (!entries[entryId]) return;
|
||||||
|
plugins.push(
|
||||||
|
new HTMLWebpackPlugin({
|
||||||
|
hash: false,
|
||||||
|
filename: `${entryId}.html`,
|
||||||
|
chunks: [entryId],
|
||||||
|
title: `${manifest.short_name} ${entryId} `,
|
||||||
|
template: path.resolve('webpack', 'plugins', 'template.html'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the manifest.json file to the build directory
|
||||||
|
plugins.push(
|
||||||
|
new CreateFileWebpack({
|
||||||
|
path: path.resolve('build'),
|
||||||
|
fileName: 'manifest.json',
|
||||||
|
content: JSON.stringify(manifest, null, 2),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// copy the public directory to the build directory, but only copy the icons for the current mode
|
||||||
|
plugins.push(
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
from: path.resolve('public'),
|
||||||
|
filter: path => (path.includes('icons') ? path.includes(mode) : true),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// run the typescript checker in a separate process
|
||||||
|
plugins.push(
|
||||||
|
new ForkTsCheckerWebpackPlugin({
|
||||||
|
async: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// notify the developer of build events when in development mode
|
||||||
|
if (mode === 'development') {
|
||||||
|
plugins.push(
|
||||||
|
new WebpackBuildNotifierPlugin({
|
||||||
|
title: `${manifest.short_name} v${manifest.version} ${mode}`,
|
||||||
|
logo: path.resolve('public', 'icons', 'icon_production_128.png'),
|
||||||
|
failureSound: 'Ping',
|
||||||
|
showDuration: true,
|
||||||
|
suppressWarning: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// notify the developer of type errors
|
||||||
|
plugins.push(new TypeErrorNotifierPlugin());
|
||||||
|
|
||||||
|
// define the environment variables that are available within the extension code
|
||||||
|
plugins.push(
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': JSON.stringify({
|
||||||
|
SEMANTIC_VERSION: process.env.SEMANTIC_VERSION,
|
||||||
|
NODE_ENV: mode,
|
||||||
|
...dotenv.config({ path: `.env.${mode}` }).parsed,
|
||||||
|
} satisfies typeof process.env),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// provide some global nodejs variables so that nodejs libraries can be used
|
||||||
|
plugins.push(
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
Buffer: ['buffer', 'Buffer'],
|
||||||
|
process: 'process/browser',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
84
webpack/plugins/custom/TypeErrorNotifierPlugin.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { Compiler } from 'webpack';
|
||||||
|
import path from 'path';
|
||||||
|
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
|
||||||
|
import WebpackBuildNotifierPlugin from 'webpack-build-notifier';
|
||||||
|
import { Issue, IssueLocation } from 'fork-ts-checker-webpack-plugin/lib/issue';
|
||||||
|
|
||||||
|
interface Resource {
|
||||||
|
path: string;
|
||||||
|
location: IssueLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This plugin hooks into the fork-ts-checker-webpack-plugin and
|
||||||
|
* notifies the developer of type errors using the webpack-build-notifier plugin.
|
||||||
|
*/
|
||||||
|
export default class TypeErrorNotifierPlugin {
|
||||||
|
apply(compiler: Compiler) {
|
||||||
|
// hook into the fork-ts-checker-webpack-plugin
|
||||||
|
const hooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(compiler);
|
||||||
|
hooks.issues.tap('MyPlugin', issues => {
|
||||||
|
const errors = issues.filter(issue => issue.severity === 'error');
|
||||||
|
if (!errors?.[0]?.message) {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
let error = errors[0];
|
||||||
|
let resource = getErrorResource(error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
notifyTypeError(resource, error.message, errors);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyTypeError(resource: Resource, message: string, errors: Issue[]) {
|
||||||
|
const { line, column } = resource.location.start;
|
||||||
|
|
||||||
|
const buildNotifier = new WebpackBuildNotifierPlugin({
|
||||||
|
logo: path.resolve('public', 'icons', 'icon_production_128.png'),
|
||||||
|
compilationSound: 'Pop',
|
||||||
|
failureSound: 'Sosumi',
|
||||||
|
title: `TS: ${errors.length} errors`,
|
||||||
|
notifyOptions: {
|
||||||
|
open: `vscode://file/${resource.path}:${line}:${column}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fakeInput = {
|
||||||
|
hasErrors: () => true,
|
||||||
|
compilation: {
|
||||||
|
children: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message,
|
||||||
|
module: {
|
||||||
|
resource: resource.path,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// @ts-ignore - private method
|
||||||
|
buildNotifier.onCompilationDone(fakeInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorResource(error: Issue): Resource {
|
||||||
|
return {
|
||||||
|
path: error.file ?? '',
|
||||||
|
location: error.location ?? {
|
||||||
|
end: {
|
||||||
|
column: 0,
|
||||||
|
line: 0,
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
column: 0,
|
||||||
|
line: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
36
webpack/plugins/custom/hotReloadServer.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Server } from 'socket.io';
|
||||||
|
import { Compiler } from 'webpack';
|
||||||
|
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
|
||||||
|
|
||||||
|
// Name of the plugin
|
||||||
|
const PLUGIN_NAME = 'HotReloadServer';
|
||||||
|
|
||||||
|
// How long to wait before reloading the browser after a successful build
|
||||||
|
const RELOAD_LOCKOUT = 2000;
|
||||||
|
|
||||||
|
// we want to cache all the "reload requests" here so we don't have to keep re-compiling the app while typing
|
||||||
|
const reloads: NodeJS.Timeout[] = [];
|
||||||
|
|
||||||
|
let io: Server;
|
||||||
|
|
||||||
|
export function initializeHotReloading(port: number, compiler: Compiler) {
|
||||||
|
io = new Server(port);
|
||||||
|
|
||||||
|
const hooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(compiler);
|
||||||
|
hooks.issues.tap(PLUGIN_NAME, (issues, compilation) => {
|
||||||
|
const typeErrors = issues.filter(issue => issue.severity === 'error');
|
||||||
|
const buildErrors = compilation?.errors ?? [];
|
||||||
|
// if no errors (thus successful build), lets queue up a reload for the browser
|
||||||
|
if (typeErrors.length === 0 && buildErrors.length === 0) {
|
||||||
|
reloads.push(setTimeout(() => io.emit('reload'), RELOAD_LOCKOUT));
|
||||||
|
}
|
||||||
|
return typeErrors;
|
||||||
|
});
|
||||||
|
|
||||||
|
// if a recompile is triggered, we want to clear out all the queue'd reloads
|
||||||
|
// (so we don't spam-reload the extension while we are still typing
|
||||||
|
compiler.hooks.compile.tap(PLUGIN_NAME, () => {
|
||||||
|
reloads.forEach(reload => clearTimeout(reload));
|
||||||
|
reloads.length = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
12
webpack/plugins/moduleResolutionPlugins.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
|
||||||
|
import path from 'path';
|
||||||
|
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
|
||||||
|
|
||||||
|
export const moduleResolutionPlugins = [
|
||||||
|
// this will make sure that webpack uses the tsconfig path aliases
|
||||||
|
new TsconfigPathsPlugin({
|
||||||
|
configFile: path.resolve('src', 'tsconfig.json'),
|
||||||
|
}),
|
||||||
|
// this will make sure that we don't import anything outside of the src directory from the src directory
|
||||||
|
new ModuleScopePlugin(path.resolve('src'), path.resolve('package.json')),
|
||||||
|
];
|
||||||
32
webpack/plugins/template.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
--></body>
|
||||||
|
</html>
|
||||||
46
webpack/production.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import webpack from 'webpack';
|
||||||
|
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
|
||||||
|
import config from './webpack.config';
|
||||||
|
import { info, success } from './utils/chalk';
|
||||||
|
import { getManifest } from './manifest.config';
|
||||||
|
import { version } from '../package.json';
|
||||||
|
import { convertSemver } from './utils/convertSemver';
|
||||||
|
import printError from './utils/printError';
|
||||||
|
import { zipProductionBuild } from './utils/zipProductionBuild';
|
||||||
|
|
||||||
|
const MODE: Environment = 'production';
|
||||||
|
|
||||||
|
// generate the manifest.json file
|
||||||
|
const semanticVersion = process.env.SEMANTIC_VERSION || version;
|
||||||
|
const manifestVersion = convertSemver(semanticVersion);
|
||||||
|
const manifest = getManifest(MODE, manifestVersion);
|
||||||
|
|
||||||
|
console.log(info(`${manifest.short_name} v${manifest.version} ${MODE} build starting...`));
|
||||||
|
|
||||||
|
// kick off the webpack build
|
||||||
|
webpack(config(MODE, manifest), async error => {
|
||||||
|
if (!error) {
|
||||||
|
await onBuildSuccess();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
await onBuildFailure(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onBuildSuccess(): Promise<void> {
|
||||||
|
// zip the output directory and put it in the artifacts directory
|
||||||
|
const fileName = `${manifest.short_name} v${manifestVersion}`;
|
||||||
|
await zipProductionBuild(fileName);
|
||||||
|
console.log(success(`${fileName} built and zipped into build/artifacts/${fileName}.zip!`));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBuildFailure(error: Error): void {
|
||||||
|
if (!error.message) {
|
||||||
|
return printError(error);
|
||||||
|
}
|
||||||
|
const messages = formatWebpackMessages({ errors: [error.message], warnings: [] });
|
||||||
|
if (messages.errors.length > 1) {
|
||||||
|
messages.errors.length = 1;
|
||||||
|
}
|
||||||
|
return printError(new Error(messages.errors.join('\n\n')));
|
||||||
|
}
|
||||||
52
webpack/release.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { simpleGit } from 'simple-git';
|
||||||
|
import prompts from 'prompts';
|
||||||
|
import { error, info } from './utils/chalk';
|
||||||
|
import { getSourceRef } from './utils/git/getSourceRef';
|
||||||
|
|
||||||
|
const git = simpleGit();
|
||||||
|
const status = await git.status();
|
||||||
|
|
||||||
|
if (status.files.length) {
|
||||||
|
console.log(error('Working directory is not clean, please commit or stash changes before releasing.'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { destinationBranch } = await prompts({
|
||||||
|
type: 'select',
|
||||||
|
name: 'destinationBranch',
|
||||||
|
message: 'What kind of release do you want to create?',
|
||||||
|
choices: ['preview', 'production'].map(releaseType => ({
|
||||||
|
title: releaseType,
|
||||||
|
value: releaseType,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
const sourceRef = await getSourceRef(destinationBranch);
|
||||||
|
|
||||||
|
const { confirm } = await prompts({
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'confirm',
|
||||||
|
message: `Are you sure you want to create a ${destinationBranch} release from ${sourceRef}?`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirm) {
|
||||||
|
console.log(error('Aborting release.'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we fetch the latest changes from the remote
|
||||||
|
await git.fetch();
|
||||||
|
|
||||||
|
// we checkout the source ref, pull the latest changes and then checkout the destination branch
|
||||||
|
console.info(`Checking out and updating ${sourceRef}...`);
|
||||||
|
await git.checkout(sourceRef);
|
||||||
|
await git.pull('origin', sourceRef);
|
||||||
|
console.info(`Checking out and updating ${destinationBranch}...`);
|
||||||
|
await git.checkout(destinationBranch);
|
||||||
|
await git.pull('origin', destinationBranch);
|
||||||
|
|
||||||
|
// we trigger the release github action by merging the source ref into the destination branch
|
||||||
|
console.info(`Merging ${sourceRef} into ${destinationBranch}...`);
|
||||||
|
await git.merge([sourceRef, '--ff-only']);
|
||||||
|
await git.push('origin', destinationBranch);
|
||||||
|
|
||||||
|
console.info(info(`Release to ${destinationBranch} created! Check github for status`));
|
||||||
15
webpack/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "esnext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"composite": true,
|
||||||
|
"lib": [
|
||||||
|
"es2021"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"chrome",
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
7
webpack/utils/chalk.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
export const error = chalk.bold.red;
|
||||||
|
export const { bold } = chalk;
|
||||||
|
export const info = chalk.bgHex('#673AB7').rgb(255, 255, 255);
|
||||||
|
export const warning = chalk.bgHex('#FF9800').rgb(255, 255, 255);
|
||||||
|
export const success = chalk.bgHex('#4CAF50').rgb(255, 255, 255);
|
||||||
22
webpack/utils/convertSemver.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { parse } from 'semver';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts npm semver-style strings (including pre-releases) to a release version compatible
|
||||||
|
* with the extension stores.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* semverVersionTo('1.0.0-beta.1`) returns 1.0.0.100
|
||||||
|
*/
|
||||||
|
export function convertSemver(version: string): string {
|
||||||
|
const semver = parse(version);
|
||||||
|
if (!semver) {
|
||||||
|
throw new Error(`Couldn't parse ${version}!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { major, minor, patch, prerelease } = semver;
|
||||||
|
let manifestVersion = `${major}.${minor}.${patch}`;
|
||||||
|
if (prerelease.length) {
|
||||||
|
manifestVersion += `.${prerelease[1]}00`;
|
||||||
|
}
|
||||||
|
return manifestVersion;
|
||||||
|
}
|
||||||
28
webpack/utils/git/getSourceRef.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import prompts from 'prompts';
|
||||||
|
import { simpleGit } from 'simple-git';
|
||||||
|
import { error } from '../chalk';
|
||||||
|
|
||||||
|
const git = simpleGit();
|
||||||
|
|
||||||
|
export async function getSourceRef(destinationBranch: 'preview' | 'production'): Promise<string> {
|
||||||
|
if (destinationBranch === 'preview') {
|
||||||
|
return 'main';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = await git.tags(['--sort=-committerdate']);
|
||||||
|
const alphaTags = tags.all.filter((tag: string) => tag.includes('alpha'));
|
||||||
|
|
||||||
|
if (!alphaTags.length) {
|
||||||
|
console.log(error('No preview builds found, please create one before releasing a production build.'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sourceTag } = await prompts({
|
||||||
|
message: 'Which preview tag do you want to create a production build from?',
|
||||||
|
type: 'select',
|
||||||
|
name: 'sourceTag',
|
||||||
|
choices: alphaTags.map(tag => ({ title: tag, value: tag })),
|
||||||
|
});
|
||||||
|
|
||||||
|
return sourceTag;
|
||||||
|
}
|
||||||
16
webpack/utils/printError.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import printBuildError from 'react-dev-utils/printBuildError';
|
||||||
|
import { error } from './chalk';
|
||||||
|
/**
|
||||||
|
* Print Errors that we got back from webpack
|
||||||
|
* @param e the error provided by webpacxk
|
||||||
|
*/
|
||||||
|
export default function printError(e: Error) {
|
||||||
|
console.log('printBuildError -> e', e);
|
||||||
|
if (process.env.TSC_COMPILE_ON_ERROR === 'true') {
|
||||||
|
printBuildError(e);
|
||||||
|
} else {
|
||||||
|
// console.log(error('Failed to compile.\n'));
|
||||||
|
printBuildError(e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
webpack/utils/zipProductionBuild.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import fs, { mkdirSync } from 'fs';
|
||||||
|
import archiver from 'archiver';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a zip file from the given source directory
|
||||||
|
* @param fileName the name of the zip file to create
|
||||||
|
* @param outDir the directory to zip up
|
||||||
|
* @param globOptions the glob options to use when finding files to zip
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function zipProductionBuild(fileName: string) {
|
||||||
|
const outDirectory = path.resolve('build');
|
||||||
|
const artifactsDir = path.join(outDirectory, 'artifacts');
|
||||||
|
|
||||||
|
mkdirSync(artifactsDir, { recursive: true });
|
||||||
|
|
||||||
|
const output = fs.createWriteStream(`${artifactsDir}/${fileName}.zip`);
|
||||||
|
const archive = archiver('zip', {
|
||||||
|
zlib: { level: 9 },
|
||||||
|
});
|
||||||
|
archive.pipe(output);
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
output.on('close', resolve);
|
||||||
|
archive.on('warning', warn => console.log(chalk.red(warn)));
|
||||||
|
archive.on('error', err => reject(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
archive.glob('**/*', { cwd: outDirectory, ignore: ['*.zip', 'artifacts'] });
|
||||||
|
// eslint-disable-next-line no-void
|
||||||
|
void archive.finalize(); // The promise returned is what's `await-ed`, not the call to `finalize()`
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
119
webpack/webpack.config.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { Configuration, EntryObject } from 'webpack';
|
||||||
|
import path from 'path';
|
||||||
|
import TerserPlugin from 'terser-webpack-plugin';
|
||||||
|
import { moduleResolutionPlugins } from './plugins/moduleResolutionPlugins';
|
||||||
|
import loaders from './loaders';
|
||||||
|
import { getBuildPlugins } from './plugins/buildProcessPlugins';
|
||||||
|
|
||||||
|
export interface Entries {
|
||||||
|
content: string[];
|
||||||
|
background: string[];
|
||||||
|
popup: string[];
|
||||||
|
// only used in development
|
||||||
|
debug?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EntryId = keyof Entries;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will generate the webpack configuration for the extension
|
||||||
|
* @param mode the mode that webpack is running in (development or production)
|
||||||
|
* * @param manifest the manifest.json object for the extension
|
||||||
|
* @returns the webpack configuration
|
||||||
|
*/
|
||||||
|
export default function config(mode: Environment, manifest: chrome.runtime.ManifestV3): Configuration {
|
||||||
|
const outDirectory = path.resolve('build');
|
||||||
|
|
||||||
|
// the entry points for the extension (the files that webpack will start bundling from)
|
||||||
|
const entry: Entries = {
|
||||||
|
content: [path.resolve('src', 'views', 'content', 'content')],
|
||||||
|
background: [path.resolve('src', 'background', 'background')],
|
||||||
|
popup: [path.resolve('src', 'views', 'popup', 'popup')],
|
||||||
|
};
|
||||||
|
|
||||||
|
// the entries that need an html file to be generated
|
||||||
|
const htmlEntries: EntryId[] = mode === 'development' ? ['popup', 'debug'] : ['popup'];
|
||||||
|
|
||||||
|
if (mode === 'development') {
|
||||||
|
// TODO: add hot reloading script to the debug entry
|
||||||
|
entry.debug = [path.resolve('src', 'debug')];
|
||||||
|
|
||||||
|
// we need to import react-devtools before the react code in development
|
||||||
|
entry.content = [path.resolve('src', 'views', 'reactDevtools'), ...entry.content];
|
||||||
|
entry.popup = [path.resolve('src', 'views', 'reactDevtools'), ...entry.popup];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @see https://webpack.js.org/configuration for documentation */
|
||||||
|
const config: Configuration = {
|
||||||
|
mode,
|
||||||
|
devtool: mode === 'development' ? 'cheap-module-source-map' : undefined,
|
||||||
|
bail: true,
|
||||||
|
cache: true,
|
||||||
|
// entry and resolve is what webpack uses for figuring out where to start bundling and how to resolve modules
|
||||||
|
entry: entry as unknown as EntryObject,
|
||||||
|
resolve: {
|
||||||
|
modules: ['node_modules'],
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||||
|
plugins: moduleResolutionPlugins,
|
||||||
|
// this is to polyfill some node-only modules
|
||||||
|
fallback: {
|
||||||
|
crypto: 'crypto-browserify',
|
||||||
|
stream: 'stream-browserify',
|
||||||
|
buffer: 'buffer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// this is where we define the loaders for different file types
|
||||||
|
module: {
|
||||||
|
strictExportPresence: true,
|
||||||
|
rules: loaders,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
clean: true,
|
||||||
|
path: outDirectory,
|
||||||
|
pathinfo: mode === 'development',
|
||||||
|
filename: 'static/js/[name].js',
|
||||||
|
publicPath: '/',
|
||||||
|
// this is for windows support (which uses backslashes in paths)
|
||||||
|
devtoolModuleFilenameTemplate: info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
|
||||||
|
// this is to make sure that the global chunk loading function name is unique
|
||||||
|
chunkLoadingGlobal: `webpackJsonp${manifest.short_name}`,
|
||||||
|
globalObject: 'this',
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
errorDetails: true,
|
||||||
|
errorsCount: true,
|
||||||
|
},
|
||||||
|
// this is where we define the plugins that webpack will use
|
||||||
|
plugins: getBuildPlugins(mode, htmlEntries, manifest),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mode === 'production') {
|
||||||
|
config.optimization = {
|
||||||
|
minimize: true,
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin({
|
||||||
|
extractComments: false,
|
||||||
|
parallel: false,
|
||||||
|
terserOptions: {
|
||||||
|
compress: {
|
||||||
|
ecma: 2020,
|
||||||
|
drop_console: true,
|
||||||
|
drop_debugger: true,
|
||||||
|
comparisons: false,
|
||||||
|
inline: 2,
|
||||||
|
},
|
||||||
|
keep_classnames: false,
|
||||||
|
keep_fnames: false,
|
||||||
|
output: {
|
||||||
|
ecma: 2020,
|
||||||
|
comments: false,
|
||||||
|
ascii_only: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||