Compare commits

..

No commits in common. "11ecc6379973675af8961200f9f7a538276789c7" and "28f710f9f5e5661eb1ed0402ab7be3a6be8fbeac" have entirely different histories.

24 changed files with 419 additions and 1322 deletions

View file

@ -1,31 +0,0 @@
{
"env": {
"browser": true,
"es2022": true,
"webextensions": true
},
"extends": ["eslint:recommended", "@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/prefer-const": "error",
"@typescript-eslint/no-var-requires": "error",
"prefer-const": "error",
"no-var": "error",
"no-console": "warn"
},
"ignorePatterns": ["dist/", "node_modules/", "*.js"]
}

View file

@ -1,8 +0,0 @@
node_modules/
dist/
*.log
.DS_Store
.vscode/
.idea/
*.sass
*.scss

View file

@ -1,19 +0,0 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "auto",
"overrides": [
{
"files": "*.json",
"options": {
"parser": "json"
}
}
]
}

219
README.md
View file

@ -4,222 +4,35 @@
[![GitHub forks](https://img.shields.io/github/forks/JosunLP/BrowserExtensionTemplate?style=for-the-badge)](https://github.com/JosunLP/BrowserExtensionTemplate/network) [![GitHub forks](https://img.shields.io/github/forks/JosunLP/BrowserExtensionTemplate?style=for-the-badge)](https://github.com/JosunLP/BrowserExtensionTemplate/network)
[![GitHub stars](https://img.shields.io/github/stars/JosunLP/BrowserExtensionTemplate?style=for-the-badge)](https://github.com/JosunLP/BrowserExtensionTemplate/stargazers) [![GitHub stars](https://img.shields.io/github/stars/JosunLP/BrowserExtensionTemplate?style=for-the-badge)](https://github.com/JosunLP/BrowserExtensionTemplate/stargazers)
[![GitHub license](https://img.shields.io/github/license/JosunLP/BrowserExtensionTemplate?style=for-the-badge)](https://github.com/JosunLP/BrowserExtensionTemplate) [![GitHub license](https://img.shields.io/github/license/JosunLP/BrowserExtensionTemplate?style=for-the-badge)](https://github.com/JosunLP/BrowserExtensionTemplate)
[![Twitter URL](https://img.shields.io/twitter/url?logo=twitter&style=for-the-badge&url=https%3A%2F%2Fgithub.com%2FJosunLP%2FBrowserExtensionTemplate)](https://twitter.com/intent/tweet?text=Look+what+i+found+on+GitHub+%23Developer%2C+%23SoftwareDeveloper%3A&url=https%3A%2F%2Fgithub.com%2FJosunLP%2FBrowserExtensionTemplate)
[![CodeFactor](https://www.codefactor.io/repository/github/josunlp/browserextensiontemplate/badge?style=for-the-badge)](https://www.codefactor.io/repository/github/josunlp/browserextensiontemplate) [![CodeFactor](https://www.codefactor.io/repository/github/josunlp/browserextensiontemplate/badge?style=for-the-badge)](https://www.codefactor.io/repository/github/josunlp/browserextensiontemplate)
[![Known Vulnerabilities](https://snyk.io/test/github/JosunLP/BrowserExtensionTemplate/badge.svg?style=for-the-badge)](https://snyk.io/test/github/JosunLP/BrowserExtensionTemplate)
## Description ## Description
A modern, production-ready template for building browser extensions using TypeScript, SASS, and Vite. This template provides a solid foundation with best practices, type safety, and modern development tools. A basic template based on SASS and TypeScript to create browser extensions without directly relying on a larger framework.
## Features
- 🚀 **Modern Tech Stack**: TypeScript, SASS, Vite, Bootstrap
- 🛡️ **Type Safety**: Strict TypeScript configuration with comprehensive error checking
- 🔧 **Development Tools**: ESLint, Prettier, automated workflows
- 🎯 **Cross-Browser**: Supports both Chrome (Manifest v3) and Firefox (Manifest v2)
- 📦 **Component System**: Reusable UI components with type safety
- 💾 **Session Management**: Robust localStorage-based session handling
- 🛠️ **Build System**: Optimized Vite configuration with code splitting
- 🎨 **Modern CSS**: CSS Custom Properties with SASS preprocessing
- 🔒 **Security**: Content Security Policy and secure coding practices
- ⚡ **Error Handling**: Comprehensive error boundary system
## Installation ## Installation
### Quick Start You can download the source code from [GitHub](https://github.com/JosunLP/BrowserExtensionTemplate). Just copy it in your project and run `npm install` to install the dependencies.
The basic configuration, wich will sync with `npm run sync` with the `package.json` file and the `manifest.json` file, is in `app.config.json`.
```bash Alternatively, you can fork the project and run `npm install` in the forked project.
git clone https://github.com/JosunLP/BrowserExtensionTemplate.git
cd BrowserExtensionTemplate
npm install
```
### Development Setup
```bash
# Install dependencies
npm install
# Start development mode with auto-rebuild
npm run dev
# Type checking
npm run type-check
# Linting and formatting
npm run validate
```
## Usage ## Usage
### Project Structure Your sourcecode can be written in the `src` folder. The `public` folder contains static files like images, html and the manifest.json.
With the `npm run deploy-v3` command you can deploy the extension to the dist folder, ready to be published to the chrome web store.
```bash With the `npm run deploy-v2` command you can deploy the extension to the dist folder, ready to be published to the firefox web store.
src/ This is necessary because the firefox web store needs the `manifest.json` file to be present in the version v2.
├── classes/ # Core classes (Session, ErrorBoundary)
├── components/ # Reusable UI components
├── sass/ # SASS styles with CSS custom properties
├── types/ # TypeScript type definitions
├── app.ts # Popup entry point
├── settings.ts # Options page entry point
└── background.ts # Background service worker
public/
├── icons/ # Extension icons
├── manifest.json # Extension manifest
├── popup.html # Popup HTML template
└── options.html # Options page HTML template
tools/ # Build and automation scripts
```
### Configuration
The main configuration is in `app.config.json`. This file is automatically synchronized with `package.json` and `manifest.json`:
```json
{
"AppData": {
"id": "your_extension_id",
"name": "Your Extension Name",
"version": "1.0.0",
"description": "Your extension description"
},
"htmlTemplatePairs": [
{
"key": "{{PLACEHOLDER}}",
"value": "Replacement Value"
}
]
}
```
### Build Commands
```bash
# Development
npm run dev # Start development with watch mode
npm run sync # Sync configuration files
# Production
npm run deploy-v3 # Build for Chrome (Manifest v3)
npm run deploy-v2 # Build for Firefox (Manifest v2)
# Quality Assurance
npm run validate # Type check + lint
npm run lint # ESLint with auto-fix
npm run format # Prettier formatting
# Utilities
npm run clean # Clean dist folder
npm run build-tooling # Compile TypeScript tools
```
### Development Workflow
1. **Configure your extension** in `app.config.json`
2. **Run sync** to update all config files: `npm run sync`
3. **Start development**: `npm run dev`
4. **Write your code** in the `src/` directory
5. **Build for production**: `npm run deploy-v3` or `npm run deploy-v2`
6. **Load the extension** from the `dist/` folder in your browser
### Session Management
The template includes a robust session management system:
```typescript
import { Session } from './classes/session';
// Get session instance (async)
const session = await Session.getInstance();
// Save data
session.contentTest = 'New value';
await session.save();
// Reset session
await Session.reset();
```
### Error Handling
Built-in error boundary system:
```typescript
import { ErrorBoundary } from './classes/errorBoundary';
const errorBoundary = ErrorBoundary.getInstance();
// Wrap async functions
const safeAsyncFunction = errorBoundary.wrapAsync(asyncFunction);
// Add custom error handlers
errorBoundary.addErrorHandler(error => {
console.log('Custom error handling:', error);
});
```
### Component System
Type-safe, reusable components:
```typescript
import { BasicButton } from './components/button';
// Create button
const button = new BasicButton('primary', 'Click me', 'my-button');
// Render as HTML string
const htmlString = button.render();
// Or create as DOM element
const buttonElement = button.createElement();
```
## Browser Compatibility
- **Chrome**: Manifest v3 (recommended)
- **Firefox**: Manifest v2 (automatically converted)
- **Edge**: Manifest v3 compatible
## Contributing
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/amazing-feature`
3. Make your changes and ensure tests pass: `npm run validate`
4. Commit your changes: `git commit -m 'Add amazing feature'`
5. Push to the branch: `git push origin feature/amazing-feature`
6. Open a Pull Request
## Development Guidelines
- Follow TypeScript best practices
- Use meaningful variable and function names
- Add proper error handling
- Write self-documenting code
- Follow the established project structure
- Run `npm run validate` before committing
## License ## License
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT). This project is licensed under the [MIT license](https://opensource.org/licenses/MIT).
## Contributing
This project is open source. Feel free to fork and contribute!
## Author ## Author
**_Jonas Pfalzgraf_** Jonas Pfalzgraf
- Email: <info@josunlp.de>
- GitHub: [@JosunLP](https://github.com/JosunLP)
## Changelog
### v0.0.1 (Current)
- ✨ Modern TypeScript setup with strict type checking
- 🛡️ Comprehensive error handling system
- 🎨 CSS Custom Properties with SASS
- 🔧 ESLint and Prettier configuration
- 📦 Optimized Vite build system
- 🚀 Cross-browser compatibility (Chrome/Firefox)
- 💾 Robust session management
- 🎯 Component-based architecture

View file

@ -1,55 +0,0 @@
import js from '@eslint/js';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
export default [
{
ignores: ['dist/', 'node_modules/', '**/*.js'],
},
js.configs.recommended,
{
files: ['src/**/*.ts'],
languageOptions: {
parser: tsParser,
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
chrome: 'readonly',
browser: 'readonly',
console: 'readonly',
document: 'readonly',
window: 'readonly',
localStorage: 'readonly',
sessionStorage: 'readonly',
HTMLElement: 'readonly',
HTMLDivElement: 'readonly',
HTMLButtonElement: 'readonly',
HTMLInputElement: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
crypto: 'readonly',
Error: 'readonly',
JSON: 'readonly',
Date: 'readonly',
String: 'readonly',
},
},
plugins: {
'@typescript-eslint': tsPlugin,
},
rules: {
...tsPlugin.configs.recommended.rules,
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-explicit-any': 'error',
'prefer-const': 'error',
'no-var': 'error',
'no-console': 'off',
},
},
];

View file

@ -2,39 +2,20 @@
"name": "browser_extension_template", "name": "browser_extension_template",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"deploy-v3": "npm run clean && npm run build-tooling && npm run sync && npm run build && npm run parse", "deploy-v3": "npx rimraf ./dist/ && npm run build-tooling && npm run sync && npm run build && node ./tools/parse.js",
"deploy-v2": "npm run deploy-v3 && node ./tools/v2.js", "deploy-v2": "npm run deploy-v3 && node ./tools/v2.js",
"build": "vite build", "build": "vite build",
"build-tooling": "tsc --project ./tooling.tsconfig.json", "build-tooling": "tsc --project ./tooling.tsconfig.json",
"watch": "vite build --watch", "watch": "vite build --watch",
"sync": "npm run build-tooling && node ./tools/syncConfig.js", "sync": "npm run build-tooling && node ./tools/syncConfig.js"
"parse": "node ./tools/parse.js",
"clean": "npx rimraf ./dist/",
"dev": "npm run sync && npm run watch",
"lint": "eslint src/**/*.ts --fix",
"format": "prettier --write src/**/*.{ts,json}",
"format:ts": "prettier --write src/**/*.ts",
"format:json": "prettier --write src/**/*.json",
"type-check": "tsc --noEmit",
"validate": "npm run type-check && npm run lint",
"prepare": "npm run validate && npm run build-tooling"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.30.1",
"@types/chrome": "^0.0.268", "@types/chrome": "^0.0.268",
"@types/node": "^20.13.0", "@types/node": "^20.13.0",
"@typescript-eslint/eslint-plugin": "^8.36.0",
"@typescript-eslint/parser": "^8.36.0",
"@webcomponents/webcomponentsjs": "^2.8.0", "@webcomponents/webcomponentsjs": "^2.8.0",
"eslint": "^9.30.1",
"prettier": "^3.6.2",
"rimraf": "^5.0.10",
"sass": "^1.77.4", "sass": "^1.77.4",
"typescript": "^5.8.3", "vite": "^5.2.12",
"vite": "^7.0.4",
"vite-tsconfig-paths": "^4.3.2" "vite-tsconfig-paths": "^4.3.2"
}, },
"browserslist": [ "browserslist": [

View file

@ -15,27 +15,12 @@
"default_popup": "popup.html" "default_popup": "popup.html"
}, },
"options_ui": { "options_ui": {
"page": "options.html", "page": "options.html"
"open_in_tab": false
}, },
"permissions": [ "permissions": [
"storage",
"notifications" "notifications"
], ],
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js"
},
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' 'unsafe-inline';"
},
"web_accessible_resources": [
{
"resources": [
"icons/*.png"
],
"matches": [
"<all_urls>"
]
} }
]
} }

View file

@ -1,22 +1,22 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="icon" href="./favicon.ico" /> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="./favicon.ico">
<title>{{BET}} Options</title> <title>{{BET}} Options</title>
</head> </head>
<body> <body>
<noscript> <noscript>
<strong <strong>We're sorry but {{BET}} doesn't work properly without JavaScript enabled. Please enable it to
>We're sorry but {{BET}} doesn't work properly without JavaScript enabled. Please enable it continue.</strong>
to continue.</strong
>
</noscript> </noscript>
<div id="app"> <div id="app">
<img src="./icons/icon128.png" alt="Logo" class="logo" /> <img src="./icons/icon128.png" class="logo" />
<h1>Settings</h1> <h1>Settings</h1>
<div id="settings"></div> <div id="settings">
</div>
</div> </div>
<footer> <footer>
<script type="module" src="./settings.js"></script> <script type="module" src="./settings.js"></script>

View file

@ -1,22 +1,22 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="icon" href="./favicon.ico" /> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="./favicon.ico">
<title>{{BET}}</title> <title>{{BET}}</title>
</head> </head>
<body> <body>
<noscript> <noscript>
<strong <strong>We're sorry but {{BET}} doesn't work properly without JavaScript enabled. Please enable it to
>We're sorry but {{BET}} doesn't work properly without JavaScript enabled. Please enable it continue.</strong>
to continue.</strong
>
</noscript> </noscript>
<div id="app"> <div id="app">
<img src="./icons/icon128.png" alt="Logo" class="logo" /> <img src="./icons/icon128.png" class="logo" />
<h1>{{BET}}</h1> <h1>{{BET}}</h1>
<div id="content"></div> <div id="content">
</div>
</div> </div>
<footer> <footer>
<script type="module" src="./app.js"></script> <script type="module" src="./app.js"></script>

View file

@ -1,59 +1,30 @@
import { Session } from './classes/session'; import { Session } from "./classes/session"
import './sass/app.sass'; import "./sass/app.sass"
class App { class App {
private static readonly CONTENT_ENTRY = 'content';
private session: Session | null = null; private static contentEntry: string = "content"
constructor() { constructor() {
this.init(); this.drawData()
this.main()
} }
private async init(): Promise<void> { async main(): Promise<void> {
try { console.log("Hello World")
this.session = await Session.getInstance();
await this.drawData();
await this.main();
} catch (error) {
console.error('Failed to initialize app:', error);
this.handleError('Failed to initialize application');
}
} }
private async main(): Promise<void> { async drawData(): Promise<void> {
console.log('Hello World'); const session = Session.getInstance()
} const contentRoot = <HTMLDivElement>document.getElementById(App.contentEntry)
const body = document.createElement("div")
private async drawData(): Promise<void> { const title = document.createElement("h1")
if (!this.session) { const text = document.createElement("p")
throw new Error('Session not initialized'); title.innerText = "Hello World"
} text.innerText = session.contentTest
body.appendChild(title)
const contentRoot = document.getElementById(App.CONTENT_ENTRY) as HTMLDivElement | null; body.appendChild(text)
if (!contentRoot) { contentRoot.appendChild(body)
throw new Error(`Element with id '${App.CONTENT_ENTRY}' not found`);
}
const body = document.createElement('div');
body.className = 'app-content';
const title = document.createElement('h1');
title.innerText = 'Hello World';
const text = document.createElement('p');
text.innerText = this.session.contentTest;
body.appendChild(title);
body.appendChild(text);
contentRoot.appendChild(body);
}
private handleError(message: string): void {
console.error(message);
const contentRoot = document.getElementById(App.CONTENT_ENTRY);
if (contentRoot) {
contentRoot.innerHTML = `<div class="error-message">${message}</div>`;
}
} }
} }

View file

@ -1,82 +1,9 @@
interface ExtensionMessage {
type: string;
payload?: unknown;
}
class Background { class Background {
constructor() { constructor() {
this.init(); this.main();
} }
private async init(): Promise<void> { async main(): Promise<void> {}
try {
await this.setupEventListeners();
await this.main();
console.log('Background service worker initialized');
} catch (error) {
console.error('Failed to initialize background service worker:', error);
}
}
private async setupEventListeners(): Promise<void> {
// Install event
chrome.runtime.onInstalled.addListener(details => {
console.log('Extension installed:', details.reason);
this.handleInstall(details.reason);
});
// Message handling
chrome.runtime.onMessage.addListener((message: ExtensionMessage, sender, sendResponse) => {
this.handleMessage(message, sender)
.then(response => sendResponse(response))
.catch(error => {
console.error('Error handling message:', error);
sendResponse({ error: error.message });
});
return true; // Indicates we will send a response asynchronously
});
// Startup event
chrome.runtime.onStartup.addListener(() => {
console.log('Extension started');
});
}
private async handleInstall(reason: string): Promise<void> {
if (reason === 'install') {
// First time installation
console.log('Extension installed for the first time');
} else if (reason === 'update') {
// Extension updated
console.log('Extension updated');
}
}
private async handleMessage(
message: ExtensionMessage,
sender: chrome.runtime.MessageSender
): Promise<unknown> {
console.log('Received message:', message, 'from:', sender);
switch (message.type) {
case 'ping':
return { type: 'pong', timestamp: Date.now() };
case 'getVersion':
return {
type: 'version',
version: chrome.runtime.getManifest().version,
};
default:
throw new Error(`Unknown message type: ${message.type}`);
}
}
private async main(): Promise<void> {
// Main background logic can be implemented here
// This method is called after initialization
}
} }
new Background(); new Background();

View file

@ -1,99 +0,0 @@
export class ErrorBoundary {
private static instance: ErrorBoundary;
private errorHandlers: Array<(error: Error) => void> = [];
private constructor() {
this.setupGlobalErrorHandlers();
}
public static getInstance(): ErrorBoundary {
if (!ErrorBoundary.instance) {
ErrorBoundary.instance = new ErrorBoundary();
}
return ErrorBoundary.instance;
}
private setupGlobalErrorHandlers(): void {
// Handle uncaught errors
window.addEventListener('error', event => {
this.handleError(new Error(event.message), {
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
});
});
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', event => {
this.handleError(
event.reason instanceof Error ? event.reason : new Error(String(event.reason)),
{ type: 'unhandledrejection' }
);
});
}
public addErrorHandler(handler: (error: Error) => void): void {
this.errorHandlers.push(handler);
}
public removeErrorHandler(handler: (error: Error) => void): void {
const index = this.errorHandlers.indexOf(handler);
if (index > -1) {
this.errorHandlers.splice(index, 1);
}
}
public handleError(error: Error, context?: Record<string, unknown>): void {
console.error('Error caught by ErrorBoundary:', error, context);
// Call all registered error handlers
this.errorHandlers.forEach(handler => {
try {
handler(error);
} catch (handlerError) {
console.error('Error in error handler:', handlerError);
}
});
// Send to background script if available
if (chrome.runtime) {
chrome.runtime
.sendMessage({
type: 'error',
payload: {
message: error.message,
stack: error.stack,
context,
timestamp: Date.now(),
},
})
.catch(() => {
// Ignore errors when sending to background
});
}
}
public wrapAsync<T extends unknown[], R>(
fn: (...args: T) => Promise<R>
): (...args: T) => Promise<R> {
return async (...args: T): Promise<R> => {
try {
return await fn(...args);
} catch (error) {
this.handleError(error instanceof Error ? error : new Error(String(error)));
throw error;
}
};
}
public wrapSync<T extends unknown[], R>(fn: (...args: T) => R): (...args: T) => R {
return (...args: T): R => {
try {
return fn(...args);
} catch (error) {
this.handleError(error instanceof Error ? error : new Error(String(error)));
throw error;
}
};
}
}

View file

@ -1,109 +1,55 @@
interface SessionData { export class Session {
sessionId: string;
contentTest: string; private static instance: Session;
private constructor() {
} }
interface StorageService { static getInstance() {
save(key: string, data: unknown): Promise<void>; if (!Session.instance && !Session.load()) {
load<T>(key: string): Promise<T | null>; Session.instance = new Session();
remove(key: string): Promise<void>; }
if (!Session.instance && Session.load()) {
Session.instance = <Session>Session.load();
}
Session.save();
return Session.instance;
} }
class LocalStorageService implements StorageService { public static save() {
async save(key: string, data: unknown): Promise<void> { localStorage.setItem('session', JSON.stringify(this.instance));
try {
localStorage.setItem(key, JSON.stringify(data));
} catch (error) {
console.error('Failed to save to localStorage:', error);
throw new Error('Storage operation failed');
}
} }
async load<T>(key: string): Promise<T | null> { public static load(): Session | null {
try { const session = localStorage.getItem('session');
const item = localStorage.getItem(key); if (session) {
return item ? (JSON.parse(item) as T) : null; const obj = <Session>JSON.parse(session);
} catch (error) { const result = new Session();
console.error('Failed to load from localStorage:', error); result.contentTest = obj.contentTest;
return result;
}
return null; return null;
} }
}
async remove(key: string): Promise<void> { public static reloadSession() {
try { const session = localStorage.getItem('session');
localStorage.removeItem(key); if (session) {
} catch (error) { const obj = <Session>JSON.parse(session);
console.error('Failed to remove from localStorage:', error); const result = new Session();
throw new Error('Storage operation failed'); result.contentTest = obj.contentTest;
} Session.instance = result;
} }
} }
export class Session implements SessionData { public static resetSession() {
private static instance: Session | null = null; localStorage.removeItem('session');
private static readonly STORAGE_KEY = 'browser_extension_session'; sessionStorage.removeItem('session');
private static readonly storageService: StorageService = new LocalStorageService(); this.instance = new Session();
Session.save();
public readonly sessionId: string; location.reload();
public contentTest: string;
private constructor(data?: Partial<SessionData>) {
this.sessionId = data?.sessionId ?? crypto.randomUUID();
this.contentTest = data?.contentTest ?? 'This is a simple example of a web application';
} }
public static async getInstance(): Promise<Session> { public readonly sessionId: string = crypto.randomUUID();
if (!Session.instance) {
await Session.loadOrCreate();
}
return Session.instance!;
}
private static async loadOrCreate(): Promise<void> { public contentTest: string = 'This is a simple example of a web application';
try {
const savedData = await Session.storageService.load<SessionData>(Session.STORAGE_KEY);
Session.instance = new Session(savedData ?? undefined);
await Session.instance.save();
} catch (error) {
console.error('Failed to load session, creating new one:', error);
Session.instance = new Session();
await Session.instance.save();
}
}
public async save(): Promise<void> {
try {
const data: SessionData = {
sessionId: this.sessionId,
contentTest: this.contentTest,
};
await Session.storageService.save(Session.STORAGE_KEY, data);
} catch (error) {
console.error('Failed to save session:', error);
throw error;
}
}
public static async reset(): Promise<void> {
try {
await Session.storageService.remove(Session.STORAGE_KEY);
Session.instance = new Session();
await Session.instance.save();
// Reload page only if we're in a browser environment
if (typeof window !== 'undefined' && window.location) {
window.location.reload();
}
} catch (error) {
console.error('Failed to reset session:', error);
throw error;
}
}
public toJSON(): SessionData {
return {
sessionId: this.sessionId,
contentTest: this.contentTest,
};
}
} }

View file

@ -1,96 +1,63 @@
import { customButton } from '../types/buttonType'; import { customButton } from "../types/buttonType";
export interface ButtonConfig {
type: customButton;
text: string;
id?: string | undefined;
className?: string | undefined;
disabled?: boolean | undefined;
onClick?: (() => void) | undefined;
}
export class BasicButton { export class BasicButton {
private readonly config: ButtonConfig;
private type: customButton;
private text: string;
private id: string | undefined;
private className: string | undefined;
constructor(type: customButton, text: string, id?: string, className?: string) { constructor(type: customButton, text: string, id?: string, className?: string) {
this.config = { this.type = type;
type, this.text = text;
text, this.id = id;
id, this.className = className;
className,
};
} }
public render(): string { public render(): string {
const button = document.createElement('button'); const result = document.createElement('button');
button.type = 'button'; result.type = "button";
button.textContent = this.config.text; result.className = this.type;
button.className = this.getBootstrapClass(); result.textContent = this.text;
if (this.config.id) { switch (this.type) {
button.id = this.config.id; case "primary":
result.className = "btn btn-primary";
break;
case "success":
result.className = "btn btn-success";
break;
case "danger":
result.className = "btn btn-danger";
break;
case "warning":
result.className = "btn btn-warning";
break;
case "info":
result.className = "btn btn-info";
break;
case "light":
result.className = "btn btn-light";
break;
case "dark":
result.className = "btn btn-dark";
break;
default:
result.className = "btn btn-primary";
break;
} }
if (this.config.className) { if (this.id) {
button.className += ` ${this.config.className}`; result.id = this.id;
}
if (this.className) {
result.className += ' ' + this.className;
} }
if (this.config.disabled) { return result.outerHTML;
button.disabled = true;
} }
return button.outerHTML;
}
public createElement(): HTMLButtonElement {
const button = document.createElement('button');
button.type = 'button';
button.textContent = this.config.text;
button.className = this.getBootstrapClass();
if (this.config.id) {
button.id = this.config.id;
}
if (this.config.className) {
button.className += ` ${this.config.className}`;
}
if (this.config.disabled) {
button.disabled = true;
}
if (this.config.onClick) {
button.addEventListener('click', this.config.onClick);
}
return button;
}
private getBootstrapClass(): string {
const typeMap: Record<customButton, string> = {
neutral: 'btn btn-secondary',
primary: 'btn btn-primary',
secondary: 'btn btn-secondary',
success: 'btn btn-success',
danger: 'btn btn-danger',
warning: 'btn btn-warning',
info: 'btn btn-info',
light: 'btn btn-light',
dark: 'btn btn-dark',
};
return typeMap[this.config.type] ?? 'btn btn-primary';
}
public static create(config: ButtonConfig): BasicButton {
const button = new BasicButton(config.type, config.text, config.id, config.className);
if (config.disabled !== undefined) {
button.config.disabled = config.disabled;
}
if (config.onClick !== undefined) {
button.config.onClick = config.onClick;
}
return button;
}
} }

View file

@ -1,64 +1,26 @@
// CSS Custom Properties for theming support $main-font: 'Ubuntu', 'Staatliches'
:root $main-font-color: white
--main-font: 'Ubuntu', 'Segoe UI', 'Roboto', sans-serif $main-font-color-hover: lightgrey
--main-font-color: #ffffff $main-font-color-focus: lightgrey
--main-font-color-hover: #f8f9fa $main-font-color-disabled: lightgrey
--main-font-color-focus: #e9ecef $main-font-color-active: lightgrey
--main-font-color-disabled: #6c757d $main-uschrift-font: 'Ubuntu', Arial
--main-font-color-active: #f8f9fa $primary-color: #007bff
$primary-color-hover: #0069d9
--primary-color: #007bff $primary-color-focus: #0062cc
--primary-color-hover: #0056b3 $primary-color-disabled: #0069d9
--primary-color-focus: #004085 $primary-color-active: #0062cc
--primary-color-disabled: #6c757d $background-color-content-shadow: #0069d9
--primary-color-active: #004085 $seccond-color: #6c757d
$seccondary-color: darkgrey
--secondary-color: #6c757d $seccondary-color-hover: black
--secondary-color-hover: #545b62 $seccondary-color-focus: black
--secondary-color-focus: #4e555b $seccondary-color-disabled: black
--secondary-color-disabled: #adb5bd $seccondary-color-active: black
--secondary-color-active: #4e555b $background-color: #77B2FF
$background-color-content: rgb(198, 223, 255)
--background-color: #77B2FF $background-color-content-hover: rgb(198, 223, 255)
--background-color-content: #c6dfff $background-color-content-focus: rgb(198, 223, 255)
--background-color-content-hover: #b3d7ff $background-color-content-active: rgb(198, 223, 255)
--background-color-content-focus: #9fcdff $background-color-content-disabled: rgb(198, 223, 255)
--background-color-content-active: #8cc4ff $logo-image: url('../icons/icon128.png')
--background-color-content-disabled: #e9ecef
--shadow-color: rgba(0, 0, 0, 0.1)
--border-radius: 0.375rem
--transition-duration: 0.15s
--font-size-base: 1rem
--line-height-base: 1.5
// SASS Variables (for backwards compatibility)
$main-font: var(--main-font)
$main-font-color: var(--main-font-color)
$main-font-color-hover: var(--main-font-color-hover)
$main-font-color-focus: var(--main-font-color-focus)
$main-font-color-disabled: var(--main-font-color-disabled)
$main-font-color-active: var(--main-font-color-active)
$primary-color: var(--primary-color)
$primary-color-hover: var(--primary-color-hover)
$primary-color-focus: var(--primary-color-focus)
$primary-color-disabled: var(--primary-color-disabled)
$primary-color-active: var(--primary-color-active)
$secondary-color: var(--secondary-color)
$secondary-color-hover: var(--secondary-color-hover)
$secondary-color-focus: var(--secondary-color-focus)
$secondary-color-disabled: var(--secondary-color-disabled)
$secondary-color-active: var(--secondary-color-active)
$background-color: var(--background-color)
$background-color-content: var(--background-color-content)
$background-color-content-hover: var(--background-color-content-hover)
$background-color-content-focus: var(--background-color-content-focus)
$background-color-content-active: var(--background-color-content-active)
$background-color-content-disabled: var(--background-color-content-disabled)
$shadow-color: var(--shadow-color)
$border-radius: var(--border-radius)
$transition-duration: var(--transition-duration)

View file

@ -1,8 +1,6 @@
@import 'root' @import 'root'
@import 'mixin' @import 'mixin'
@import 'content' @import 'content'
// Bootstrap with legacy import (suppressed warnings via Vite config)
@import "../../node_modules/bootstrap/scss/bootstrap" @import "../../node_modules/bootstrap/scss/bootstrap"
body body

View file

@ -1,120 +1,33 @@
import { Session } from './classes/session'; import { Session } from "./classes/session";
import { BasicButton } from './components/button'; import { BasicButton } from "./components/button";
import './sass/app.sass'; import "./sass/app.sass";
class Settings { class Settings {
private session: Session | null = null;
private session = Session.getInstance();
constructor() { constructor() {
this.init(); this.renderSettings();
}
private async init(): Promise<void> {
try {
this.session = await Session.getInstance();
await this.renderSettings();
} catch (error) {
console.error('Failed to initialize settings:', error);
this.handleError('Failed to load settings');
}
} }
private async renderSettings(): Promise<void> { private async renderSettings(): Promise<void> {
if (!this.session) { const settings = <HTMLDivElement>document.getElementById('settings');
throw new Error('Session not initialized');
}
const settingsElement = document.getElementById('settings') as HTMLDivElement | null;
if (!settingsElement) {
throw new Error('Settings element not found');
}
const saveButton = new BasicButton('success', 'Save', 'saveSettings').render(); const saveButton = new BasicButton('success', 'Save', 'saveSettings').render();
settings.innerHTML = `
settingsElement.innerHTML = `
<div class="form-group"> <div class="form-group">
<label for="contentTest">Content Test</label> <label for="contentTest">Content Test</label>
<input <input type="text" class="form-control text-input" id="contentTest" placeholder="Enter content test" value="${this.session.contentTest}">
type="text"
class="form-control text-input"
id="contentTest"
placeholder="Enter content test"
value="${this.escapeHtml(this.session.contentTest)}"
>
</div> </div>
${saveButton}
`; `;
settings.innerHTML += saveButton;
this.attachEventListeners(); const saveSettings = <HTMLButtonElement>document.getElementById('saveSettings');
} saveSettings.addEventListener('click', () => {
this.session.contentTest = (<HTMLInputElement>document.getElementById('contentTest')).value;
private attachEventListeners(): void { Session.save();
const saveButton = document.getElementById('saveSettings') as HTMLButtonElement | null; Session.reloadSession();
const contentInput = document.getElementById('contentTest') as HTMLInputElement | null;
if (!saveButton || !contentInput) {
console.error('Required elements not found');
return;
}
saveButton.addEventListener('click', async () => {
try {
await this.saveSettings(contentInput.value);
} catch (error) {
console.error('Failed to save settings:', error);
this.showNotification('Failed to save settings', 'error');
}
}); });
} }
private async saveSettings(contentTest: string): Promise<void> {
if (!this.session) {
throw new Error('Session not initialized');
}
this.session.contentTest = contentTest;
await this.session.save();
this.showNotification('Settings saved successfully!', 'success');
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
private showNotification(message: string, type: 'success' | 'error'): void {
// Simple notification - could be enhanced with a proper notification system
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 10px 20px;
border-radius: 4px;
color: white;
background-color: ${type === 'success' ? '#28a745' : '#dc3545'};
z-index: 1000;
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
}
private handleError(message: string): void {
console.error(message);
const settingsElement = document.getElementById('settings');
if (settingsElement) {
settingsElement.innerHTML = `<div class="error-message">${message}</div>`;
}
}
} }
new Settings(); new Settings();

View file

@ -1,10 +1 @@
export type customButton = export type customButton = "neutral" | "primary" | "secondary" | "success" | "danger" | "warning" | "info" | "light" | "dark";
| 'neutral'
| 'primary'
| 'secondary'
| 'success'
| 'danger'
| 'warning'
| 'info'
| 'light'
| 'dark';

View file

@ -1,7 +1,7 @@
import * as fs from 'fs'; import * as fs from "fs";
import * as path from 'path'; import * as path from "path";
const appConfig = JSON.parse(fs.readFileSync('./app.config.json', 'utf8')); const appConfig = JSON.parse(fs.readFileSync("./app.config.json", "utf8"));
const DEPLOY_TARGET = './dist/'; const DEPLOY_TARGET = "./dist/";
function findCssFileNames(source: string): string[] { function findCssFileNames(source: string): string[] {
let files: string[] = []; let files: string[] = [];
@ -12,7 +12,7 @@ function findCssFileNames(source: string): string[] {
if (stat.isDirectory()) { if (stat.isDirectory()) {
files = files.concat(findCssFileNames(sourceFile)); files = files.concat(findCssFileNames(sourceFile));
} else { } else {
if (path.extname(sourceFile) === '.css') { if (path.extname(sourceFile) == ".css") {
files.push(file); files.push(file);
} }
} }
@ -29,7 +29,7 @@ function findHtmlFilesRecursive(source: string): string[] {
if (stat.isDirectory()) { if (stat.isDirectory()) {
files = files.concat(findHtmlFilesRecursive(sourceFile)); files = files.concat(findHtmlFilesRecursive(sourceFile));
} else { } else {
if (path.extname(sourceFile) == '.html') { if (path.extname(sourceFile) == ".html") {
files.push(sourceFile); files.push(sourceFile);
} }
} }
@ -38,13 +38,13 @@ function findHtmlFilesRecursive(source: string): string[] {
} }
function replaceKeywordsInHtmlFile(file: string) { function replaceKeywordsInHtmlFile(file: string) {
let content = fs.readFileSync(file, 'utf8'); let content = fs.readFileSync(file, "utf8");
const pairs: { key: string; value: string }[] = appConfig.htmlTemplatePairs; const pairs: { key: string; value: string }[] = appConfig.htmlTemplatePairs;
pairs.forEach(function (pair: { key: string; value: string }) { pairs.forEach(function (pair: { key: string; value: string }) {
//@ts-ignore //@ts-ignore
content = content.replaceAll(pair.key, pair.value); content = content.replaceAll(pair.key, pair.value);
}); });
file = file.replace('public\\', DEPLOY_TARGET); file = file.replace("public\\", DEPLOY_TARGET);
fs.writeFileSync(file, content); fs.writeFileSync(file, content);
} }
@ -58,9 +58,9 @@ function buildHtmlFiles(source: string) {
findCssFileNames(DEPLOY_TARGET).forEach((file: string) => { findCssFileNames(DEPLOY_TARGET).forEach((file: string) => {
const files = findHtmlFilesRecursive(DEPLOY_TARGET); const files = findHtmlFilesRecursive(DEPLOY_TARGET);
files.forEach(function (htmlFile: string) { files.forEach(function (htmlFile: string) {
let content = fs.readFileSync(htmlFile, 'utf8'); let content = fs.readFileSync(htmlFile, "utf8");
content = content.replace( content = content.replace(
'</head>', "</head>",
`<link rel="stylesheet" href="./assets/${file}">\n</head>` `<link rel="stylesheet" href="./assets/${file}">\n</head>`
); );
fs.writeFileSync(htmlFile, content); fs.writeFileSync(htmlFile, content);
@ -69,4 +69,4 @@ findCssFileNames(DEPLOY_TARGET).forEach((file: string) => {
buildHtmlFiles(DEPLOY_TARGET); buildHtmlFiles(DEPLOY_TARGET);
console.log('Parsed Files: ', findHtmlFilesRecursive(DEPLOY_TARGET)); console.log("Parsed Files: ", findHtmlFilesRecursive(DEPLOY_TARGET));

View file

@ -1,151 +1,23 @@
import * as fs from 'fs'; // @ts-ignore
const fs = require('fs');
interface AppConfig { const appConfig = JSON.parse(fs.readFileSync('./app.config.json', 'utf8'));
AppData: { const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
id: string; const manifestJson = JSON.parse(fs.readFileSync('./public/manifest.json', 'utf8'));
name: string;
version: string;
description: string;
repository: {
type: string;
url: string;
};
license: string;
homepage: string;
bugs: {
url: string;
};
authors: Array<{
name: string;
email: string;
}>;
};
htmlTemplatePairs: Array<{
key: string;
value: string;
}>;
}
interface PackageJson { pkg.version = appConfig.AppData.version;
version: string; pkg.name = appConfig.AppData.id;
name: string; pkg.authors = appConfig.AppData.authors;
authors: Array<{ pkg.description = appConfig.AppData.description;
name: string; pkg.homepage = appConfig.AppData.homepage;
email: string; pkg.license = appConfig.AppData.license;
}>; pkg.repository = appConfig.AppData.repository;
description: string; pkg.bugs = appConfig.AppData.bugs;
homepage: string;
license: string;
repository: {
type: string;
url: string;
};
bugs: {
url: string;
};
[key: string]: unknown;
}
interface ManifestJson { manifestJson.version = appConfig.AppData.version;
version: string; manifestJson.name = appConfig.AppData.name;
name: string; manifestJson.description = appConfig.AppData.description;
description: string; manifestJson.homepage_url = appConfig.AppData.homepage;
homepage_url: string;
[key: string]: unknown;
}
class ConfigSyncer { fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2));
private readonly appConfigPath = './app.config.json'; fs.writeFileSync('./public/manifest.json', JSON.stringify(manifestJson, null, 2));
private readonly packageJsonPath = './package.json';
private readonly manifestJsonPath = './public/manifest.json';
public async sync(): Promise<void> {
try {
console.log('Starting configuration synchronization...');
const appConfig = await this.loadAppConfig();
const packageJson = await this.loadPackageJson();
const manifestJson = await this.loadManifestJson();
this.updatePackageJson(packageJson, appConfig.AppData);
this.updateManifestJson(manifestJson, appConfig.AppData);
await this.savePackageJson(packageJson);
await this.saveManifestJson(manifestJson);
console.log('Configuration synchronization completed successfully!');
} catch (error) {
console.error('Failed to sync configuration:', error);
process.exit(1);
}
}
private async loadAppConfig(): Promise<AppConfig> {
try {
const content = await fs.promises.readFile(this.appConfigPath, 'utf8');
return JSON.parse(content) as AppConfig;
} catch (error) {
throw new Error(`Failed to load app config: ${error}`);
}
}
private async loadPackageJson(): Promise<PackageJson> {
try {
const content = await fs.promises.readFile(this.packageJsonPath, 'utf8');
return JSON.parse(content) as PackageJson;
} catch (error) {
throw new Error(`Failed to load package.json: ${error}`);
}
}
private async loadManifestJson(): Promise<ManifestJson> {
try {
const content = await fs.promises.readFile(this.manifestJsonPath, 'utf8');
return JSON.parse(content) as ManifestJson;
} catch (error) {
throw new Error(`Failed to load manifest.json: ${error}`);
}
}
private updatePackageJson(packageJson: PackageJson, appData: AppConfig['AppData']): void {
packageJson.version = appData.version;
packageJson.name = appData.id;
packageJson.authors = appData.authors;
packageJson.description = appData.description;
packageJson.homepage = appData.homepage;
packageJson.license = appData.license;
packageJson.repository = appData.repository;
packageJson.bugs = appData.bugs;
}
private updateManifestJson(manifestJson: ManifestJson, appData: AppConfig['AppData']): void {
manifestJson.version = appData.version;
manifestJson.name = appData.name;
manifestJson.description = appData.description;
manifestJson.homepage_url = appData.homepage;
}
private async savePackageJson(packageJson: PackageJson): Promise<void> {
try {
const content = JSON.stringify(packageJson, null, 2);
await fs.promises.writeFile(this.packageJsonPath, content, 'utf8');
console.log('✓ package.json updated');
} catch (error) {
throw new Error(`Failed to save package.json: ${error}`);
}
}
private async saveManifestJson(manifestJson: ManifestJson): Promise<void> {
try {
const content = JSON.stringify(manifestJson, null, 2);
await fs.promises.writeFile(this.manifestJsonPath, content, 'utf8');
console.log('✓ manifest.json updated');
} catch (error) {
throw new Error(`Failed to save manifest.json: ${error}`);
}
}
}
// Run the sync process
const syncer = new ConfigSyncer();
syncer.sync().catch(console.error);

View file

@ -3,44 +3,44 @@ const fs = require('fs');
const manifest = JSON.parse(fs.readFileSync('./dist/manifest.json', 'utf8')); const manifest = JSON.parse(fs.readFileSync('./dist/manifest.json', 'utf8'));
manifest.manifest_version = 2; manifest.manifest_version = 2
manifest.background.scripts = []; manifest.background.scripts = []
manifest.background.scripts.push(manifest.background.service_worker); manifest.background.scripts.push(manifest.background.service_worker)
delete manifest.background.type; delete manifest.background.type
delete manifest.background.service_worker; delete manifest.background.service_worker
manifest.background.persistent = true; manifest.background.persistent = true
if (manifest.host_permissions) { if (manifest.host_permissions) {
manifest.permissions.push(manifest.host_permissions); manifest.permissions.push(manifest.host_permissions)
} }
if (manifest.optional_host_permissions) { if (manifest.optional_host_permissions) {
manifest.permissions.push(manifest.optional_host_permissions); manifest.permissions.push(manifest.optional_host_permissions)
} }
delete manifest.host_permissions; delete manifest.host_permissions
delete manifest.optional_host_permissions; delete manifest.optional_host_permissions
let newContentSecurityPolicy = ''; let newContentSecurityPolicy = ""
try { try {
for (const policy of manifest.content_security_policy) { for (const policy of manifest.content_security_policy) {
newContentSecurityPolicy += policy.key + "'" + policy.value + "'" + ' '; newContentSecurityPolicy += policy.key + "'" + policy.value + "'" + " "
} }
} catch (e) { } catch (e) {
newContentSecurityPolicy = "default-src 'self'"; newContentSecurityPolicy = "default-src 'self'"
} }
manifest.content_security_policy = newContentSecurityPolicy; manifest.content_security_policy = newContentSecurityPolicy
try { try {
manifest.web_accessible_resources = manifest.web_accessible_resources.resources; manifest.web_accessible_resources = manifest.web_accessible_resources.resources
} catch (e) { } catch (e) {
manifest.web_accessible_resources = []; manifest.web_accessible_resources = []
} }
if (manifest.action) { if (manifest.action) {
manifest.browser_action = manifest.action; manifest.browser_action = manifest.action
} }
delete manifest.action; delete manifest.action
fs.writeFileSync('./dist/manifest.json', JSON.stringify(manifest, null, 2)); fs.writeFileSync('./dist/manifest.json', JSON.stringify(manifest, null, 2));

View file

@ -1,49 +1,75 @@
{ {
"compilerOptions": { "compilerOptions": {
/* Language and Environment */ /* Visit https://aka.ms/tsconfig.json to read more about this file */
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
/* Type Checking */ /* Basic Options */
"strict": true, // "incremental": true, /* Enable incremental compilation */
"noImplicitAny": true, "target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
"strictNullChecks": true, "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
"strictFunctionTypes": true, "lib": ["ESNext", "DOM"], /* Specify library files to be included in the compilation. */
"strictBindCallApply": true, // "allowJs": true, /* Allow javascript files to be compiled. */
"strictPropertyInitialization": true, // "checkJs": true, /* Report errors in .js files. */
"noImplicitThis": true, // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
"alwaysStrict": true, "declaration": true, /* Generates corresponding '.d.ts' file. */
"noUnusedLocals": true, "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"noUnusedParameters": true, "sourceMap": true /* Generates corresponding '.map' file. */,
"exactOptionalPropertyTypes": true, // "outFile": "./public/js/app.js", /* Concatenate and emit output to single file. */
"noImplicitReturns": true, "outDir": "./dist/js/" /* Redirect output structure to the directory. */,
"noFallthroughCasesInSwitch": true, // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"noUncheckedIndexedAccess": true, // "composite": true, /* Enable project compilation */
"noImplicitOverride": true, // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
"noPropertyAccessFromIndexSignature": true, "removeComments": true /* Do not emit comments to output. */,
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
"moduleResolution": "node",
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Emit */ /* Additional Checks */
"declaration": true, // "noUnusedLocals": true, /* Report errors on unused locals. */
"declarationMap": true, // "noUnusedParameters": true, /* Report errors on unused parameters. */
"sourceMap": true, // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"outDir": "./dist", // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
"removeComments": false, // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
"importHelpers": true, // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Interop Constraints */ /* Module Resolution Options */
"esModuleInterop": true, // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"allowSyntheticDefaultImports": true, // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"forceConsistentCasingInFileNames": true, // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Completeness */ /* Source Map Options */
"skipLibCheck": true // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}, },
"include": ["src/**/*.ts"], "include": ["./src/**/*.ts", "./src/*.ts"],
"exclude": ["node_modules", "dist", "tools/*.js"] "exclude": ["node_modules", "./tools/*.ts", "./dist/*.ts", "./dist/*.js"],
"types": ["node", "chrome"]
} }

View file

@ -1,71 +1,28 @@
import { resolve } from 'path'; import { defineConfig } from "vite";
import { defineConfig } from 'vite'; import { resolve } from "path";
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({ export default defineConfig({
build: { build: {
rollupOptions: { rollupOptions: {
input: { input: {
app: resolve(__dirname, 'src/app.ts'), app: resolve(__dirname, "src/app.ts"),
settings: resolve(__dirname, 'src/settings.ts'), settings: resolve(__dirname, "src/settings.ts"),
background: resolve(__dirname, 'src/background.ts'), background: resolve(__dirname, "src/background.ts"),
}, },
output: { output: {
entryFileNames: '[name].js', entryFileNames: "[name].js",
chunkFileNames: 'chunks/[name]-[hash].js', dir: resolve(__dirname, "dist"),
assetFileNames: assetInfo => {
if (assetInfo.name?.endsWith('.css')) {
return 'assets/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
},
dir: resolve(__dirname, 'dist'),
}, },
}, },
sourcemap: true, sourcemap: true,
target: 'es2022',
minify: 'esbuild',
reportCompressedSize: false,
chunkSizeWarningLimit: 1000,
}, },
plugins: [tsconfigPaths()], plugins: [tsconfigPaths()],
resolve: { resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx', '.scss', '.sass'], extensions: [".tsx", ".ts", ".scss", ".sass"],
alias: {
'@': resolve(__dirname, './src'),
'@components': resolve(__dirname, './src/components'),
'@classes': resolve(__dirname, './src/classes'),
'@types': resolve(__dirname, './src/types'),
'@sass': resolve(__dirname, './src/sass'),
},
}, },
esbuild: { esbuild: {
target: 'es2022',
include: /.*\.tsx?$/, include: /.*\.tsx?$/,
exclude: [/node_modules/, /dist/], exclude: [/node_modules/, /dist/],
legalComments: 'none',
},
define: {
__DEV__: JSON.stringify(process.env.NODE_ENV === 'development'),
__VERSION__: JSON.stringify(process.env.npm_package_version || '0.0.1'),
},
css: {
preprocessorOptions: {
sass: {
additionalData: `@import "@sass/_root.sass"\n@import "@sass/_mixin.sass"\n`,
quietDeps: true,
verbose: false,
charset: false,
silenceDeprecations: [
'import',
'global-builtin',
'color-functions',
'legacy-js-api',
'mixed-decls',
'slash-div',
],
},
},
devSourcemap: true,
}, },
}); });