├── .nvmrc ├── .prettierignore ├── typings.d.ts ├── src ├── components │ ├── hello │ │ ├── index.ts │ │ ├── __tests__ │ │ │ ├── test_hello.tsx │ │ │ └── __snapshots__ │ │ │ │ └── test_hello.tsx.snap │ │ ├── component.tsx │ │ └── stories │ │ │ └── Hello.stories.tsx │ └── scroller │ │ ├── index.ts │ │ ├── __tests__ │ │ ├── test_scroller.tsx │ │ └── __snapshots__ │ │ │ └── test_scroller.tsx.snap │ │ ├── styles.module.css │ │ ├── component.tsx │ │ └── stories │ │ └── Scroller.stories.tsx ├── popup │ ├── styles.module.css │ ├── __tests__ │ │ ├── test_popup.tsx │ │ └── __snapshots__ │ │ │ └── test_popup.tsx.snap │ ├── stories │ │ └── Popup.stories.tsx │ ├── index.tsx │ └── component.tsx ├── css │ └── app.css ├── backgroundPage.ts └── __mocks__ │ └── webextension-polyfill.ts ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .storybook ├── preview.js ├── tsconfig.json └── main.js ├── .vscode ├── settings.json └── extensions.json ├── dist ├── icon-16.png ├── icon-48.png ├── icon-128.png ├── popup.html └── manifest.json ├── .gitignore ├── postcss.config.js ├── .prettierrc.js ├── babel.config.js ├── webpack.prod.js ├── tailwind.config.js ├── webpack.dev.js ├── tsconfig.json ├── CHANGELOG.md ├── LICENSE ├── .eslintrc.js ├── webpack.common.js ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── package.json ├── jest.config.js └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.13.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | *.snap -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.module.css"; 2 | -------------------------------------------------------------------------------- /src/components/hello/index.ts: -------------------------------------------------------------------------------- 1 | export { Hello } from "./component"; 2 | -------------------------------------------------------------------------------- /src/components/scroller/index.ts: -------------------------------------------------------------------------------- 1 | export { Scroller } from "./component"; 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: aeksco 3 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | // Import global app.css file 2 | import "../src/css/app.css"; 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "css.lint.unknownAtRules": "ignore" 4 | } -------------------------------------------------------------------------------- /dist/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeksco/react-typescript-web-extension-starter/HEAD/dist/icon-16.png -------------------------------------------------------------------------------- /dist/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeksco/react-typescript-web-extension-starter/HEAD/dist/icon-48.png -------------------------------------------------------------------------------- /dist/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeksco/react-typescript-web-extension-starter/HEAD/dist/icon-128.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/js/* 2 | storybook-static/* 3 | 4 | /node_modules 5 | npm-debug.log 6 | yarn-error.log 7 | 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /src/popup/styles.module.css: -------------------------------------------------------------------------------- 1 | .popupContainer { 2 | min-width: 300px; 3 | min-height: 300px; 4 | display: flex; 5 | } 6 | -------------------------------------------------------------------------------- /src/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | @apply bg-gray-200; 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: false, 5 | tabWidth: 4, 6 | }; 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "production", 6 | }); 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ["./src/**/*.{js,jsx,ts,tsx}", "./dist/popup.html"], 3 | content: ["./src/**/*.{html,js,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | } -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "development", 6 | devtool: "inline-source-map", 7 | }); 8 | -------------------------------------------------------------------------------- /dist/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chrome Extension (built with TypeScript + React) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/popup/__tests__/test_popup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Popup } from "../component"; 3 | import renderer from "react-test-renderer"; 4 | 5 | it("component renders", () => { 6 | const tree = renderer.create().toJSON(); 7 | expect(tree).toMatchSnapshot(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/hello/__tests__/test_hello.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Hello } from "../component"; 3 | import renderer from "react-test-renderer"; 4 | 5 | it("component renders", () => { 6 | const tree = renderer.create().toJSON(); 7 | expect(tree).toMatchSnapshot(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/hello/component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // // // // 4 | 5 | export function Hello() { 6 | return ( 7 |
8 |
9 |

Example Extension

10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/popup/stories/Popup.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Popup } from "../component"; 3 | import { ComponentMeta } from "@storybook/react"; 4 | 5 | // // // // 6 | 7 | export default { 8 | title: "Components/Popup", 9 | component: Popup, 10 | } as ComponentMeta; 11 | 12 | export const Render = () => ; 13 | -------------------------------------------------------------------------------- /src/components/hello/stories/Hello.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Hello } from "../component"; 3 | import { ComponentMeta } from "@storybook/react"; 4 | 5 | // // // // 6 | 7 | export default { 8 | title: "Components/Hello", 9 | component: Hello, 10 | } as ComponentMeta; 11 | 12 | export const Render = () => ; 13 | -------------------------------------------------------------------------------- /src/components/hello/__tests__/__snapshots__/test_hello.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`component renders 1`] = ` 4 |
7 |
10 |

13 | Example Extension 14 |

15 |
16 |
17 | `; 18 | -------------------------------------------------------------------------------- /src/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import browser from "webextension-polyfill"; 4 | import { Popup } from "./component"; 5 | import "../css/app.css"; 6 | 7 | // // // // 8 | 9 | browser.tabs.query({ active: true, currentWindow: true }).then(() => { 10 | ReactDOM.render(, document.getElementById("popup")); 11 | }); 12 | -------------------------------------------------------------------------------- /src/backgroundPage.ts: -------------------------------------------------------------------------------- 1 | import browser from "webextension-polyfill"; 2 | 3 | // Listen for messages sent from other parts of the extension 4 | browser.runtime.onMessage.addListener((request: { popupMounted: boolean }) => { 5 | // Log statement if request.popupMounted is true 6 | // NOTE: this request is sent in `popup/component.tsx` 7 | if (request.popupMounted) { 8 | console.log("backgroundPage notified that Popup.tsx has mounted."); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/scroller/__tests__/test_scroller.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Scroller } from "../component"; 3 | import renderer from "react-test-renderer"; 4 | 5 | it("component renders", () => { 6 | const tree = renderer 7 | .create( 8 | , 12 | ) 13 | .toJSON(); 14 | expect(tree).toMatchSnapshot(); 15 | }); 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "esbenp.prettier-vscode", 7 | "bradlc.vscode-tailwindcss" 8 | ], 9 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 10 | "unwantedRecommendations": [] 11 | } -------------------------------------------------------------------------------- /src/components/scroller/__tests__/__snapshots__/test_scroller.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`component renders 1`] = ` 4 |
7 | 14 | 21 |
22 | `; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@src/*": ["src/*"] 6 | }, 7 | "target": "es5", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "rootDir": "./src", 11 | "outDir": "dist/js", 12 | "strict": true, 13 | "sourceMap": true, 14 | "jsx": "react", 15 | "esModuleInterop": true, 16 | "lib": ["es2015", "dom"], 17 | "experimentalDecorators": true, 18 | "allowSyntheticDefaultImports": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/scroller/styles.module.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | @apply inline-flex; 3 | @apply items-center; 4 | @apply justify-center; 5 | @apply px-4; 6 | @apply py-2; 7 | @apply border; 8 | @apply border-transparent; 9 | @apply text-sm; 10 | @apply font-medium; 11 | @apply whitespace-nowrap; 12 | @apply rounded-md; 13 | @apply shadow-sm; 14 | @apply text-white; 15 | @apply bg-indigo-600; 16 | @apply hover:bg-indigo-700; 17 | @apply focus:outline-none; 18 | @apply focus:ring-2; 19 | @apply focus:ring-offset-2; 20 | @apply focus:ring-indigo-500; 21 | } 22 | -------------------------------------------------------------------------------- /dist/manifest.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "manifest_version": 3, 4 | "name": "Chrome Extension Starter", 5 | "description": "A Chrome Extension starter kit", 6 | "version": "1.0.0", 7 | "action": { 8 | "default_icon": { 9 | "16": "icon-16.png", 10 | "48": "icon-48.png", 11 | "128": "icon-128.png" 12 | }, 13 | "default_popup": "popup.html" 14 | }, 15 | "background": { 16 | "service_worker": "js/backgroundPage.js" 17 | }, 18 | "icons": { 19 | "16": "icon-16.png", 20 | "48": "icon-48.png", 21 | "128": "icon-128.png" 22 | }, 23 | "host_permissions": ["https:\/\/*/*"], 24 | "permissions": ["tabs", "activeTab", "notifications", "scripting"] 25 | } 26 | 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v2.0.0 2 | 3 | - Bump `version` field in `package.json` to `2.0.0` 4 | - Upgrade `react` + `react-dom` to version `17.x` 5 | - Upgrade Storybook to version `6.4` 6 | - Upgrade `webpack` to `5.x` 7 | - Storybook stories written in Component Story Format 8 | - Add Tailwind@3.x with CSS modules 9 | - Removed Bootstrap and SCSS 10 | - Add `CONTRIBUTING.md` 11 | - Add `CHANGELOG.md` 12 | - Add `CODE_OF_CONDUCT.md` 13 | - Add `.github/PULL_REQUEST_TEMPLATE.md` 14 | - Add `.github/ISSUE_TEMPLATE.md` 15 | - Simplify Storybook tooling 16 | - Updated placeholder icon for extension 17 | - Verified support for Microsoft Edge 18 | - Updated `README.md` screenshots 19 | - Add `.vscode` directory with workplace settings and recommended extensions 20 | -------------------------------------------------------------------------------- /src/components/scroller/component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import css from "./styles.module.css"; 3 | 4 | // // // // 5 | 6 | /** 7 | * Component that renders buttons to scroll to the top and bottom of the page 8 | */ 9 | export function Scroller(props: { 10 | onClickScrollTop: () => void; 11 | onClickScrollBottom: () => void; 12 | }) { 13 | return ( 14 |
15 | 22 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/__mocks__/webextension-polyfill.ts: -------------------------------------------------------------------------------- 1 | // src/__mocks__/webextension-polyfill 2 | // Update this file to include any mocks for the `webextension-polyfill` package 3 | // This is used to mock these values for Storybook so you can develop your components 4 | // outside the Web Extension environment provided by a compatible browser 5 | // See .storybook/main.js to see how this module is swapped in for `webextension-polyfill` 6 | const browser: any = { 7 | tabs: { 8 | executeScript(currentTabId: number, details: any) { 9 | return Promise.resolve({ done: true }); 10 | }, 11 | query(params: any): Promise { 12 | return Promise.resolve([]); 13 | }, 14 | }, 15 | runtime: { 16 | sendMessage: (params: { popupMounted: boolean }) => { 17 | return; 18 | }, 19 | }, 20 | }; 21 | export default browser; 22 | 23 | interface Tab { 24 | id: number; 25 | } 26 | 27 | export interface Tabs { 28 | Tab: Tab; 29 | } 30 | -------------------------------------------------------------------------------- /.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "module": "es2015", 5 | "target": "es5", 6 | "lib": ["es6", "dom"], 7 | "sourceMap": true, 8 | "allowJs": false, 9 | "jsx": "react", 10 | "moduleResolution": "node", 11 | "rootDir": "../", 12 | "baseUrl": "../", 13 | "paths": { 14 | "@src/*": ["src/*"] 15 | }, 16 | "outDir": "dist", 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noImplicitAny": true, 20 | "strictNullChecks": true, 21 | "declaration": true 22 | }, 23 | "include": ["../src/**/*"], 24 | "exclude": [ 25 | "node_modules", 26 | "build", 27 | "dist", 28 | "scripts", 29 | "acceptance-tests", 30 | "webpack", 31 | "jest", 32 | "src/setupTests.ts", 33 | "**/*/*.test.ts", 34 | "examples" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/popup/__tests__/__snapshots__/test_popup.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`component renders 1`] = ` 4 |
7 |
10 |
13 |
16 |

19 | Example Extension 20 |

21 |
22 |
23 |
24 |
27 | 34 | 41 |
42 |
43 |
44 | `; 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alexander Schwartzberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | Please answer the following questions for yourself before submitting an issue: 4 | 5 | - [ ] I am running the latest version 6 | - [ ] I checked the documentation and found no answer 7 | - [ ] I checked to make sure that this issue has not already been filed 8 | - [ ] I'm reporting the issue to the correct repository (for multi-repository projects) 9 | 10 | ## Context 11 | 12 | Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions. 13 | 14 | ## Expected Behavior 15 | 16 | If relevant, please describe the behavior you are expecting 17 | 18 | ## Current Behavior 19 | 20 | If relevant, describe the current behavior 21 | 22 | ## Failure Information (for bugs) 23 | 24 | Please help provide information about the failure if this is a bug. If it is not a bug, please remove the rest of this template. 25 | 26 | ### Steps to Reproduce 27 | 28 | Please provide detailed steps for reproducing the issue. 29 | 30 | 1. step 1 31 | 2. step 2 32 | 3. you get it... 33 | 34 | ### Failure Logs 35 | 36 | Please include any relevant log snippets or files here. 37 | -------------------------------------------------------------------------------- /src/components/scroller/stories/Scroller.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Scroller } from "../component"; 3 | import { ComponentMeta, ComponentStory } from "@storybook/react"; 4 | import { within, userEvent } from "@storybook/testing-library"; 5 | import { action } from "@storybook/addon-actions"; 6 | 7 | // // // // 8 | 9 | export default { 10 | title: "Components/Scroller", 11 | component: Scroller, 12 | args: { 13 | onClickScrollTop: action("click-scroll-top"), 14 | onClickScrollBottom: action("click-scroll-bottom"), 15 | }, 16 | } as ComponentMeta; 17 | 18 | const Template: ComponentStory = (args) => ( 19 | 20 | ); 21 | 22 | // // // // 23 | 24 | export const Render = Template.bind({}); 25 | 26 | export const ScrollTop = Template.bind({}); 27 | ScrollTop.play = async ({ canvasElement }) => { 28 | const canvas = within(canvasElement); 29 | await userEvent.click(canvas.getByTestId("scroll-to-top")); 30 | }; 31 | 32 | export const ScrollBottom = Template.bind({}); 33 | ScrollBottom.play = async ({ canvasElement }) => { 34 | const canvas = within(canvasElement); 35 | await userEvent.click(canvas.getByTestId("scroll-to-bottom")); 36 | }; 37 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from @typescript-eslint/eslint-plugin 5 | 'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react 6 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 7 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 11 | sourceType: 'module', // Allows for the use of imports 12 | ecmaFeatures: { 13 | jsx: true, // Allows for the parsing of JSX 14 | }, 15 | }, 16 | rules: { 17 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 18 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 19 | }, 20 | settings: { 21 | react: { 22 | version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | ## How Has This Been Tested? 17 | 18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 19 | 20 | ## Checklist: 21 | 22 | - [ ] My code follows the style guidelines of this project 23 | - [ ] I have performed a self-review of my own code 24 | - [ ] I have commented my code, particularly in hard-to-understand areas 25 | - [ ] I have made corresponding changes to the documentation 26 | - [ ] My changes generate no new warnings 27 | - [ ] I have added tests that prove my fix is effective or that my feature works 28 | - [ ] New and existing unit tests pass locally with my changes 29 | - [ ] Any dependent changes have been merged and published in downstream modules 30 | - [ ] I have checked my code and corrected any misspellings 31 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: { 5 | backgroundPage: path.join(__dirname, "src/backgroundPage.ts"), 6 | popup: path.join(__dirname, "src/popup/index.tsx"), 7 | }, 8 | output: { 9 | path: path.join(__dirname, "dist/js"), 10 | filename: "[name].js", 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | exclude: /node_modules/, 16 | test: /\.tsx?$/, 17 | use: "ts-loader", 18 | }, 19 | // Treat src/css/app.css as a global stylesheet 20 | { 21 | test: /\app.css$/, 22 | use: [ 23 | "style-loader", 24 | "css-loader", 25 | "postcss-loader", 26 | ], 27 | }, 28 | // Load .module.css files as CSS modules 29 | { 30 | test: /\.module.css$/, 31 | use: [ 32 | "style-loader", 33 | { 34 | loader: "css-loader", 35 | options: { 36 | modules: true, 37 | }, 38 | }, 39 | "postcss-loader", 40 | ], 41 | }, 42 | ], 43 | }, 44 | // Setup @src path resolution for TypeScript files 45 | resolve: { 46 | extensions: [".ts", ".tsx", ".js"], 47 | alias: { 48 | "@src": path.resolve(__dirname, "src/"), 49 | }, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Welcome and thanks for stopping by! There are many ways to contribute, including submitting bug reports, improving documentation, submitting feature requests, reviewing new submissions, or contributing code that can be incorporated into the project. 4 | 5 | **Table of Contents:** 6 | 7 | 1. [Code of Conduct](#code-of-conduct) 8 | 2. [Questions](#questions) 9 | 3. [Feature Requests](#feature-requests) 10 | 4. [Reporting Bugs](#reporting-bugs) 11 | 5. [Contributing Code](#contributing-code) 12 | 13 | ## Code of Conduct 14 | 15 | By participating in this project, you agree to abide by our [Code of Conduct][0]. 16 | 17 | ## Questions 18 | 19 | Please open a GitHub issue if you have any questions about the project. 20 | 21 | ## Feature Requests 22 | 23 | Please request new features by opening a GitHub issue. 24 | 25 | ## Reporting Bugs 26 | 27 | **If you find a security vulnerability, do NOT open an issue. Email aeksco@gmail.COM instead.** 28 | 29 | Please check open issues before opening a new ticket. Also, provide any references to FAQs or debugging guides that you might have. 30 | 31 | ## Contributing Code 32 | 33 | Unsure where to begin contributing to this project? You can start by looking through open `help-wanted` issues! 34 | 35 | Working on your first open source project or pull request? Here are some helpful tutorials: 36 | 37 | - [How to Contribute to an Open Source Project on GitHub][1] 38 | - [Make a Pull Request][2] 39 | - [First Timers Only][3] 40 | 41 | [0]: CODE_OF_CONDUCT.md 42 | [1]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 43 | [2]: http://makeapullrequest.com/ 44 | [3]: http://www.firsttimersonly.com 45 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All participants of `react-typescript-web-extension-starter` are expected to abide by our Code of Conduct, both online and during in-person events that are hosted and/or associated with `react-typescript-web-extension-starter`. 4 | 5 | ## The Pledge 6 | 7 | In the interest of fostering an open and welcoming environment, we pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 8 | 9 | ## The Standards 10 | 11 | Examples of behaviour that contributes to creating a positive environment include: 12 | 13 | - Using welcoming and inclusive language 14 | - Being respectful of differing viewpoints and experiences 15 | - Gracefully accepting constructive criticism 16 | 17 | Examples of unacceptable behaviour by participants include: 18 | 19 | - Trolling, insulting/derogatory comments, public or private harassment 20 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 21 | - Not being respectful to reasonable communication boundaries, such as 'leave me alone,' 'go away,' or 'I’m not discussing this with you.' 22 | - The usage of sexualised language or imagery and unwelcome sexual attention or advances 23 | - Demonstrating the graphics or any other content you know may be considered disturbing 24 | - Starting and/or participating in arguments related to politics 25 | - Other conduct which you know could reasonably be considered inappropriate in a professional setting. 26 | 27 | ## Enforcement 28 | 29 | Violations of the Code of Conduct may be reported by sending an email to `aeksco at gmail.com`. All reports will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Further details of specific enforcement policies may be posted separately. 30 | 31 | We hold the right and responsibility to remove comments or other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any members for other behaviours that they deem inappropriate, threatening, offensive, or harmful. 32 | 33 | ## Attribution 34 | 35 | This Code of Conduct is adapted from dev.to. 36 | -------------------------------------------------------------------------------- /src/popup/component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Hello } from "@src/components/hello"; 3 | import browser, { Tabs } from "webextension-polyfill"; 4 | import { Scroller } from "@src/components/scroller"; 5 | import css from "./styles.module.css"; 6 | 7 | // // // // 8 | 9 | // Scripts to execute in current tab 10 | const scrollToTopPosition = 0; 11 | const scrollToBottomPosition = 9999999; 12 | 13 | function scrollWindow(position: number) { 14 | window.scroll(0, position); 15 | } 16 | 17 | /** 18 | * Executes a string of Javascript on the current tab 19 | * @param code The string of code to execute on the current tab 20 | */ 21 | function executeScript(position: number): void { 22 | // Query for the active tab in the current window 23 | browser.tabs 24 | .query({ active: true, currentWindow: true }) 25 | .then((tabs: Tabs.Tab[]) => { 26 | // Pulls current tab from browser.tabs.query response 27 | const currentTab: Tabs.Tab | number = tabs[0]; 28 | 29 | // Short circuits function execution is current tab isn't found 30 | if (!currentTab) { 31 | return; 32 | } 33 | const currentTabId: number = currentTab.id as number; 34 | 35 | // Executes the script in the current tab 36 | browser.scripting 37 | .executeScript({ 38 | target: { 39 | tabId: currentTabId, 40 | }, 41 | func: scrollWindow, 42 | args: [position], 43 | }) 44 | .then(() => { 45 | console.log("Done Scrolling"); 46 | }); 47 | }); 48 | } 49 | 50 | // // // // 51 | 52 | export function Popup() { 53 | // Sends the `popupMounted` event 54 | React.useEffect(() => { 55 | browser.runtime.sendMessage({ popupMounted: true }); 56 | }, []); 57 | 58 | // Renders the component tree 59 | return ( 60 |
61 |
62 | 63 |
64 | { 66 | executeScript(scrollToTopPosition); 67 | }} 68 | onClickScrollBottom={() => { 69 | executeScript(scrollToBottomPosition); 70 | }} 71 | /> 72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-typescript-chrome-extension-starter", 3 | "version": "2.0.0", 4 | "description": "Web Extension starter kit built with React, TypeScript, Tailwind CSS, EsLint, Prettier & Webpack", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.prod.js", 8 | "dev": "webpack -w --config webpack.dev.js", 9 | "test": "jest --config=jest.config.js", 10 | "lint": "eslint --fix -c ./.eslintrc.js \"src/**/*.ts*\"", 11 | "prettify": "prettier --write \"src/**/*.ts*\"", 12 | "storybook": "start-storybook -p 6006", 13 | "build-storybook": "build-storybook" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/aeksco/react-typescript-chrome-extension-starter.git" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "typescript", 22 | "chrome", 23 | "extension", 24 | "boilerplate" 25 | ], 26 | "author": "Alexander Schwartzberg", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/aeksco/react-typescript-chrome-extension-starter/issues" 30 | }, 31 | "homepage": "https://github.com/aeksco/react-typescript-chrome-extension-starter#readme", 32 | "devDependencies": { 33 | "@babel/core": "^7.11.6", 34 | "@babel/preset-env": "^7.11.5", 35 | "@babel/preset-typescript": "^7.10.4", 36 | "@storybook/addon-essentials": "^6.4.9", 37 | "@storybook/addon-interactions": "^6.4.9", 38 | "@storybook/builder-webpack5": "^6.4.9", 39 | "@storybook/manager-webpack5": "^6.4.9", 40 | "@storybook/react": "^6.4.9", 41 | "@storybook/testing-library": "^0.0.7", 42 | "@types/chrome": "^0.0.124", 43 | "@types/jest": "^26.0.14", 44 | "@types/node": "^14.11.8", 45 | "@types/react": "^17.0.2", 46 | "@types/react-dom": "^17.0.2", 47 | "@types/react-test-renderer": "^17.0.1", 48 | "@types/webextension-polyfill": "^0.9.0", 49 | "@typescript-eslint/eslint-plugin": "^4.4.1", 50 | "@typescript-eslint/parser": "^4.4.1", 51 | "autoprefixer": "^10.4.1", 52 | "awesome-typescript-loader": "^5.2.1", 53 | "babel-core": "^6.26.3", 54 | "babel-jest": "^26.5.2", 55 | "babel-loader": "^8.1.0", 56 | "css-loader": "^4.3.0", 57 | "eslint": "^7.11.0", 58 | "eslint-config-prettier": "^6.12.0", 59 | "eslint-plugin-prettier": "^3.1.4", 60 | "eslint-plugin-react": "^7.21.4", 61 | "jest": "^26.5.3", 62 | "jest-css-modules": "^2.1.0", 63 | "postcss": "^8.4.31", 64 | "postcss-loader": "^6.2.1", 65 | "prettier": "^2.1.2", 66 | "react-test-renderer": "^17.0.2", 67 | "tailwindcss": "^3.0.8", 68 | "ts-jest": "^26.4.1", 69 | "ts-loader": "^8.0.5", 70 | "typescript": "^4.0.3", 71 | "webpack": "^5.76.0", 72 | "webpack-cli": "^4.9.1", 73 | "webpack-merge": "^5.8.0" 74 | }, 75 | "dependencies": { 76 | "react": "^17.0.2", 77 | "react-dom": "^17.0.2", 78 | "webextension-polyfill": "^0.9.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | 4 | module.exports = { 5 | stories: ["../src/**/*.stories.tsx"], 6 | addons: [ 7 | "@storybook/addon-essentials", 8 | "@storybook/addon-interactions", 9 | ], 10 | // Enable the Storybook Interactions debugger 11 | // Docs: https://storybook.js.org/addons/@storybook/addon-interactions 12 | features: { 13 | interactionsDebugger: true, 14 | }, 15 | // Configure Storybook to use Webpack@5.x 16 | // Docs: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#webpack-5 17 | core: { 18 | builder: "webpack5", 19 | }, 20 | webpackFinal: async (config) => { 21 | // Setup @src path resolution for TypeScript files 22 | config.resolve = { 23 | ...config.resolve, 24 | extensions: [".ts", ".tsx", ".js"], 25 | alias: { 26 | "@src": path.resolve(__dirname, "../src/"), 27 | }, 28 | }; 29 | 30 | // Setup module replacement for mocked webextension-polyfill 31 | config.plugins = [ 32 | ...config.plugins, 33 | new webpack.NormalModuleReplacementPlugin( 34 | /webextension-polyfill/, 35 | (resource) => { 36 | // Gets absolute path to mock `webextension-polyfill-ts` package 37 | // NOTE: this is required beacuse the `webextension-polyfill-ts` 38 | // package can't be used outside the environment provided by web extensions 39 | const absRootMockPath = path.resolve( 40 | __dirname, 41 | "../src/__mocks__/webextension-polyfill.ts", 42 | ); 43 | 44 | // Gets relative path from requesting module to our mocked module 45 | const relativePath = path.relative( 46 | resource.context, 47 | absRootMockPath, 48 | ); 49 | 50 | // Updates the `resource.request` to reference our mocked module instead of the real one 51 | switch (process.platform) { 52 | case "win32": { 53 | resource.request = "./" + relativePath; 54 | break; 55 | } 56 | default: { 57 | resource.request = relativePath; 58 | break; 59 | } 60 | } 61 | }, 62 | ), 63 | ]; 64 | 65 | // Remove the default .css webpack module rule 66 | // This is necessary because we use both global CSS and CSS modules 67 | // in the extension and in Storybook 68 | config.module.rules = config.module.rules.filter((r) => { 69 | if (".css".match(r.test)) { 70 | return false; 71 | } 72 | return true 73 | }) 74 | 75 | // Treat src/css/app.css as a global stylesheet 76 | config.module.rules.push({ 77 | test: /\app.css$/, 78 | use: [ 79 | "style-loader", 80 | "css-loader", 81 | "postcss-loader", 82 | ], 83 | }) 84 | 85 | // Load .module.css files as CSS modules 86 | config.module.rules.push({ 87 | test: /\.module.css$/, 88 | use: [ 89 | "style-loader", 90 | { 91 | loader: "css-loader", 92 | options: { 93 | modules: true, 94 | }, 95 | }, 96 | "postcss-loader", 97 | ], 98 | }) 99 | 100 | // Return the final Webpack configuration 101 | return config; 102 | }, 103 | }; 104 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | // Stop running tests after `n` failures 8 | // bail: 0, 9 | // Respect "browser" field in package.json when resolving modules 10 | // browser: false, 11 | // The directory where Jest should store its cached dependency information 12 | // cacheDirectory: "/tmp/jest_rs", 13 | // Automatically clear mock calls and instances between every test 14 | // clearMocks: true, 15 | // Indicates whether the coverage information should be collected while executing the test 16 | // collectCoverage: false, 17 | // An array of glob patterns indicating a set of files for which coverage information should be collected 18 | // collectCoverageFrom: null, 19 | // The directory where Jest should output its coverage files 20 | // coverageDirectory: "coverage", 21 | // An array of regexp pattern strings used to skip coverage collection 22 | // coveragePathIgnorePatterns: [ 23 | // "/node_modules/" 24 | // ], 25 | // A list of reporter names that Jest uses when writing coverage reports 26 | // coverageReporters: [ 27 | // "json", 28 | // "text", 29 | // "lcov", 30 | // "clover" 31 | // ], 32 | // An object that configures minimum threshold enforcement for coverage results 33 | // coverageThreshold: null, 34 | // A path to a custom dependency extractor 35 | // dependencyExtractor: null, 36 | // Make calling deprecated APIs throw helpful error messages 37 | // errorOnDeprecated: false, 38 | // Force coverage collection from ignored files using an array of glob patterns 39 | // forceCoverageMatch: [], 40 | // A path to a module which exports an async function that is triggered once before all test suites 41 | // globalSetup: null, 42 | // A path to a module which exports an async function that is triggered once after all test suites 43 | // globalTeardown: null, 44 | // A set of global variables that need to be available in all test environments 45 | // globals: {}, 46 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 47 | // maxWorkers: "50%", 48 | // An array of directory names to be searched recursively up from the requiring module's location 49 | // moduleDirectories: [ 50 | // "node_modules" 51 | // ], 52 | // An array of file extensions your modules use 53 | moduleFileExtensions: ["js", "json", "jsx", "ts", "tsx", "node"], 54 | // A map from regular expressions to module names that allow to stub out resources with a single module 55 | moduleNameMapper: { 56 | "@src/(.*)": "/src/$1", 57 | "\\.(css|less|scss|sss|styl)$": 58 | "/node_modules/jest-css-modules", 59 | }, 60 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 61 | // modulePathIgnorePatterns: [], 62 | // Activates notifications for test results 63 | // notify: false, 64 | // An enum that specifies notification mode. Requires { notify: true } 65 | // notifyMode: "failure-change", 66 | // A preset that is used as a base for Jest's configuration 67 | // preset: null, 68 | // Run tests from one or more projects 69 | // projects: null, 70 | // Use this configuration option to add custom reporters to Jest 71 | // reporters: undefined, 72 | // Automatically reset mock state between every test 73 | // resetMocks: false, 74 | // Reset the module registry before running each individual test 75 | // resetModules: false, 76 | // A path to a custom resolver 77 | // resolver: null, 78 | // Automatically restore mock state between every test 79 | // restoreMocks: false, 80 | // The root directory that Jest should scan for tests and modules within 81 | // rootDir: null, 82 | // A list of paths to directories that Jest should use to search for files in 83 | roots: ["/src"], 84 | // Allows you to use a custom runner instead of Jest's default test runner 85 | // runner: "jest-runner", 86 | // The paths to modules that run some code to configure or set up the testing environment before each test 87 | // setupFiles: [], 88 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 89 | // setupFilesAfterEnv: [], 90 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 91 | // snapshotSerializers: [], 92 | // The test environment that will be used for testing 93 | // testEnvironment: "jest-environment-jsdom", 94 | // Options that will be passed to the testEnvironment 95 | // testEnvironmentOptions: {}, 96 | // Adds a location field to test results 97 | // testLocationInResults: false, 98 | // The glob patterns Jest uses to detect test files 99 | // testMatch: [ 100 | // "**/__tests__/**/*.[jt]s?(x)", 101 | // "**/?(*.)+(spec|test).[tj]s?(x)" 102 | // ], 103 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 104 | testPathIgnorePatterns: ["/node_modules/", "stories.tsx"], 105 | // The regexp pattern or array of patterns that Jest uses to detect test files 106 | // testRegex: [], 107 | // This option allows the use of a custom results processor 108 | // testResultsProcessor: null, 109 | // This option allows use of a custom test runner 110 | // testRunner: "jasmine2", 111 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 112 | // testURL: "http://localhost", 113 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 114 | // timers: "real", 115 | // A map from regular expressions to paths to transformers 116 | // transform: null, 117 | transform: { 118 | "\\.tsx?$": "ts-jest", 119 | }, 120 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 121 | // transformIgnorePatterns: ["/node_modules/"], 122 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 123 | // unmockedModulePathPatterns: undefined, 124 | // Indicates whether each individual test should be reported during the run 125 | // verbose: null, 126 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 127 | // watchPathIgnorePatterns: [], 128 | // Whether to use watchman for file crawling 129 | // watchman: true, 130 | }; 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub stars](https://img.shields.io/github/stars/aeksco/react-typescript-web-extension-starter.svg?style=social&label=Stars&style=plastic)]() 2 | [![GitHub watchers](https://img.shields.io/github/watchers/aeksco/react-typescript-web-extension-starter.svg?style=social&label=Watch&style=plastic)]() 3 | [![GitHub forks](https://img.shields.io/github/forks/aeksco/react-typescript-web-extension-starter.svg?style=social&label=Fork&style=plastic)]() 4 | [![GitHub contributors](https://img.shields.io/github/contributors/aeksco/react-typescript-web-extension-starter.svg)](https://github.com/aeksco/react-typescript-web-extension-starter/graphs/contributors) 5 | [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/aeksco/react-typescript-web-extension-starter/blob/main/LICENSE) 6 | [![GitHub issues](https://img.shields.io/github/issues/aeksco/react-typescript-web-extension-starter.svg)](https://github.com/aeksco/react-typescript-web-extension-starter/issues) 7 | [![GitHub last commit](https://img.shields.io/github/last-commit/aeksco/react-typescript-web-extension-starter.svg)](https://github.com/aeksco/react-typescript-web-extension-starter/commits/master) 8 | [![GitHub pull requests](https://img.shields.io/github/issues-pr/aeksco/react-typescript-web-extension-starter.svg?style=flat)]() 9 | [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com) 10 | 11 | [![HitCount](http://hits.dwyl.com/aeksco/react-typescript-web-extension-starter.svg)](http://hits.dwyl.com/aeksco/react-typescript-web-extension-starter) 12 | [![Tweet](https://img.shields.io/twitter/url/https/github.com/aeksco/react-typescript-web-extension-starter.svg?style=social)](https://twitter.com/intent/tweet?text=https://github.com/aeksco/react-typescript-web-extension-starter) 13 | [![Twitter Follow](https://img.shields.io/twitter/follow/aeksco.svg?style=social)](https://twitter.com/aeksco) 14 | 15 | ![React TypeScript Web Extension Starter](https://i.imgur.com/wjIiRSv.png) 16 | 17 | :desktop_computer: A Web Extension starter kit built with React, TypeScript, Storybook, EsLint, Prettier, Jest, TailwindCSS, & Webpack. Compatible with Google Chrome, Mozilla Firefox, Brave, and Microsoft Edge. 18 | 19 | ![Example Extension Popup](https://i.imgur.com/UvOOWlv.png "Example Extension Popup") 20 | 21 | **Getting Started** 22 | 23 | Run the following commands to install dependencies and start developing 24 | 25 | ``` 26 | yarn install 27 | yarn dev 28 | ``` 29 | 30 | **Scripts** 31 | 32 | - `yarn dev` - run `webpack` in `watch` mode 33 | - `yarn storybook` - runs the Storybook server 34 | - `yarn build` - builds the production-ready unpacked extension 35 | - `yarn test -u` - runs Jest + updates test snapshots 36 | - `yarn lint` - runs EsLint 37 | - `yarn prettify` - runs Prettier 38 | 39 |
40 | Loading the extension in Google Chrome 41 | 42 | In [Google Chrome](https://www.google.com/chrome/), open up [chrome://extensions](chrome://extensions) in a new tab. Make sure the `Developer Mode` checkbox in the upper-right corner is turned on. Click `Load unpacked` and select the `dist` directory in this repository - your extension should now be loaded. 43 | 44 | ![Installed Extension in Google Chrome](https://i.imgur.com/Y2dQFte.png "Installed Extension in Google Chrome") 45 | 46 |
47 | 48 |
49 | Loading the extension in Brave 50 | 51 | In [Brave](https://brave.com/), open up [brave://extensions](brave://extensions) in a new tab. Make sure the `Developer Mode` checkbox in the upper-right corner is turned on. Click `Load unpacked` and select the `dist` directory in this repository - your extension should now be loaded. 52 | 53 | ![Installed Extension in Brave](https://i.imgur.com/rKsbtcO.png "Installed Extension in Brave") 54 | 55 |
56 | 57 |
58 | Loading the extension in Mozilla Firefox 59 | 60 | In [Mozilla Firefox](https://www.mozilla.org/en-US/firefox/new/), open up the [about:debugging](about:debugging) page in a new tab. Click the `This Firefox` link in the sidebar. One the `This Firefox` page, click the `Load Temporary Add-on...` button and select the `manifest.json` from the `dist` directory in this repository - your extension should now be loaded. 61 | 62 | ![Installed Extension in Mozilla Firefox](https://i.imgur.com/FKfTw4B.png "Installed Extension in Mozilla Firefox") 63 | 64 |
65 | 66 |
67 | Loading the extension in Microsoft Edge 68 | 69 | In [Microsoft Edge](https://www.microsoft.com/en-us/edge), open up [edge://extensions](edge://extensions) in a new tab. Make sure the `Developer Mode` checkbox in the lower-left corner is turned on. Click `Load unpacked` and select the `dist` directory in this repository - your extension should now be loaded. 70 | 71 | ![Installed Extension in Microsoft Edge](https://i.imgur.com/ykesx0g.png "Installed Extension in Microsoft Edge") 72 | 73 |
74 | 75 | **Notes** 76 | 77 | - This project is a [repository template](https://github.blog/2019-06-06-generate-new-repositories-with-repository-templates/) - click the `Use this template` button to use this starter codebase for your next project. 78 | 79 | - Includes ESLint configured to work with TypeScript and Prettier. 80 | 81 | - Includes tests with Jest - note that the `babel.config.js` and associated dependencies are only necessary for Jest to work with TypeScript. 82 | 83 | - Recommended to use `Visual Studio Code` with the `Format on Save` setting turned on. 84 | 85 | - Example icons courtesy of [Heroicons](https://heroicons.com/). 86 | 87 | - Includes Storybook configured to work with React + TypeScript. Note that it maintains its own `webpack.config.js` and `tsconfig.json` files. See example story in `src/components/hello/__tests__/hello.stories.tsx` 88 | 89 | - Includes a custom mock for the [webextension-polyfill-ts](https://github.com/Lusito/webextension-polyfill-ts) package in `src/__mocks__`. This allows you to mock any browser APIs used by your extension so you can develop your components inside Storybook. 90 | 91 | **Built with** 92 | 93 | - [React](https://reactjs.org) 94 | - [TypeScript](https://www.typescriptlang.org/) 95 | - [Storybook](https://storybook.js.org/) 96 | - [Jest](https://jestjs.io) 97 | - [Eslint](https://eslint.org/) 98 | - [Prettier](https://prettier.io/) 99 | - [Webpack](https://webpack.js.org/) 100 | - [Babel](https://babeljs.io/) 101 | - [TailwindCSS](https://tailwindcss.com/) 102 | - [webextension-polyfill](https://github.com/mozilla/webextension-polyfill) 103 | 104 | **Misc. References** 105 | 106 | - [Chrome Extension Developer Guide](https://developer.chrome.com/extensions/devguide) 107 | - [Firefox Extension Developer Guide](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension) 108 | - [Eslint + Prettier + Typescript Guide](https://dev.to/robertcoopercode/using-eslint-and-prettier-in-a-typescript-project-53jb) 109 | 110 | **Notable forks** 111 | 112 | - [capaj](https://github.com/capaj/react-typescript-web-extension-starter) - Chakra-ui instead of TailwindCSS, Storybook removed 113 | - [DesignString](https://github.com/DesignString/react-typescript-web-extension-starter) - Vite Js instead of Webpack 114 | --------------------------------------------------------------------------------