Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/init #1

Merged
merged 7 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: ci

on: [push]

permissions:
contents: read

env:
NODE_VERSION: '20'

jobs:
ci-ChromeExtension:
name: Chrome extension CI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
registry-url: 'https://npm.pkg.github.com'

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Lint the code
run: yarn run lint

- name: Build the code
run: yarn run build
29 changes: 29 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
node_modules/
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

.idea/
dist/
out/
*.crx
*.pem
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
10 changes: 10 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"semi": true,
"tabWidth": 2,
"printWidth": 100,
"singleQuote": true,
"jsxSingleQuote": true,
"trailingComma": "none",
"bracketSameLine": false,
"bracketSpacing": false
}
89 changes: 88 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,88 @@
# chrome-extension-boilerplate-react-vite
<img src="assets/img/icon-128.png" width="64"/>

# Chrome Extension (MV3) Boilerplate with React 18 and Vite 5

[//]: # 'TODO: Publish the template on NPM'
[//]: # '[![npm](https://img.shields.io/npm/v/chrome-extension-boilerplate-react)](https://www.npmjs.com/package/chrome-extension-boilerplate-react)'
[//]: # '[![npm-download](https://img.shields.io/npm/dw/chrome-extension-boilerplate-react)](https://www.npmjs.com/package/chrome-extension-boilerplate-react)'
[//]: # '[![npm](https://img.shields.io/npm/dm/chrome-extension-boilerplate-react)](https://www.npmjs.com/package/chrome-extension-boilerplate-react)'

This repository contains a boilerplate for building Chrome Extensions with React 18, TypeScript, and Vite 5.
This boilerplate is inspired by and adapted
from [chrome-extension-boilerplate-react](https://github.com/lxieyang/chrome-extension-boilerplate-react).

## Features

This is a basic Chrome Extensions boilerplate to help you write modular and modern Javascript code and load CSS easily.
This boilerplate is updated with:

- [Chrome Extension Manifest V3](https://developer.chrome.com/docs/extensions/mv3/intro/mv3-overview/)
- [React 18](https://reactjs.org)
- [MUI](https://mui.com/)
- ESLint:
- [eslint-plugin-react](https://www.npmjs.com/package/eslint-plugin-react)
- [eslint-config-prettier](https://www.npmjs.com/package/eslint-config-prettier)
- [eslint-plugin-simple-import-sort](https://www.npmjs.com/package/eslint-plugin-simple-import-sort)
- [typescript-eslint](https://www.npmjs.com/package/typescript-eslint)
- [Prettier](https://prettier.io/)
- [TypeScript](https://www.typescriptlang.org/)

I have avoided using CRXJS Vite Plugin on purpose as it's last update was in 2022, and it could possibly have some
issues with newer versions of Vite.

Please open up an issue to nudge me to keep the npm packages up-to-date.

## Installing and Running

### Procedures:

1. Check if your [Node.js](https://nodejs.org/) version is >= **18**.
2. Clone this repository.
3. Change the package's `name`, `description`, and `repository` fields in `package.json`.
4. Change the name of your extension on `src/manifest.json`.
5. Run `yarn install` to install the dependencies.
6. Run `yarn dist`
7. Load your extension on Chrome following:
1. Access `chrome://extensions/`
2. Turn the `Developer mode` switch on (top right corner)
3. Click on `Load unpacked`
4. Select the `dist` folder.
8. Happy hacking.

## Structure

All your extension's code must be placed in the `src` folder.

The boilerplate is already prepared to have a popup, a background script, and a content script.
This example Chrome extension implements logic which lets the user scrape the page title.
It was done so to demonstrate some of the [chrome API](https://developer.chrome.com/docs/extensions/reference/api)
functionality.

## TypeScript

This boilerplate supports TypeScript! Everything that can be written in TypeScript is written in TypeScript.

## Change Watchers

This boilerplate has watch scripts for the popup (`yarn watch:popup`), background script (`yarn watch:background`), and
content script (`yarn watch:content`).
`yarn dist` has to be run first to copy assets, `manifest.json` and Chrome extension files to the `dist` folder.

## Packing

After the development of your extension run the command

```
$ yarn dist
```

Now, the content of `dist` folder will be the extension ready to be submitted to the Chrome Web Store. Just take a look
at the [official guide](https://developer.chrome.com/webstore/publish) to more infos about publishing.

## Resources:

- [Chrome Extension documentation](https://developer.chrome.com/extensions/getstarted)

---

Tomasz Kiljańczyk | [Website](https://github.com/Gunock)
Binary file added assets/icons/icon-128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icons/icon-34.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// @ts-check

import eslint from '@eslint/js';
import eslintConfigPrettier from 'eslint-config-prettier';
import reactJsxRuntime from 'eslint-plugin-react/configs/jsx-runtime.js';
import reactRecommended from 'eslint-plugin-react/configs/recommended.js';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
import tseslint from 'typescript-eslint';

export default tseslint.config(
{
ignores: ['dist']
},
eslint.configs.recommended,
...tseslint.configs.recommended,
reactRecommended,
reactJsxRuntime,
eslintConfigPrettier,
{
plugins: {
'simple-import-sort': simpleImportSort
},
rules: {
'simple-import-sort/imports': 'warn',
'simple-import-sort/exports': 'warn'
}
}
);
18 changes: 18 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!doctype html>

<!-- Chrome Extension language is not determined by language set in HTML tag -->
<!--suppress HtmlRequiredLangAttribute -->
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Chrome Extension React</title>

<link rel="stylesheet" href="src/popup/styles/index.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="src/popup/index.tsx"></script>
</body>
</html>
33 changes: 33 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"manifest_version": 3,
"name": "Chrome Extension with React & Vite",
"version": "1.0.0",
"description": "A chrome extension boilerplate built with React 18 and Vite 5",
"icons": {
"34": "assets/icons/icon-34.png",
"128": "assets/icons/icon-128.png"
},
"action": {
"default_title": "Chrome Extension React",
"default_popup": "popup/index.html",
"default_icon": "assets/icons/icon-34.png"
},
"content_scripts": [
{
"js": ["content/index.js"],
"matches": ["*://*/*"]
}
],
"background": {
"service_worker": "background/index.js"
},
"web_accessible_resources": [
{
"resources": ["assets/icons/*.png"],
"matches": [],
"extension_ids": []
}
],
"permissions": ["storage", "tabs", "activeTab"],
"host_permissions": []
}
55 changes: 55 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "chrome-extension-boilerplate-react-vite",
"version": "1.0.0",
"description": "A chrome extension boilerplate built with React 18 and Vite 5",
"license": "MIT",
"type": "module",
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"watch": "yarn dist && yarn watch:popup",
"watch:background": "vite --config vite.config.background.ts build --watch",
"watch:content": "vite --config vite.config.content.ts build --watch",
"watch:popup": "vite --config vite.config.popup.ts build --watch",
"build": "yarn build:background && yarn build:content && yarn build:popup",
"build:background": "vite --config vite.config.background.ts build",
"build:content": "vite --config vite.config.content.ts build",
"build:popup": "vite --config vite.config.popup.ts build",
"clean": "rimraf dist",
"copy:static": "copyfiles manifest.json assets/**/* dist",
"dist:dev": "yarn lint && yarn dist",
"dist": "yarn clean && yarn build && yarn copy:static"
},
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0",
"@fontsource/roboto": "^5.0.12",
"@mui/icons-material": "^5.15.14",
"@mui/material": "^5.15.14",
"@mui/system": "^5.15.14",
"axios": "^1.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3"
},
"devDependencies": {
"@eslint/js": "^8.57.0",
"@types/react": "^18.2.69",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"@vitejs/plugin-react": "^4.2.1",
"chrome-types": "^0.1.274",
"copyfiles": "^2.4.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-simple-import-sort": "^12.0.0",
"prettier": "^3.2.5",
"rimraf": "^5.0.5",
"typescript": "5.4.3",
"typescript-eslint": "^7.3.1",
"vite": "^5.2.6"
}
}
10 changes: 10 additions & 0 deletions src/background/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
chrome.runtime.onInstalled.addListener(() => {
chrome.storage.local.clear();
});

chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// TODO: Here you can add logic such as e.g. disable popup button on specific pages
console.debug('tabId', tabId, 'changeInfo', changeInfo, 'tab', tab);
});

export {};
43 changes: 43 additions & 0 deletions src/common/chrome-api-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export enum ChromeMessageType {
SCRAPER_COMMAND,
SCRAPING_RESULTS
}

export class ChromeMessage<T> {
readonly type: ChromeMessageType;
readonly payload: T;

constructor(type: ChromeMessageType, payload: T) {
this.type = type;
this.payload = payload;
}
}

export class ChromeApiWrapper {
public static async sendTabMessage<T>(message: ChromeMessage<T>): Promise<void> {
const currentTab = await this.getCurrentTab();
if (currentTab == null) {
throw new Error('Could not find current tab');
}

await chrome.tabs.sendMessage(currentTab.id ?? 0, message);
}

public static async getCurrentTab(): Promise<chrome.tabs.Tab | null> {
const dateStarted = Date.now();

// Prevents code from failing when used too soon
while (Date.now() - dateStarted < 5000) {
const tabs = await chrome.tabs.query({active: true, lastFocusedWindow: true});

const currentTab = tabs[0];
if (currentTab?.status === 'complete') {
return currentTab;
}

await new Promise((r) => setTimeout(r, 50));
}

return null;
}
}
7 changes: 7 additions & 0 deletions src/common/types/scraper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum ScraperCommand {
SCRAPE
}

export interface ScraperMessage {
command: ScraperCommand;
}
25 changes: 25 additions & 0 deletions src/content/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {ChromeMessage, ChromeMessageType} from '../common/chrome-api-wrapper';
import {ScraperCommand, ScraperMessage} from '../common/types/scraper';

async function handleScrapeCommand() {
const pageTitle = document.title;
const message = new ChromeMessage(ChromeMessageType.SCRAPING_RESULTS, pageTitle);
await chrome.runtime.sendMessage(chrome.runtime.id, message);
}

chrome.runtime.onMessage.addListener((message: ChromeMessage<ScraperMessage>) => {
console.debug('Received message', message);
if (message.type !== ChromeMessageType.SCRAPER_COMMAND) {
return false;
}

if (message.payload.command === ScraperCommand.SCRAPE) {
handleScrapeCommand().catch((error) => console.error(error));
}

return false;
});

console.debug('Chrome plugin content script loaded');

export {};
Loading