Merge pull request #14 from JosunLP/dev
Some checks failed
CodeQL / Analyze (push) Has been cancelled

Release 1.7.0
This commit is contained in:
Jonas Pfalzgraf 2025-07-11 17:10:24 +02:00 committed by GitHub
commit 11ecc63799
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1321 additions and 418 deletions

31
.eslintrc.json Normal file
View file

@ -0,0 +1,31 @@
{
"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"]
}

8
.prettierignore Normal file
View file

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

19
.prettierrc.json Normal file
View file

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

215
README.md
View file

@ -4,35 +4,222 @@
[![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 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)
[![Known Vulnerabilities](https://snyk.io/test/github/JosunLP/BrowserExtensionTemplate/badge.svg?style=for-the-badge)](https://snyk.io/test/github/JosunLP/BrowserExtensionTemplate)
## Description
A basic template based on SASS and TypeScript to create browser extensions without directly relying on a larger framework.
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.
## 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
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`.
Alternatively, you can fork the project and run `npm install` in the forked project.
### Quick Start
```bash
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
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.
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.
This is necessary because the firefox web store needs the `manifest.json` file to be present in the version v2.
### Project Structure
## License
```bash
src/
├── 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
This project is licensed under the [MIT license](https://opensource.org/licenses/MIT).
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
This project is open source. Feel free to fork and contribute!
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
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
## 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,29 +1,29 @@
{
"AppData": {
"id": "browser_extension_template",
"name": "Browser Extension Template",
"version": "0.0.1",
"description": "A basic template based on SASS and TypeScript to create browser extensions without directly relying on a larger framework.",
"repository": {
"type": "git",
"url": "git+ssh://git@github.com:JosunLP/BrowserExtensionTemplate.git"
},
"license": "MIT",
"homepage": "https://github.com/JosunLP/BrowserExtensionTemplate",
"bugs": {
"url": "https://github.com/JosunLP/BrowserExtensionTemplate/issues"
},
"authors": [
{
"name": "Jonas Pfalzgraf",
"email": "info@josunlp.de"
}
]
"AppData": {
"id": "browser_extension_template",
"name": "Browser Extension Template",
"version": "0.0.1",
"description": "A basic template based on SASS and TypeScript to create browser extensions without directly relying on a larger framework.",
"repository": {
"type": "git",
"url": "git+ssh://git@github.com:JosunLP/BrowserExtensionTemplate.git"
},
"htmlTemplatePairs": [
{
"key": "{{BET}}",
"value": "Browser Extension Template"
}
"license": "MIT",
"homepage": "https://github.com/JosunLP/BrowserExtensionTemplate",
"bugs": {
"url": "https://github.com/JosunLP/BrowserExtensionTemplate/issues"
},
"authors": [
{
"name": "Jonas Pfalzgraf",
"email": "info@josunlp.de"
}
]
},
"htmlTemplatePairs": [
{
"key": "{{BET}}",
"value": "Browser Extension Template"
}
]
}

55
eslint.config.js Normal file
View file

@ -0,0 +1,55 @@
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,20 +2,39 @@
"name": "browser_extension_template",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"deploy-v3": "npx rimraf ./dist/ && npm run build-tooling && npm run sync && npm run build && node ./tools/parse.js",
"deploy-v3": "npm run clean && npm run build-tooling && npm run sync && npm run build && npm run parse",
"deploy-v2": "npm run deploy-v3 && node ./tools/v2.js",
"build": "vite build",
"build-tooling": "tsc --project ./tooling.tsconfig.json",
"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": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.30.1",
"@types/chrome": "^0.0.268",
"@types/node": "^20.13.0",
"@typescript-eslint/eslint-plugin": "^8.36.0",
"@typescript-eslint/parser": "^8.36.0",
"@webcomponents/webcomponentsjs": "^2.8.0",
"eslint": "^9.30.1",
"prettier": "^3.6.2",
"rimraf": "^5.0.10",
"sass": "^1.77.4",
"vite": "^5.2.12",
"typescript": "^5.8.3",
"vite": "^7.0.4",
"vite-tsconfig-paths": "^4.3.2"
},
"browserslist": [

View file

@ -15,12 +15,27 @@
"default_popup": "popup.html"
},
"options_ui": {
"page": "options.html"
"page": "options.html",
"open_in_tab": false
},
"permissions": [
"storage",
"notifications"
],
"background": {
"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>
<html lang="">
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="./favicon.ico">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="./favicon.ico" />
<title>{{BET}} Options</title>
</head>
<body>
<noscript>
<strong>We're sorry but {{BET}} doesn't work properly without JavaScript enabled. Please enable it to
continue.</strong>
<strong
>We're sorry but {{BET}} doesn't work properly without JavaScript enabled. Please enable it
to continue.</strong
>
</noscript>
<div id="app">
<img src="./icons/icon128.png" class="logo" />
<img src="./icons/icon128.png" alt="Logo" class="logo" />
<h1>Settings</h1>
<div id="settings">
</div>
<div id="settings"></div>
</div>
<footer>
<script type="module" src="./settings.js"></script>

View file

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

View file

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

View file

@ -1,9 +1,82 @@
class Background {
constructor() {
this.main();
}
interface ExtensionMessage {
type: string;
payload?: unknown;
}
async main(): Promise<void> {}
class Background {
constructor() {
this.init();
}
private async init(): 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();

View file

@ -0,0 +1,99 @@
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,55 +1,109 @@
export class Session {
private static instance: Session;
private constructor() {
}
static getInstance() {
if (!Session.instance && !Session.load()) {
Session.instance = new Session();
}
if (!Session.instance && Session.load()) {
Session.instance = <Session>Session.load();
}
Session.save();
return Session.instance;
}
public static save() {
localStorage.setItem('session', JSON.stringify(this.instance));
}
public static load(): Session | null {
const session = localStorage.getItem('session');
if (session) {
const obj = <Session>JSON.parse(session);
const result = new Session();
result.contentTest = obj.contentTest;
return result;
}
return null;
}
public static reloadSession() {
const session = localStorage.getItem('session');
if (session) {
const obj = <Session>JSON.parse(session);
const result = new Session();
result.contentTest = obj.contentTest;
Session.instance = result;
}
}
public static resetSession() {
localStorage.removeItem('session');
sessionStorage.removeItem('session');
this.instance = new Session();
Session.save();
location.reload();
}
public readonly sessionId: string = crypto.randomUUID();
public contentTest: string = 'This is a simple example of a web application';
interface SessionData {
sessionId: string;
contentTest: string;
}
interface StorageService {
save(key: string, data: unknown): Promise<void>;
load<T>(key: string): Promise<T | null>;
remove(key: string): Promise<void>;
}
class LocalStorageService implements StorageService {
async save(key: string, data: unknown): Promise<void> {
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> {
try {
const item = localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : null;
} catch (error) {
console.error('Failed to load from localStorage:', error);
return null;
}
}
async remove(key: string): Promise<void> {
try {
localStorage.removeItem(key);
} catch (error) {
console.error('Failed to remove from localStorage:', error);
throw new Error('Storage operation failed');
}
}
}
export class Session implements SessionData {
private static instance: Session | null = null;
private static readonly STORAGE_KEY = 'browser_extension_session';
private static readonly storageService: StorageService = new LocalStorageService();
public readonly sessionId: string;
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> {
if (!Session.instance) {
await Session.loadOrCreate();
}
return Session.instance!;
}
private static async loadOrCreate(): Promise<void> {
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,63 +1,96 @@
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 {
private readonly config: ButtonConfig;
private type: customButton;
constructor(type: customButton, text: string, id?: string, className?: string) {
this.config = {
type,
text,
id,
className,
};
}
private text: string;
public render(): string {
const button = document.createElement('button');
button.type = 'button';
button.textContent = this.config.text;
button.className = this.getBootstrapClass();
private id: string | undefined;
private className: string | undefined;
constructor(type: customButton, text: string, id?: string, className?: string) {
this.type = type;
this.text = text;
this.id = id;
this.className = className;
if (this.config.id) {
button.id = this.config.id;
}
public render(): string {
const result = document.createElement('button');
result.type = "button";
result.className = this.type;
result.textContent = this.text;
switch (this.type) {
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.id) {
result.id = this.id;
}
if (this.className) {
result.className += ' ' + this.className;
}
return result.outerHTML;
if (this.config.className) {
button.className += ` ${this.config.className}`;
}
if (this.config.disabled) {
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,26 +1,64 @@
$main-font: 'Ubuntu', 'Staatliches'
$main-font-color: white
$main-font-color-hover: lightgrey
$main-font-color-focus: lightgrey
$main-font-color-disabled: lightgrey
$main-font-color-active: lightgrey
$main-uschrift-font: 'Ubuntu', Arial
$primary-color: #007bff
$primary-color-hover: #0069d9
$primary-color-focus: #0062cc
$primary-color-disabled: #0069d9
$primary-color-active: #0062cc
$background-color-content-shadow: #0069d9
$seccond-color: #6c757d
$seccondary-color: darkgrey
$seccondary-color-hover: black
$seccondary-color-focus: black
$seccondary-color-disabled: black
$seccondary-color-active: black
$background-color: #77B2FF
$background-color-content: rgb(198, 223, 255)
$background-color-content-hover: rgb(198, 223, 255)
$background-color-content-focus: rgb(198, 223, 255)
$background-color-content-active: rgb(198, 223, 255)
$background-color-content-disabled: rgb(198, 223, 255)
$logo-image: url('../icons/icon128.png')
// CSS Custom Properties for theming support
:root
--main-font: 'Ubuntu', 'Segoe UI', 'Roboto', sans-serif
--main-font-color: #ffffff
--main-font-color-hover: #f8f9fa
--main-font-color-focus: #e9ecef
--main-font-color-disabled: #6c757d
--main-font-color-active: #f8f9fa
--primary-color: #007bff
--primary-color-hover: #0056b3
--primary-color-focus: #004085
--primary-color-disabled: #6c757d
--primary-color-active: #004085
--secondary-color: #6c757d
--secondary-color-hover: #545b62
--secondary-color-focus: #4e555b
--secondary-color-disabled: #adb5bd
--secondary-color-active: #4e555b
--background-color: #77B2FF
--background-color-content: #c6dfff
--background-color-content-hover: #b3d7ff
--background-color-content-focus: #9fcdff
--background-color-content-active: #8cc4ff
--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,6 +1,8 @@
@import 'root'
@import 'mixin'
@import 'content'
// Bootstrap with legacy import (suppressed warnings via Vite config)
@import "../../node_modules/bootstrap/scss/bootstrap"
body

View file

@ -1,33 +1,120 @@
import { Session } from "./classes/session";
import { BasicButton } from "./components/button";
import "./sass/app.sass";
import { Session } from './classes/session';
import { BasicButton } from './components/button';
import './sass/app.sass';
class Settings {
private session: Session | null = null;
private session = Session.getInstance();
constructor() {
this.init();
}
constructor() {
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> {
if (!this.session) {
throw new Error('Session not initialized');
}
private async renderSettings(): Promise<void> {
const settings = <HTMLDivElement>document.getElementById('settings');
const saveButton = new BasicButton('success', 'Save', 'saveSettings').render();
settings.innerHTML = `
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();
settingsElement.innerHTML = `
<div class="form-group">
<label for="contentTest">Content Test</label>
<input type="text" class="form-control text-input" id="contentTest" placeholder="Enter content test" value="${this.session.contentTest}">
<label for="contentTest">Content Test</label>
<input
type="text"
class="form-control text-input"
id="contentTest"
placeholder="Enter content test"
value="${this.escapeHtml(this.session.contentTest)}"
>
</div>
${saveButton}
`;
settings.innerHTML += saveButton;
const saveSettings = <HTMLButtonElement>document.getElementById('saveSettings');
saveSettings.addEventListener('click', () => {
this.session.contentTest = (<HTMLInputElement>document.getElementById('contentTest')).value;
Session.save();
Session.reloadSession();
});
this.attachEventListeners();
}
private attachEventListeners(): void {
const saveButton = document.getElementById('saveSettings') as HTMLButtonElement | null;
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();

View file

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

View file

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

View file

@ -1,23 +1,151 @@
// @ts-ignore
const fs = require('fs');
import * as fs from 'fs';
const appConfig = JSON.parse(fs.readFileSync('./app.config.json', 'utf8'));
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
const manifestJson = JSON.parse(fs.readFileSync('./public/manifest.json', 'utf8'));
interface AppConfig {
AppData: {
id: string;
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;
}>;
}
pkg.version = appConfig.AppData.version;
pkg.name = appConfig.AppData.id;
pkg.authors = appConfig.AppData.authors;
pkg.description = appConfig.AppData.description;
pkg.homepage = appConfig.AppData.homepage;
pkg.license = appConfig.AppData.license;
pkg.repository = appConfig.AppData.repository;
pkg.bugs = appConfig.AppData.bugs;
interface PackageJson {
version: string;
name: string;
authors: Array<{
name: string;
email: string;
}>;
description: string;
homepage: string;
license: string;
repository: {
type: string;
url: string;
};
bugs: {
url: string;
};
[key: string]: unknown;
}
manifestJson.version = appConfig.AppData.version;
manifestJson.name = appConfig.AppData.name;
manifestJson.description = appConfig.AppData.description;
manifestJson.homepage_url = appConfig.AppData.homepage;
interface ManifestJson {
version: string;
name: string;
description: string;
homepage_url: string;
[key: string]: unknown;
}
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2));
fs.writeFileSync('./public/manifest.json', JSON.stringify(manifestJson, null, 2));
class ConfigSyncer {
private readonly appConfigPath = './app.config.json';
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'));
manifest.manifest_version = 2
manifest.manifest_version = 2;
manifest.background.scripts = []
manifest.background.scripts = [];
manifest.background.scripts.push(manifest.background.service_worker)
delete manifest.background.type
delete manifest.background.service_worker
manifest.background.persistent = true
manifest.background.scripts.push(manifest.background.service_worker);
delete manifest.background.type;
delete manifest.background.service_worker;
manifest.background.persistent = true;
if (manifest.host_permissions) {
manifest.permissions.push(manifest.host_permissions)
manifest.permissions.push(manifest.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.optional_host_permissions
delete manifest.host_permissions;
delete manifest.optional_host_permissions;
let newContentSecurityPolicy = ""
let newContentSecurityPolicy = '';
try {
for (const policy of manifest.content_security_policy) {
newContentSecurityPolicy += policy.key + "'" + policy.value + "'" + " "
}
for (const policy of manifest.content_security_policy) {
newContentSecurityPolicy += policy.key + "'" + policy.value + "'" + ' ';
}
} catch (e) {
newContentSecurityPolicy = "default-src 'self'"
newContentSecurityPolicy = "default-src 'self'";
}
manifest.content_security_policy = newContentSecurityPolicy
manifest.content_security_policy = newContentSecurityPolicy;
try {
manifest.web_accessible_resources = manifest.web_accessible_resources.resources
manifest.web_accessible_resources = manifest.web_accessible_resources.resources;
} catch (e) {
manifest.web_accessible_resources = []
manifest.web_accessible_resources = [];
}
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));

View file

@ -1,75 +1,49 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
"compilerOptions": {
/* Language and Environment */
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
"module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
"lib": ["ESNext", "DOM"], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
"declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true /* Generates corresponding '.map' file. */,
// "outFile": "./public/js/app.js", /* Concatenate and emit output to single file. */
"outDir": "./dist/js/" /* Redirect output structure to the directory. */,
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
"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. */
/* Type Checking */
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "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. */
/* Emit */
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"removeComments": false,
"importHelpers": true,
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "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. */
/* Interop Constraints */
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
/* Source Map Options */
// "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", "./src/*.ts"],
"exclude": ["node_modules", "./tools/*.ts", "./dist/*.ts", "./dist/*.js"],
"types": ["node", "chrome"]
/* Completeness */
"skipLibCheck": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "tools/*.js"]
}

View file

@ -1,28 +1,71 @@
import { defineConfig } from "vite";
import { resolve } from "path";
import tsconfigPaths from "vite-tsconfig-paths";
import { resolve } from 'path';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
build: {
rollupOptions: {
input: {
app: resolve(__dirname, "src/app.ts"),
settings: resolve(__dirname, "src/settings.ts"),
background: resolve(__dirname, "src/background.ts"),
},
output: {
entryFileNames: "[name].js",
dir: resolve(__dirname, "dist"),
},
},
sourcemap: true,
},
plugins: [tsconfigPaths()],
resolve: {
extensions: [".tsx", ".ts", ".scss", ".sass"],
},
esbuild: {
include: /.*\.tsx?$/,
exclude: [/node_modules/, /dist/],
},
build: {
rollupOptions: {
input: {
app: resolve(__dirname, 'src/app.ts'),
settings: resolve(__dirname, 'src/settings.ts'),
background: resolve(__dirname, 'src/background.ts'),
},
output: {
entryFileNames: '[name].js',
chunkFileNames: 'chunks/[name]-[hash].js',
assetFileNames: assetInfo => {
if (assetInfo.name?.endsWith('.css')) {
return 'assets/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
},
dir: resolve(__dirname, 'dist'),
},
},
sourcemap: true,
target: 'es2022',
minify: 'esbuild',
reportCompressedSize: false,
chunkSizeWarningLimit: 1000,
},
plugins: [tsconfigPaths()],
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx', '.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: {
target: 'es2022',
include: /.*\.tsx?$/,
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,
},
});