refactor: use shared linting config

This commit is contained in:
Abhijith Vijayan [FLUXON] 2026-01-03 20:54:03 +05:30
parent 88fea12ee6
commit 74d10f2424
23 changed files with 4522 additions and 322 deletions

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

View File

@ -1,5 +0,0 @@
node_modules/
dist/
extension/
.yarn/
.pnp.js

View File

@ -1,34 +0,0 @@
{
"extends": [
"@abhijithvijayan/eslint-config/typescript",
"@abhijithvijayan/eslint-config/node",
"@abhijithvijayan/eslint-config/react"
],
"parserOptions": {
"project": [
"./tsconfig.json"
],
"sourceType": "module"
},
"rules": {
"no-console": "off",
"no-extend-native": "off",
"react/jsx-props-no-spreading": "off",
"jsx-a11y/label-has-associated-control": "off",
"class-methods-use-this": "off",
"max-classes-per-file": "off",
"node/no-missing-import": "off",
"node/no-unpublished-import": "off",
"node/no-unsupported-features/es-syntax": ["error", {
"ignores": ["modules"]
}]
},
"env": {
"webextensions": true
},
"settings": {
"node": {
"tryExtensions": [".tsx"] // append tsx to the list as well
}
}
}

42
eslint.config.mjs Normal file
View File

@ -0,0 +1,42 @@
import nodeConfig from '@abhijithvijayan/eslint-config/node';
import tsConfig from '@abhijithvijayan/eslint-config/typescript';
import reactConfig from '@abhijithvijayan/eslint-config/react';
export default [
{
ignores: [
'node_modules/**',
'extension/**',
'*.js',
'*.mjs',
'vite.config.ts',
],
},
...nodeConfig({
files: ['**/*.ts', '**/*.tsx'],
}),
...tsConfig({
files: ['**/*.ts', '**/*.tsx'],
}),
...reactConfig({
files: ['**/*.tsx'],
}),
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'no-console': 'off',
'@typescript-eslint/no-use-before-define': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
// Disable due to resolver issues in ESM
'import-x/no-duplicates': 'off',
},
},
{
files: ['**/*.tsx'],
rules: {
'react/jsx-props-no-spreading': 'off',
'react/react-in-jsx-scope': 'off',
'jsx-a11y/label-has-associated-control': 'off',
},
},
];

4187
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,8 +20,8 @@
"build:chrome": "cross-env TARGET_BROWSER=chrome vite build --config vite.config.ts", "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:firefox": "cross-env TARGET_BROWSER=firefox vite build --config vite.config.ts",
"build": "npm run build:chrome && npm run build:firefox", "build": "npm run build:chrome && npm run build:firefox",
"lint": "eslint . --ext .ts,.tsx", "lint": "eslint .",
"lint:fix": "eslint . --ext .ts,.tsx --fix" "lint:fix": "eslint . --fix"
}, },
"dependencies": { "dependencies": {
"advanced-css-reset": "^2.1.3", "advanced-css-reset": "^2.1.3",
@ -30,15 +30,28 @@
"webextension-polyfill": "^0.12.0" "webextension-polyfill": "^0.12.0"
}, },
"devDependencies": { "devDependencies": {
"@abhijithvijayan/eslint-config": "^3.0.0",
"@abhijithvijayan/tsconfig": "^1.5.1", "@abhijithvijayan/tsconfig": "^1.5.1",
"@types/node": "^25.0.3", "@types/node": "^25.0.3",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/webextension-polyfill": "^0.12.4", "@types/webextension-polyfill": "^0.12.4",
"@typescript-eslint/eslint-plugin": "^8.51.0",
"@typescript-eslint/parser": "^8.51.0",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.2",
"autoprefixer": "^10.4.23", "autoprefixer": "^10.4.23",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^9.1.2",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-n": "^17.23.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"globals": "^15.15.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.7.4",
"sass": "^1.97.1", "sass": "^1.97.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.3.0", "vite": "^7.3.0",

View File

@ -21,6 +21,9 @@ browser.runtime.onMessage.addListener(
// Log when content script loads (if logging is enabled) // Log when content script loads (if logging is enabled)
getStorage(['enableLogging']).then(({enableLogging}) => { getStorage(['enableLogging']).then(({enableLogging}) => {
if (enableLogging) { if (enableLogging) {
console.log('[Web Extension Starter] Content script loaded on:', window.location.href); console.log(
'[Web Extension Starter] Content script loaded on:',
window.location.href
);
} }
}); });

View File

@ -1,20 +1,19 @@
import * as React from "react"; import {useEffect, useState} from 'react';
import { useEffect, useState } from "react"; import type {FC} from 'react';
import type { FC } from "react"; import {getStorage, setStorage} from '../utils/storage';
import { getStorage, setStorage } from "../utils/storage"; import {Button} from '../components/Button/Button';
import { Button } from "../components/Button/Button"; import {Input} from '../components/Input/Input';
import { Input } from "../components/Input/Input"; import {Checkbox} from '../components/Checkbox/Checkbox';
import { Checkbox } from "../components/Checkbox/Checkbox"; import {GitHubIcon} from '../components/icons/GitHubIcon';
import { GitHubIcon } from "../components/icons/GitHubIcon"; import styles from './Options.module.scss';
import styles from "./Options.module.scss";
const Options: FC = () => { const Options: FC = () => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState('');
const [enableLogging, setEnableLogging] = useState(false); const [enableLogging, setEnableLogging] = useState(false);
const [saved, setSaved] = useState(false); const [saved, setSaved] = useState(false);
useEffect(() => { useEffect(() => {
getStorage(["username", "enableLogging"]).then((result) => { getStorage(['username', 'enableLogging']).then((result) => {
setUsername(result.username); setUsername(result.username);
setEnableLogging(result.enableLogging); setEnableLogging(result.enableLogging);
}); });
@ -22,7 +21,7 @@ const Options: FC = () => {
const handleSave = async (e: React.FormEvent): Promise<void> => { const handleSave = async (e: React.FormEvent): Promise<void> => {
e.preventDefault(); e.preventDefault();
await setStorage({ username, enableLogging }); await setStorage({username, enableLogging});
setSaved(true); setSaved(true);
setTimeout(() => setSaved(false), 2000); setTimeout(() => setSaved(false), 2000);
}; };

View File

@ -1,17 +1,17 @@
import * as React from 'react'; import {StrictMode} from 'react';
import ReactDOM from 'react-dom/client'; import {createRoot} from 'react-dom/client';
import Options from './Options'; import Options from './Options';
const container = document.getElementById('options-root'); const container = document.getElementById('options-root');
if (!container) { if (!container) {
throw new Error("Could not find root container to mount the app"); throw new Error('Could not find root container to mount the app');
} }
const root = ReactDOM.createRoot(container); const root = createRoot(container);
root.render( root.render(
<React.StrictMode> <StrictMode>
<Options /> <Options />
</React.StrictMode> </StrictMode>
); );

View File

@ -1,14 +1,13 @@
import * as React from "react"; import {useEffect, useState} from 'react';
import { useEffect, useState } from "react"; import type {FC} from 'react';
import type { FC } from "react"; import browser, {Tabs} from 'webextension-polyfill';
import browser, { Tabs } from "webextension-polyfill"; import {getStorage} from '../utils/storage';
import { getStorage } from "../utils/storage"; import {TabInfo} from './components/TabInfo/TabInfo';
import { TabInfo } from "./components/TabInfo/TabInfo"; import {FooterActions} from './components/FooterActions/FooterActions';
import { FooterActions } from "./components/FooterActions/FooterActions"; import styles from './Popup.module.scss';
import styles from "./Popup.module.scss";
function openWebPage(url: string): Promise<Tabs.Tab> { function openWebPage(url: string): Promise<Tabs.Tab> {
return browser.tabs.create({ url }); return browser.tabs.create({url});
} }
interface TabData { interface TabData {
@ -19,21 +18,21 @@ interface TabData {
const Popup: FC = () => { const Popup: FC = () => {
const [tabInfo, setTabInfo] = useState<TabData | null>(null); const [tabInfo, setTabInfo] = useState<TabData | null>(null);
const [username, setUsername] = useState<string>(""); const [username, setUsername] = useState<string>('');
useEffect(() => { useEffect(() => {
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { browser.tabs.query({active: true, currentWindow: true}).then((tabs) => {
const tab = tabs[0]; const tab = tabs[0];
if (tab) { if (tab) {
setTabInfo({ setTabInfo({
title: tab.title || "Unknown", title: tab.title || 'Unknown',
url: tab.url || "Unknown", url: tab.url || 'Unknown',
favIconUrl: tab.favIconUrl, favIconUrl: tab.favIconUrl,
}); });
} }
}); });
getStorage(["username"]).then(({ username: storedUsername }) => { getStorage(['username']).then(({username: storedUsername}) => {
setUsername(storedUsername); setUsername(storedUsername);
}); });
}, []); }, []);
@ -69,13 +68,15 @@ const Popup: FC = () => {
<FooterActions <FooterActions
onSettings={(): Promise<Tabs.Tab> => onSettings={(): Promise<Tabs.Tab> =>
openWebPage("/Options/options.html") openWebPage('/Options/options.html')
} }
onGitHub={(): Promise<Tabs.Tab> => onGitHub={(): Promise<Tabs.Tab> =>
openWebPage("https://github.com/abhijithvijayan/web-extension-starter") openWebPage(
'https://github.com/abhijithvijayan/web-extension-starter'
)
} }
onSupport={(): Promise<Tabs.Tab> => onSupport={(): Promise<Tabs.Tab> =>
openWebPage("https://www.buymeacoffee.com/abhijithvijayan") openWebPage('https://www.buymeacoffee.com/abhijithvijayan')
} }
/> />
</section> </section>

View File

@ -1,10 +1,9 @@
import * as React from "react"; import type {FC} from 'react';
import type { FC } from "react"; import {Button} from '../../../components/Button/Button';
import { Button } from "../../../components/Button/Button"; import {SettingsIcon} from '../../../components/icons/SettingsIcon';
import { SettingsIcon } from "../../../components/icons/SettingsIcon"; import {GitHubIcon} from '../../../components/icons/GitHubIcon';
import { GitHubIcon } from "../../../components/icons/GitHubIcon"; import {HeartIcon} from '../../../components/icons/HeartIcon';
import { HeartIcon } from "../../../components/icons/HeartIcon"; import styles from './FooterActions.module.scss';
import styles from "./FooterActions.module.scss";
interface FooterActionsProps { interface FooterActionsProps {
onSettings: () => void; onSettings: () => void;
@ -16,36 +15,34 @@ export const FooterActions: FC<FooterActionsProps> = ({
onSettings, onSettings,
onGitHub, onGitHub,
onSupport, onSupport,
}) => { }) => (
return ( <div className={styles.footer}>
<div className={styles.footer}> <Button
<Button variant="settings"
variant="settings" size="small"
size="small" className={styles.button}
className={styles.button} onClick={onSettings}
onClick={onSettings} >
> <SettingsIcon />
<SettingsIcon /> <span>Settings</span>
<span>Settings</span> </Button>
</Button> <Button
<Button variant="github"
variant="github" size="small"
size="small" className={styles.button}
className={styles.button} onClick={onGitHub}
onClick={onGitHub} >
> <GitHubIcon />
<GitHubIcon /> <span>GitHub</span>
<span>GitHub</span> </Button>
</Button> <Button
<Button variant="support"
variant="support" size="small"
size="small" className={styles.button}
className={styles.button} onClick={onSupport}
onClick={onSupport} >
> <HeartIcon />
<HeartIcon /> <span>Support</span>
<span>Support</span> </Button>
</Button> </div>
</div> );
);
};

View File

@ -1,8 +1,7 @@
import * as React from "react"; import type {FC} from 'react';
import type { FC } from "react"; import {Card} from '../../../components/Card/Card';
import { Card } from "../../../components/Card/Card"; import {Button} from '../../../components/Button/Button';
import { Button } from "../../../components/Button/Button"; import styles from './TabInfo.module.scss';
import styles from "./TabInfo.module.scss";
interface TabInfoProps { interface TabInfoProps {
title: string; title: string;
@ -17,9 +16,7 @@ export const TabInfo: FC<TabInfoProps> = ({
favIconUrl, favIconUrl,
onReload, onReload,
}) => { }) => {
const getInitial = (text: string): string => { const getInitial = (text: string): string => text.charAt(0).toUpperCase();
return text.charAt(0).toUpperCase();
};
return ( return (
<Card title="Current Tab"> <Card title="Current Tab">

View File

@ -1,17 +1,17 @@
import * as React from 'react'; import {StrictMode} from 'react';
import ReactDOM from 'react-dom/client'; import {createRoot} from 'react-dom/client';
import Popup from './Popup'; import Popup from './Popup';
const container = document.getElementById('popup-root'); const container = document.getElementById('popup-root');
if (!container) { if (!container) {
throw new Error("Could not find root container to mount the app"); throw new Error('Could not find root container to mount the app');
} }
const root = ReactDOM.createRoot(container); const root = createRoot(container);
root.render( root.render(
<React.StrictMode> <StrictMode>
<Popup /> <Popup />
</React.StrictMode> </StrictMode>
); );

View File

@ -1,9 +1,13 @@
import * as React from "react"; import type {FC, ReactNode, ButtonHTMLAttributes} from 'react';
import type { FC, ReactNode, ButtonHTMLAttributes } from "react"; import styles from './Button.module.scss';
import styles from "./Button.module.scss";
type ButtonVariant = "primary" | "secondary" | "settings" | "github" | "support"; type ButtonVariant =
type ButtonSize = "small" | "medium" | "large"; | 'primary'
| 'secondary'
| 'settings'
| 'github'
| 'support';
type ButtonSize = 'small' | 'medium' | 'large';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant; variant?: ButtonVariant;
@ -13,8 +17,8 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
} }
export const Button: FC<ButtonProps> = ({ export const Button: FC<ButtonProps> = ({
variant = "primary", variant = 'primary',
size = "medium", size = 'medium',
fullWidth = false, fullWidth = false,
children, children,
className, className,
@ -28,7 +32,7 @@ export const Button: FC<ButtonProps> = ({
className, className,
] ]
.filter(Boolean) .filter(Boolean)
.join(" "); .join(' ');
return ( return (
<button type="button" className={classNames} {...props}> <button type="button" className={classNames} {...props}>

View File

@ -1,27 +1,22 @@
import * as React from "react"; import type {FC, ReactNode} from 'react';
import type { FC, ReactNode } from "react"; import styles from './Card.module.scss';
import styles from "./Card.module.scss";
interface CardProps { interface CardProps {
title?: string; title?: string;
size?: "default" | "large"; size?: 'default' | 'large';
children: ReactNode; children: ReactNode;
className?: string; className?: string;
} }
export const Card: FC<CardProps> = ({ export const Card: FC<CardProps> = ({
title, title,
size = "default", size = 'default',
children, children,
className, className,
}) => { }) => {
const classNames = [ const classNames = [styles.card, size === 'large' && styles.large, className]
styles.card,
size === "large" && styles.large,
className,
]
.filter(Boolean) .filter(Boolean)
.join(" "); .join(' ');
return ( return (
<div className={classNames}> <div className={classNames}>

View File

@ -1,8 +1,10 @@
import * as React from "react"; import type {FC, InputHTMLAttributes} from 'react';
import type { FC, InputHTMLAttributes } from "react"; import styles from './Checkbox.module.scss';
import styles from "./Checkbox.module.scss";
interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> { interface CheckboxProps extends Omit<
InputHTMLAttributes<HTMLInputElement>,
'type'
> {
label: string; label: string;
} }
@ -12,27 +14,16 @@ export const Checkbox: FC<CheckboxProps> = ({
checked, checked,
onChange, onChange,
...props ...props
}) => { }) => (
const handleWrapperClick = (e: React.MouseEvent<HTMLLabelElement>): void => { <label htmlFor={id} className={styles.wrapper}>
if ((e.target as HTMLElement).tagName !== "INPUT") { <input
const input = e.currentTarget.querySelector("input"); type="checkbox"
if (input) { id={id}
input.click(); className={styles.checkbox}
} checked={checked}
} onChange={onChange}
}; {...props}
/>
return ( <span className={styles.text}>{label}</span>
<label htmlFor={id} className={styles.wrapper} onClick={handleWrapperClick}> </label>
<input );
type="checkbox"
id={id}
className={styles.checkbox}
checked={checked}
onChange={onChange}
{...props}
/>
<span className={styles.text}>{label}</span>
</label>
);
};

View File

@ -1,24 +1,21 @@
import * as React from "react"; import type {FC, InputHTMLAttributes} from 'react';
import type { FC, InputHTMLAttributes } from "react"; import styles from './Input.module.scss';
import styles from "./Input.module.scss";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> { interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string; label?: string;
} }
export const Input: FC<InputProps> = ({ label, id, className, ...props }) => { export const Input: FC<InputProps> = ({label, id, className, ...props}) => (
return ( <div className={styles.wrapper}>
<div className={styles.wrapper}> {label && (
{label && ( <label htmlFor={id} className={styles.label}>
<label htmlFor={id} className={styles.label}> {label}
{label} </label>
</label> )}
)} <input
<input id={id}
id={id} className={`${styles.input} ${className || ''}`.trim()}
className={`${styles.input} ${className || ""}`.trim()} {...props}
{...props} />
/> </div>
</div> );
);
};

View File

@ -1,11 +1,10 @@
import * as React from "react"; import type {FC} from 'react';
import type { FC } from "react";
interface IconProps { interface IconProps {
size?: number; size?: number;
} }
export const GitHubIcon: FC<IconProps> = ({ size = 14 }) => ( export const GitHubIcon: FC<IconProps> = ({size = 14}) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor"> <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" /> <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
</svg> </svg>

View File

@ -1,11 +1,10 @@
import * as React from "react"; import type {FC} from 'react';
import type { FC } from "react";
interface IconProps { interface IconProps {
size?: number; size?: number;
} }
export const HeartIcon: FC<IconProps> = ({ size = 14 }) => ( export const HeartIcon: FC<IconProps> = ({size = 14}) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor"> <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" /> <path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
</svg> </svg>

View File

@ -1,11 +1,10 @@
import * as React from "react"; import type {FC} from 'react';
import type { FC } from "react";
interface IconProps { interface IconProps {
size?: number; size?: number;
} }
export const SettingsIcon: FC<IconProps> = ({ size = 14 }) => ( export const SettingsIcon: FC<IconProps> = ({size = 14}) => (
<svg <svg
width={size} width={size}
height={size} height={size}

4
source/globals.d.ts vendored
View File

@ -1,5 +1,5 @@
// https://www.typescriptlang.org/tsconfig/#noUncheckedSideEffectImports // https://www.typescriptlang.org/tsconfig/#noUncheckedSideEffectImports
declare module "*.scss" { declare module '*.scss' {
const content: { [className: string]: string }; const content: {[className: string]: string};
export default content; export default content;
} }

View File

@ -15,7 +15,10 @@
/* Bundler-specific settings */ /* Bundler-specific settings */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"isolatedModules": true, "isolatedModules": true,
"declaration": false "declaration": false,
/* React JSX transform */
"jsx": "react-jsx"
}, },
"include": [ "include": [
"source" "source"

View File

@ -1,134 +1,135 @@
import { defineConfig } from "vite"; import { defineConfig } from 'vite';
import path from "node:path"; import path from 'node:path';
import react from "@vitejs/plugin-react"; import react from '@vitejs/plugin-react';
import process from "node:process"; import process from 'node:process';
import zipPack from "vite-plugin-zip-pack"; import zipPack from 'vite-plugin-zip-pack';
import checker from 'vite-plugin-checker'; import checker from 'vite-plugin-checker';
import clean from 'vite-plugin-clean'; import clean from 'vite-plugin-clean';
import WextManifest from "vite-plugin-wext-manifest"; import WextManifest from 'vite-plugin-wext-manifest';
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const isDevelopment = mode !== "production"; const isDevelopment = mode !== 'production';
const sourcePath = path.resolve(__dirname, "source"); const sourcePath = path.resolve(__dirname, 'source');
const destPath = path.resolve(__dirname, "extension"); const destPath = path.resolve(__dirname, 'extension');
const targetBrowser = process.env.TARGET_BROWSER || "chrome"; const targetBrowser = process.env.TARGET_BROWSER || 'chrome';
const getOutDir = () => path.resolve(destPath, targetBrowser); const getOutDir = () => path.resolve(destPath, targetBrowser);
const getExtensionZipFileName = () => { const getExtensionZipFileName = () => {
switch (targetBrowser) { switch (targetBrowser) {
case 'opera': { case 'opera': {
return `${targetBrowser}.crx`; return `${targetBrowser}.crx`;
} }
case 'firefox': { case 'firefox': {
return `${targetBrowser}.xpi`; return `${targetBrowser}.xpi`;
} }
default: { default: {
return `${targetBrowser}.zip`; return `${targetBrowser}.zip`;
} }
} }
}; };
return { return {
root: sourcePath, root: sourcePath,
publicDir: path.resolve(sourcePath, "public"), publicDir: path.resolve(sourcePath, 'public'),
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(sourcePath), '@': path.resolve(sourcePath),
"~": path.resolve(__dirname, "node_modules"), '~': path.resolve(__dirname, 'node_modules'),
}, },
}, },
define: { define: {
__DEV__: isDevelopment, __DEV__: isDevelopment,
__TARGET_BROWSER__: JSON.stringify(targetBrowser), __TARGET_BROWSER__: JSON.stringify(targetBrowser),
}, },
plugins: [ plugins: [
react(), react(),
// delete previous built compressed file // delete previous built compressed file
clean({ clean({
targetFiles: [ targetFiles: [path.resolve(destPath, getExtensionZipFileName())],
path.resolve(destPath, getExtensionZipFileName()) }),
],
}),
// Run typescript checker in worker thread // Run typescript checker in worker thread
checker({ checker({
typescript: { typescript: {
tsconfigPath: './tsconfig.json' tsconfigPath: './tsconfig.json',
}, },
}), }),
// Generate manifest.json for the browser // Generate manifest.json for the browser
WextManifest({ WextManifest({
manifestPath: "manifest.json", manifestPath: 'manifest.json',
usePackageJSONVersion: true, usePackageJSONVersion: true,
}), }),
!isDevelopment && !isDevelopment &&
zipPack({ zipPack({
inDir: getOutDir(), inDir: getOutDir(),
outDir: destPath, outDir: destPath,
outFileName: getExtensionZipFileName(), outFileName: getExtensionZipFileName(),
enableLogging: true, enableLogging: true,
}), }),
], ],
build: { build: {
outDir: getOutDir(), outDir: getOutDir(),
emptyOutDir: !isDevelopment, emptyOutDir: !isDevelopment,
sourcemap: isDevelopment ? "inline" : false, sourcemap: isDevelopment ? 'inline' : false,
minify: mode === "production", minify: mode === 'production',
rollupOptions: { rollupOptions: {
input: { input: {
// For UI pages, use the HTML file as the entry. // For UI pages, use the HTML file as the entry.
// Vite will find the <script> tag inside and bundle it. // Vite will find the <script> tag inside and bundle it.
popup: path.resolve(sourcePath, 'Popup/popup.html'), popup: path.resolve(sourcePath, 'Popup/popup.html'),
options: path.resolve(sourcePath, 'Options/options.html'), options: path.resolve(sourcePath, 'Options/options.html'),
// For script-only parts, use the TS file directly. // For script-only parts, use the TS file directly.
background: path.resolve(sourcePath, 'Background/index.ts'), background: path.resolve(sourcePath, 'Background/index.ts'),
contentScript: path.resolve(sourcePath, 'ContentScript/index.ts'), contentScript: path.resolve(sourcePath, 'ContentScript/index.ts'),
}, },
output: { output: {
// For main entry scripts (background, contentScript, etc.) // For main entry scripts (background, contentScript, etc.)
entryFileNames: "assets/js/[name].bundle.js", entryFileNames: 'assets/js/[name].bundle.js',
// For other assets like CSS // For other assets like CSS
assetFileNames: (assetInfo) => { assetFileNames: (assetInfo) => {
if (!!assetInfo.name && /\.(css|s[ac]ss|less)$/.test(assetInfo.name)) { if (
return "assets/css/[name]-[hash].css"; !!assetInfo.name &&
} /\.(css|s[ac]ss|less)$/.test(assetInfo.name)
) {
return 'assets/css/[name]-[hash].css';
}
// For other assets like fonts or images // For other assets like fonts or images
return "assets/[name]-[hash].[ext]"; return 'assets/[name]-[hash].[ext]';
}, },
// For code-split chunks (if any) // For code-split chunks (if any)
chunkFileNames: "assets/js/[name]-[hash].chunk.js", chunkFileNames: 'assets/js/[name]-[hash].chunk.js',
}, },
}, },
terserOptions: { terserOptions: {
mangle: true, mangle: true,
compress: { compress: {
drop_console: true, drop_console: true,
drop_debugger: true, drop_debugger: true,
}, },
format: { format: {
comments: false, comments: false,
}, },
}, },
}, },
}; };
}); });