feat: initial rewrite
38
.babelrc
@ -1,38 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
// Latest stable ECMAScript features
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"useBuiltIns": false,
|
||||
// Do not transform modules to CJS
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"chrome": "49",
|
||||
"firefox": "52",
|
||||
"opera": "36",
|
||||
"edge": "79"
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/typescript",
|
||||
"@babel/react"
|
||||
],
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-class-properties"],
|
||||
["@babel/plugin-transform-destructuring", {
|
||||
"useBuiltIns": true
|
||||
}],
|
||||
["@babel/plugin-proposal-object-rest-spread", {
|
||||
"useBuiltIns": true
|
||||
}],
|
||||
[
|
||||
// Polyfills the runtime needed for async/await and generators
|
||||
"@babel/plugin-transform-runtime",
|
||||
{
|
||||
"helpers": false,
|
||||
"regenerator": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
3
.gitignore
vendored
@ -170,9 +170,6 @@ typings/
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# react / gatsby
|
||||
public/
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
|
||||
19
.travis.yml
@ -1,19 +0,0 @@
|
||||
language: node_js
|
||||
cache:
|
||||
directories:
|
||||
- ~/.npm
|
||||
node_js:
|
||||
- 12
|
||||
git:
|
||||
depth: 3
|
||||
script:
|
||||
- yarn run build
|
||||
deploy:
|
||||
provider: pages
|
||||
skip-cleanup: true
|
||||
keep-history: true
|
||||
github-token: $GITHUB_TOKEN
|
||||
local-dir: extension
|
||||
target-branch: extension
|
||||
on:
|
||||
branch: master
|
||||
@ -84,10 +84,8 @@ Then run the following:
|
||||
- `npm install` to install dependencies.
|
||||
- `npm run dev:chrome` to start the development server for chrome extension
|
||||
- `npm run dev:firefox` to start the development server for firefox addon
|
||||
- `npm run dev:opera` to start the development server for opera extension
|
||||
- `npm run build:chrome` to build chrome extension
|
||||
- `npm run build:firefox` to build firefox addon
|
||||
- `npm run build:opera` to build opera extension
|
||||
- `npm run build` builds and packs extensions all at once to extension/ directory
|
||||
|
||||
### Development
|
||||
@ -99,8 +97,6 @@ Then run the following:
|
||||
- `npm run dev:chrome`
|
||||
- Firefox
|
||||
- `npm run dev:firefox`
|
||||
- Opera
|
||||
- `npm run dev:opera`
|
||||
|
||||
- **Load extension in browser**
|
||||
|
||||
@ -116,11 +112,6 @@ Then run the following:
|
||||
- Load the Add-on via `about:debugging` as temporary Add-on.
|
||||
- Choose the `manifest.json` file in the extracted directory
|
||||
|
||||
- ### Opera
|
||||
|
||||
- Load the extension via `opera:extensions`
|
||||
- Check the `Developer Mode` and load as unpacked from extension’s extracted directory.
|
||||
|
||||
### Production
|
||||
|
||||
- `npm run build` builds the extension for all the browsers to `extension/BROWSER` directory respectively.
|
||||
|
||||
11618
package-lock.json
generated
93
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web-extension-starter",
|
||||
"version": "2.0.0",
|
||||
"version": "3.0.0",
|
||||
"description": "Web extension starter using react and typescript",
|
||||
"private": true,
|
||||
"repository": "https://github.com/abhijithvijayan/web-extension-starter.git",
|
||||
@ -11,77 +11,40 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0",
|
||||
"yarn": ">= 1.0.0"
|
||||
"node": ">=20"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:chrome": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=chrome webpack --watch",
|
||||
"dev:firefox": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=firefox webpack --watch",
|
||||
"dev:opera": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=opera webpack --watch",
|
||||
"build:chrome": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=chrome webpack",
|
||||
"build:firefox": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=firefox webpack",
|
||||
"build:opera": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=opera webpack",
|
||||
"build": "yarn run build:chrome && yarn run build:firefox && yarn run build:opera",
|
||||
"dev:chrome": "cross-env TARGET_BROWSER=chrome vite build --config vite.config.ts --mode development --watch",
|
||||
"dev:firefox": "cross-env TARGET_BROWSER=firefox vite build --config vite.config.ts --mode development --watch",
|
||||
"build:chrome": "cross-env TARGET_BROWSER=chrome vite build --config vite.config.ts",
|
||||
"build:firefox": "cross-env TARGET_BROWSER=firefox vite build --config vite.config.ts",
|
||||
"build": "npm run build:chrome && npm run build:firefox",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.9",
|
||||
"advanced-css-reset": "^1.2.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"webext-base-css": "^1.4.4",
|
||||
"webextension-polyfill-ts": "^0.26.0"
|
||||
"advanced-css-reset": "2.1.3",
|
||||
"find-up-simple": "1.0.1",
|
||||
"load-json-file": "7.0.1",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"read-pkg": "9.0.1",
|
||||
"webextension-polyfill": "0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@abhijithvijayan/eslint-config": "^2.8.1",
|
||||
"@abhijithvijayan/eslint-config-airbnb": "^1.1.0",
|
||||
"@abhijithvijayan/tsconfig": "^1.3.0",
|
||||
"@babel/core": "^7.23.9",
|
||||
"@babel/eslint-parser": "^7.23.9",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
|
||||
"@babel/plugin-transform-destructuring": "^7.23.3",
|
||||
"@babel/plugin-transform-runtime": "^7.23.9",
|
||||
"@babel/preset-env": "^7.23.9",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@types/react": "^17.0.75",
|
||||
"@types/react-dom": "^17.0.25",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"babel-loader": "^9.1.3",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.10.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"filemanager-webpack-plugin": "^8.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"mini-css-extract-plugin": "^2.7.7",
|
||||
"optimize-css-assets-webpack-plugin": "^6.0.1",
|
||||
"postcss": "^8.4.33",
|
||||
"postcss-loader": "^8.1.0",
|
||||
"prettier": "^3.2.4",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-loader": "^14.1.0",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"typescript": "4.9.5",
|
||||
"webpack": "^5.90.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-ext-reloader": "^1.1.12",
|
||||
"wext-manifest-loader": "^2.4.1",
|
||||
"wext-manifest-webpack-plugin": "^1.4.0"
|
||||
"@abhijithvijayan/tsconfig": "1.3.0",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/webextension-polyfill": "0.12.3",
|
||||
"@vitejs/plugin-react": "4.5.2",
|
||||
"autoprefixer": "10.4.21",
|
||||
"cross-env": "7.0.3",
|
||||
"postcss": "8.5.6",
|
||||
"sass": "1.89.2",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "6.3.5",
|
||||
"vite-plugin-checker": "0.9.3",
|
||||
"vite-plugin-zip-pack": "1.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
7
postcss.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
import autoprefixer from 'autoprefixer';
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
autoprefixer(),
|
||||
],
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import {browser} from 'webextension-polyfill-ts';
|
||||
import browser from "webextension-polyfill";
|
||||
|
||||
browser.runtime.onInstalled.addListener((): void => {
|
||||
console.log('🦄', 'extension installed');
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
const Options: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
@ -23,8 +21,6 @@ const Options: React.FC = () => {
|
||||
<input type="checkbox" name="logging" /> Show the features enabled
|
||||
on each page in the console
|
||||
</label>
|
||||
|
||||
<p>cool cool cool</p>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import Options from './Options';
|
||||
import './styles.scss';
|
||||
|
||||
ReactDOM.render(<Options />, document.getElementById('options-root'));
|
||||
const container = document.getElementById('options-root');
|
||||
|
||||
if (!container) {
|
||||
throw new Error("Could not find root container to mount the app");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Options />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -7,5 +7,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="options-root"></div>
|
||||
|
||||
<script type="module" src="/Options/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,10 +1,8 @@
|
||||
@import "../styles/fonts";
|
||||
@import "../styles/reset";
|
||||
@import "../styles/variables";
|
||||
|
||||
@import "~webext-base-css/webext-base.css";
|
||||
@use "../styles/fonts";
|
||||
@use "../styles/reset";
|
||||
@use "../styles/variables";
|
||||
|
||||
body {
|
||||
color: $black;
|
||||
background-color: $greyWhite;
|
||||
color: variables.$black;
|
||||
background-color: variables.$greyWhite;
|
||||
}
|
||||
@ -1,7 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import {browser, Tabs} from 'webextension-polyfill-ts';
|
||||
|
||||
import './styles.scss';
|
||||
import browser, {Tabs} from "webextension-polyfill";
|
||||
|
||||
function openWebPage(url: string): Promise<Tabs.Tab> {
|
||||
return browser.tabs.create({url});
|
||||
@ -15,7 +13,7 @@ const Popup: React.FC = () => {
|
||||
id="options__button"
|
||||
type="button"
|
||||
onClick={(): Promise<Tabs.Tab> => {
|
||||
return openWebPage('options.html');
|
||||
return openWebPage('/Options/options.html');
|
||||
}}
|
||||
>
|
||||
Options Page
|
||||
|
||||
@ -1,6 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import Popup from './Popup';
|
||||
|
||||
ReactDOM.render(<Popup />, document.getElementById('popup-root'));
|
||||
import './styles.scss';
|
||||
|
||||
const container = document.getElementById('popup-root');
|
||||
|
||||
if (!container) {
|
||||
throw new Error("Could not find root container to mount the app");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Popup />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -7,5 +7,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="popup-root"></div>
|
||||
|
||||
<script type="module" src="/Popup/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,12 +1,16 @@
|
||||
@import "../styles/fonts";
|
||||
@import "../styles/reset";
|
||||
@import "../styles/variables";
|
||||
@use "../styles/fonts";
|
||||
@use "../styles/reset";
|
||||
@use "../styles/variables";
|
||||
|
||||
body {
|
||||
color: $black;
|
||||
background-color: $greyWhite;
|
||||
color: variables.$black;
|
||||
background-color: variables.$greyWhite;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
#popup {
|
||||
min-width: 350px;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"__chrome__manifest_version": 3,
|
||||
"__firefox__manifest_version": 2,
|
||||
"name": "Sample WebExtension",
|
||||
"version": "0.0.0",
|
||||
|
||||
@ -13,19 +14,33 @@
|
||||
"homepage_url": "https://github.com/abhijithvijayan/web-extension-starter",
|
||||
"short_name": "Sample Name",
|
||||
|
||||
"permissions": [
|
||||
"__chrome__permissions": [
|
||||
"activeTab",
|
||||
"storage"
|
||||
],
|
||||
|
||||
"__chrome__optional_permissions": [],
|
||||
|
||||
"__chrome__host_permissions": [],
|
||||
|
||||
"__chrome__optional_host_permissions": [
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
|
||||
"__firefox__permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
"__chrome__content_security_policy": {
|
||||
"extension_pages": "script-src 'self'; object-src 'self';"
|
||||
},
|
||||
"__firefox__content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
|
||||
"__chrome|firefox__author": "abhijithvijayan",
|
||||
"__opera__developer": {
|
||||
"name": "abhijithvijayan"
|
||||
},
|
||||
|
||||
"__firefox__applications": {
|
||||
"gecko": {
|
||||
@ -33,11 +48,21 @@
|
||||
}
|
||||
},
|
||||
|
||||
"__chrome__minimum_chrome_version": "49",
|
||||
"__opera__minimum_opera_version": "36",
|
||||
"__chrome__minimum_chrome_version": "88",
|
||||
|
||||
"browser_action": {
|
||||
"default_popup": "popup.html",
|
||||
"__chrome__action": {
|
||||
"default_popup": "Popup/popup.html",
|
||||
"default_icon": {
|
||||
"16": "assets/icons/favicon-16.png",
|
||||
"32": "assets/icons/favicon-32.png",
|
||||
"48": "assets/icons/favicon-48.png",
|
||||
"128": "assets/icons/favicon-128.png"
|
||||
},
|
||||
"default_title": "tiny title"
|
||||
},
|
||||
|
||||
"__firefox__browser_action": {
|
||||
"default_popup": "Popup/popup.html",
|
||||
"default_icon": {
|
||||
"16": "assets/icons/favicon-16.png",
|
||||
"32": "assets/icons/favicon-32.png",
|
||||
@ -45,31 +70,45 @@
|
||||
"128": "assets/icons/favicon-128.png"
|
||||
},
|
||||
"default_title": "tiny title",
|
||||
"__chrome|opera__chrome_style": false,
|
||||
"__firefox__browser_style": false
|
||||
"browser_style": false
|
||||
},
|
||||
|
||||
"__chrome|opera__options_page": "options.html",
|
||||
"__chrome__options_page": "Options/options.html",
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": true,
|
||||
"__chrome__chrome_style": false
|
||||
"page": "Options/options.html",
|
||||
"open_in_tab": true
|
||||
},
|
||||
|
||||
"background": {
|
||||
"scripts": [
|
||||
"js/background.bundle.js"
|
||||
],
|
||||
"__chrome|opera__persistent": false
|
||||
"__chrome__service_worker": "assets/js/background.bundle.js",
|
||||
"__chrome__type": "module",
|
||||
"__firefox__scripts": [
|
||||
"assets/js/background.bundle.js"
|
||||
]
|
||||
},
|
||||
|
||||
"content_scripts": [{
|
||||
"matches": [
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"js": [
|
||||
"js/contentScript.bundle.js"
|
||||
]
|
||||
}]
|
||||
"content_scripts": [
|
||||
{
|
||||
"run_at": "document_start",
|
||||
"matches": [
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"css": [],
|
||||
"js": [
|
||||
"assets/js/contentScript.bundle.js"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"__firefox__web_accessible_resources": [
|
||||
"assets/*"
|
||||
],
|
||||
|
||||
"__chrome__web_accessible_resources": [
|
||||
{
|
||||
"resources": [ "assets/*" ],
|
||||
"matches": [ "https://*/*" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@ -1,4 +1,4 @@
|
||||
@import '~advanced-css-reset/dist/reset.css';
|
||||
@import '~/advanced-css-reset/dist/reset.min.css';
|
||||
|
||||
// Add your custom reset rules here
|
||||
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
"include": [
|
||||
"source",
|
||||
"webpack.config.js"
|
||||
"source"
|
||||
]
|
||||
}
|
||||
|
||||
181
vite-plugin-wext-manifest.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import { Plugin, ResolvedConfig } from 'vite';
|
||||
import path from 'node:path';
|
||||
import {findUp} from 'find-up-simple';
|
||||
import {readPackage} from 'read-pkg';
|
||||
import {loadJsonFile} from 'load-json-file';
|
||||
|
||||
export const PLUGIN_NAME = 'vite-plugin-wext-manifest';
|
||||
|
||||
export const ENVKeys = {
|
||||
DEV: 'dev',
|
||||
PROD: 'prod',
|
||||
} as const;
|
||||
|
||||
export const Browser = {
|
||||
CHROME: 'chrome',
|
||||
FIREFOX: 'firefox',
|
||||
EDGE: 'edge',
|
||||
BRAVE: 'brave',
|
||||
OPERA: 'opera',
|
||||
VIVALDI: 'vivaldi',
|
||||
ARC: 'arc',
|
||||
YANDEX: 'yandex',
|
||||
} as const;
|
||||
|
||||
export type BrowserType = (typeof Browser)[keyof typeof Browser];
|
||||
|
||||
export const browserVendors: BrowserType[] = Object.values(Browser);
|
||||
export const envVariables: string[] = [ENVKeys.DEV, ENVKeys.PROD];
|
||||
|
||||
// Refer: https://regex101.com/r/ddSEHh/1
|
||||
export const CUSTOM_PREFIX_REGEX = new RegExp(
|
||||
`^__((?:(?:${[...browserVendors, ...envVariables].join('|')})\\|?)+)__(.*)`
|
||||
);
|
||||
|
||||
export const transformManifest = (
|
||||
manifest: Record<string, string> | string | number | Awaited<ReturnType<typeof loadJsonFile>>,
|
||||
selectedVendor: BrowserType,
|
||||
nodeEnv: 'production' | 'development' | string
|
||||
): any => {
|
||||
if (Array.isArray(manifest)) {
|
||||
return manifest.map((newManifest) => {
|
||||
return transformManifest(newManifest, selectedVendor, nodeEnv);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof manifest === 'object') {
|
||||
return Object.entries(manifest).reduce(
|
||||
(newManifest: Record<string, string>, [key, value]) => {
|
||||
// match with vendors regex
|
||||
const vendorMatch: RegExpMatchArray | null =
|
||||
key.match(CUSTOM_PREFIX_REGEX);
|
||||
|
||||
if (vendorMatch) {
|
||||
// match[1] => 'opera|firefox|dev' => ['opera', 'firefox', 'dev']
|
||||
const matches: string[] = vendorMatch[1].split('|');
|
||||
const isProd: boolean = nodeEnv === 'production';
|
||||
|
||||
const hasCurrentVendor = matches.includes(selectedVendor);
|
||||
const hasVendorKeys = matches.some((m) =>
|
||||
browserVendors.includes(m as never)
|
||||
);
|
||||
const hasEnvKey = matches.some((m) =>
|
||||
envVariables.includes(m as never)
|
||||
);
|
||||
|
||||
const hasCurrentEnvKey =
|
||||
hasEnvKey &&
|
||||
// if production env key is found
|
||||
((isProd && matches.includes(ENVKeys.PROD)) ||
|
||||
// or if development env key is found
|
||||
(!isProd && matches.includes(ENVKeys.DEV)));
|
||||
|
||||
// handles cases like
|
||||
// 1. __dev__
|
||||
// 2. __chrome__
|
||||
// 3. __chrome|dev__
|
||||
|
||||
if (
|
||||
// case: __chrome|dev__ (current vendor key and current env key)
|
||||
(hasCurrentVendor && hasCurrentEnvKey) ||
|
||||
// case: __dev__ (no vendor keys but current env key)
|
||||
(!hasVendorKeys && hasCurrentEnvKey) ||
|
||||
// case: __chrome__ (no env keys but current vendor key)
|
||||
(!hasEnvKey && hasCurrentVendor)
|
||||
) {
|
||||
// Swap key with non prefixed name
|
||||
// match[2] => will be the key
|
||||
newManifest[vendorMatch[2]] = transformManifest(
|
||||
value,
|
||||
selectedVendor,
|
||||
nodeEnv
|
||||
);
|
||||
}
|
||||
} else {
|
||||
newManifest[key] = transformManifest(value, selectedVendor, nodeEnv);
|
||||
}
|
||||
|
||||
return newManifest;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
return manifest;
|
||||
};
|
||||
|
||||
interface WextManifestOptions {
|
||||
/**
|
||||
* The path to the source manifest.json file, relative to the project root.
|
||||
*/
|
||||
manifestPath: string;
|
||||
/**
|
||||
* If true, updates manifest.json version field with package.json version. It is often useful for easy release of web-extension.
|
||||
*/
|
||||
usePackageJSONVersion?: boolean;
|
||||
}
|
||||
|
||||
export default function vitePluginWextManifest(options: WextManifestOptions): Plugin {
|
||||
let config: ResolvedConfig;
|
||||
|
||||
if (!options?.manifestPath) {
|
||||
throw new Error(`${PLUGIN_NAME}: \`manifestPath\` option is required.`);
|
||||
}
|
||||
|
||||
return {
|
||||
name: PLUGIN_NAME,
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
},
|
||||
async buildStart() {
|
||||
const { mode, root } = config;
|
||||
const targetBrowser = process.env.TARGET_BROWSER;
|
||||
|
||||
if (!targetBrowser) {
|
||||
this.error('`TARGET_BROWSER` environment variable is not set.');
|
||||
}
|
||||
|
||||
if (!browserVendors.includes(targetBrowser)) {
|
||||
this.error(`Browser "${targetBrowser}" is not supported.`);
|
||||
}
|
||||
|
||||
try {
|
||||
const sourceManifestPath = path.resolve(root, options.manifestPath);
|
||||
this.addWatchFile(sourceManifestPath);
|
||||
// Read and parse manifest.json file
|
||||
const manifestInput = await loadJsonFile(sourceManifestPath);
|
||||
// 1. Transform the manifest
|
||||
const transformed = transformManifest(manifestInput, targetBrowser, mode);
|
||||
|
||||
// 2. Inject version from package.json if option is enabled
|
||||
const usePackageJSONVersion = !!options.usePackageJSONVersion;
|
||||
if (usePackageJSONVersion) {
|
||||
try {
|
||||
// find the closest package.json file
|
||||
const packageJsonPath = await findUp('package.json');
|
||||
if (!packageJsonPath) {
|
||||
throw new Error("Couldn't find a closest package.json")
|
||||
}
|
||||
|
||||
this.addWatchFile(packageJsonPath);
|
||||
const packageJson = await readPackage({...options, cwd: path.dirname(packageJsonPath)})
|
||||
if (!!transformed.version) {
|
||||
transformed.version = packageJson.version.replace('-beta.', '.'); // eg: replaces `2.0.0-beta.1` to `2.0.0.1`
|
||||
}
|
||||
} catch (err) {
|
||||
this.error(`Failed to process package.json: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Emit the final manifest file
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: 'manifest.json',
|
||||
source: JSON.stringify(transformed, null, 2),
|
||||
});
|
||||
} catch (err) {
|
||||
this.error(`Failed to process manifest.json: ${err.message}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
108
vite.config.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { defineConfig } from "vite";
|
||||
import path from "node:path";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import process from "node:process";
|
||||
import zipPack from "vite-plugin-zip-pack";
|
||||
import checker from 'vite-plugin-checker';
|
||||
|
||||
import vitePluginWextManifest from "./vite-plugin-wext-manifest";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const isDevelopment = mode !== "production";
|
||||
const sourcePath = path.resolve(__dirname, "source");
|
||||
const destPath = path.resolve(__dirname, "extension");
|
||||
const targetBrowser = process.env.TARGET_BROWSER || "chrome";
|
||||
|
||||
const getOutDir = () => path.resolve(destPath, targetBrowser);
|
||||
|
||||
return {
|
||||
root: sourcePath,
|
||||
|
||||
publicDir: path.resolve(sourcePath, "public"),
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(sourcePath),
|
||||
"~": path.resolve(__dirname, "node_modules"),
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [
|
||||
react(),
|
||||
|
||||
checker({
|
||||
typescript: {
|
||||
tsconfigPath: './tsconfig.json'
|
||||
},
|
||||
}),
|
||||
|
||||
vitePluginWextManifest({
|
||||
manifestPath: "manifest.json",
|
||||
usePackageJSONVersion: true,
|
||||
}),
|
||||
|
||||
!isDevelopment &&
|
||||
zipPack({
|
||||
outDir: destPath,
|
||||
outFileName: `${targetBrowser}.zip`,
|
||||
inDir: getOutDir(),
|
||||
}),
|
||||
],
|
||||
|
||||
build: {
|
||||
outDir: getOutDir(),
|
||||
|
||||
emptyOutDir: !isDevelopment,
|
||||
|
||||
sourcemap: isDevelopment ? "inline" : false,
|
||||
|
||||
minify: mode === "production",
|
||||
|
||||
rollupOptions: {
|
||||
input: {
|
||||
// For UI pages, use the HTML file as the entry.
|
||||
// Vite will find the <script> tag inside and bundle it.
|
||||
popup: path.resolve(sourcePath, 'Popup/popup.html'),
|
||||
options: path.resolve(sourcePath, 'Options/options.html'),
|
||||
// For script-only parts, use the TS file directly.
|
||||
background: path.resolve(sourcePath, 'Background/index.ts'),
|
||||
contentScript: path.resolve(sourcePath, 'ContentScript/index.ts'),
|
||||
},
|
||||
|
||||
output: {
|
||||
// For main entry scripts (background, contentScript, etc.)
|
||||
entryFileNames: "assets/js/[name].bundle.js",
|
||||
|
||||
// For other assets like CSS
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (/\.(css|s[ac]ss|less)$/.test(assetInfo.name)) {
|
||||
return "assets/css/[name]-[hash].css";
|
||||
}
|
||||
|
||||
// For other assets like fonts or images
|
||||
return "assets/[name]-[hash].[ext]";
|
||||
},
|
||||
|
||||
// For code-split chunks (if any)
|
||||
chunkFileNames: "assets/js/[name]-[hash].chunk.js",
|
||||
},
|
||||
},
|
||||
|
||||
terserOptions: {
|
||||
mangle: true,
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
},
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
define: {
|
||||
__DEV__: isDevelopment,
|
||||
__TARGET_BROWSER__: JSON.stringify(targetBrowser),
|
||||
},
|
||||
};
|
||||
});
|
||||
@ -1,211 +0,0 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const FilemanagerPlugin = require('filemanager-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
|
||||
const ExtensionReloader = require('webpack-ext-reloader');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const WextManifestWebpackPlugin = require('wext-manifest-webpack-plugin');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
|
||||
const viewsPath = path.join(__dirname, 'views');
|
||||
const sourcePath = path.join(__dirname, 'source');
|
||||
const destPath = path.join(__dirname, 'extension');
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const targetBrowser = process.env.TARGET_BROWSER;
|
||||
|
||||
const extensionReloaderPlugin =
|
||||
nodeEnv === 'development'
|
||||
? new ExtensionReloader({
|
||||
port: 9090, // Which port use to create the server
|
||||
reloadPage: true, // Force the reload of the page also
|
||||
entries: {
|
||||
// TODO: reload manifest on update
|
||||
contentScript: 'contentScript',
|
||||
background: 'background',
|
||||
extensionPage: ['popup', 'options'],
|
||||
},
|
||||
})
|
||||
: () => {
|
||||
this.apply = () => {};
|
||||
};
|
||||
|
||||
const getExtensionFileType = (browser) => {
|
||||
if (browser === 'opera') {
|
||||
return 'crx';
|
||||
}
|
||||
|
||||
if (browser === 'firefox') {
|
||||
return 'xpi';
|
||||
}
|
||||
|
||||
return 'zip';
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
devtool: false, // https://github.com/webpack/webpack/issues/1194#issuecomment-560382342
|
||||
|
||||
stats: {
|
||||
all: false,
|
||||
builtAt: true,
|
||||
errors: true,
|
||||
hash: true,
|
||||
},
|
||||
|
||||
mode: nodeEnv,
|
||||
|
||||
entry: {
|
||||
manifest: path.join(sourcePath, 'manifest.json'),
|
||||
background: path.join(sourcePath, 'Background', 'index.ts'),
|
||||
contentScript: path.join(sourcePath, 'ContentScript', 'index.ts'),
|
||||
popup: path.join(sourcePath, 'Popup', 'index.tsx'),
|
||||
options: path.join(sourcePath, 'Options', 'index.tsx'),
|
||||
},
|
||||
|
||||
output: {
|
||||
path: path.join(destPath, targetBrowser),
|
||||
filename: 'js/[name].bundle.js',
|
||||
},
|
||||
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
||||
alias: {
|
||||
'webextension-polyfill-ts': path.resolve(
|
||||
path.join(__dirname, 'node_modules', 'webextension-polyfill-ts')
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
type: 'javascript/auto', // prevent webpack handling json with its own loaders,
|
||||
test: /manifest\.json$/,
|
||||
use: {
|
||||
loader: 'wext-manifest-loader',
|
||||
options: {
|
||||
usePackageJSONVersion: true, // set to false to not use package.json version for manifest
|
||||
},
|
||||
},
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.(js|ts)x?$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.(sa|sc|c)ss$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader, // It creates a CSS file per JS file which contains CSS
|
||||
},
|
||||
{
|
||||
loader: 'css-loader', // Takes the CSS files and returns the CSS with imports and url(...) for Webpack
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
postcssOptions: {
|
||||
plugins: [
|
||||
[
|
||||
'autoprefixer',
|
||||
{
|
||||
// Options
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'resolve-url-loader', // Rewrites relative paths in url() statements
|
||||
'sass-loader', // Takes the Sass/SCSS file and compiles to the CSS
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
plugins: [
|
||||
// Plugin to not generate js bundle for manifest entry
|
||||
new WextManifestWebpackPlugin(),
|
||||
// Generate sourcemaps
|
||||
new webpack.SourceMapDevToolPlugin({filename: false}),
|
||||
new ForkTsCheckerWebpackPlugin(),
|
||||
// environmental variables
|
||||
new webpack.EnvironmentPlugin(['NODE_ENV', 'TARGET_BROWSER']),
|
||||
// delete previous build files
|
||||
new CleanWebpackPlugin({
|
||||
cleanOnceBeforeBuildPatterns: [
|
||||
path.join(process.cwd(), `extension/${targetBrowser}`),
|
||||
path.join(
|
||||
process.cwd(),
|
||||
`extension/${targetBrowser}.${getExtensionFileType(targetBrowser)}`
|
||||
),
|
||||
],
|
||||
cleanStaleWebpackAssets: false,
|
||||
verbose: true,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(viewsPath, 'popup.html'),
|
||||
inject: 'body',
|
||||
chunks: ['popup'],
|
||||
hash: true,
|
||||
filename: 'popup.html',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(viewsPath, 'options.html'),
|
||||
inject: 'body',
|
||||
chunks: ['options'],
|
||||
hash: true,
|
||||
filename: 'options.html',
|
||||
}),
|
||||
// write css file(s) to build folder
|
||||
new MiniCssExtractPlugin({filename: 'css/[name].css'}),
|
||||
// copy static assets
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{from: 'source/assets', to: 'assets'}],
|
||||
}),
|
||||
// plugin to enable browser reloading in development mode
|
||||
extensionReloaderPlugin,
|
||||
],
|
||||
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
},
|
||||
extractComments: false,
|
||||
}),
|
||||
new OptimizeCSSAssetsPlugin({
|
||||
cssProcessorPluginOptions: {
|
||||
preset: ['default', {discardComments: {removeAll: true}}],
|
||||
},
|
||||
}),
|
||||
new FilemanagerPlugin({
|
||||
events: {
|
||||
onEnd: {
|
||||
archive: [
|
||||
{
|
||||
format: 'zip',
|
||||
source: path.join(destPath, targetBrowser),
|
||||
destination: `${path.join(destPath, targetBrowser)}.${getExtensionFileType(targetBrowser)}`,
|
||||
options: {zlib: {level: 6}},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||