diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..83d0a6a --- /dev/null +++ b/.eslintrc.json @@ -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"] +} diff --git a/.gitignore b/.gitignore index 786253e..7a21a8d 100644 --- a/.gitignore +++ b/.gitignore @@ -688,7 +688,7 @@ FodyWeavers.xsd dist tools/syncConfig.js -tools/deploy.js +tools/parse.js tools/v2.js tools/clean.js package-lock.json diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..611cf4f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +*.log +.DS_Store +.vscode/ +.idea/ +*.sass +*.scss diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..aa3f49e --- /dev/null +++ b/.prettierrc.json @@ -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" + } + } + ] +} diff --git a/LICENSE b/LICENSE index 0404ba7..79f09ef 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Jonas Pfalzgraf +Copyright (c) 2024 Jonas Pfalzgraf Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d9c9633..7ed4257 100644 --- a/README.md +++ b/README.md @@ -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: +- 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 diff --git a/app.config.json b/app.config.json index e5ceac0..96a13dc 100644 --- a/app.config.json +++ b/app.config.json @@ -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" + } ] -} \ No newline at end of file + }, + "htmlTemplatePairs": [ + { + "key": "{{BET}}", + "value": "Browser Extension Template" + } + ] +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..0a5419c --- /dev/null +++ b/eslint.config.js @@ -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', + }, + }, +]; diff --git a/package.json b/package.json index a383aac..1bfa990 100644 --- a/package.json +++ b/package.json @@ -4,24 +4,38 @@ "private": true, "type": "module", "scripts": { - "deploy-v3": "npm run build-tooling && node ./tools/deploy.js && npm run sync && npm run build-js && npm run build-css && node ./tools/clean.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-js": "webpack --config ./webpack.config.cjs && webpack --config ./webpack.config.settings.cjs && webpack --config ./webpack.config.background.cjs", - "build-css": "sass ./src/sass/:./dist/css/", + "build": "vite build", "build-tooling": "tsc --project ./tooling.tsconfig.json", - "watch-ts": "tsc -w -p tsconfig.json", - "watch-sass": "sass --watch ./src/sass/:./dist/css/", - "sync": "npm run build-tooling && node ./tools/syncConfig.js" + "watch": "vite build --watch", + "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": { - "@types/chrome": "^0.0.206", - "@types/node": "^18.11.18", - "@webcomponents/webcomponentsjs": "^2.7.0", - "sass": "^1.39.0", - "ts-loader": "^9.4.2", - "typescript": "^4.2.4", - "webpack": "^5.75.0", - "webpack-cli": "^5.0.1" + "@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", + "typescript": "^5.8.3", + "vite": "^7.0.4", + "vite-tsconfig-paths": "^4.3.2" }, "browserslist": [ "> 1%", @@ -45,7 +59,7 @@ "url": "https://github.com/JosunLP/BrowserExtensionTemplate/issues" }, "dependencies": { - "@webcomponents/custom-elements": "^1.5.1", - "bootstrap": "^5.2.3" + "@webcomponents/custom-elements": "^1.6.0", + "bootstrap": "^5.3.3" } } \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index 2f57490..6d17cfa 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -15,21 +15,27 @@ "default_popup": "popup.html" }, "options_ui": { - "page": "options.html" + "page": "options.html", + "open_in_tab": false }, "permissions": [ + "storage", "notifications" ], "background": { - "service_worker": "js/background.js" + "service_worker": "background.js" }, - "commands": { - "_execute_browser_action": { - "suggested_key": { - "default": "Ctrl+Shift+F", - "mac": "MacCtrl+Shift+F" - }, - "description": "Opens popup.html" + "content_security_policy": { + "extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' 'unsafe-inline';" + }, + "web_accessible_resources": [ + { + "resources": [ + "icons/*.png" + ], + "matches": [ + "" + ] } - } + ] } \ No newline at end of file diff --git a/public/options.html b/public/options.html index 161fb1a..c9a0e6b 100644 --- a/public/options.html +++ b/public/options.html @@ -1,26 +1,25 @@ - - + + - - - - + + + {{BET}} Options -
- +

Settings

-
-
+
- +
diff --git a/public/popup.html b/public/popup.html index e467626..a59f164 100644 --- a/public/popup.html +++ b/public/popup.html @@ -1,26 +1,25 @@ - - + + - - - - + + + {{BET}} -
- +

{{BET}}

-
-
+
- +
diff --git a/src/app.ts b/src/app.ts index 3dc1fa6..8157802 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,30 +1,60 @@ -import { Session } from "./classes/session" +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 { + 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 { + console.log('Hello World'); + } + + private async drawData(): Promise { + if (!this.session) { + throw new Error('Session not initialized'); } - async main(): Promise { - 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 { - const session = Session.getInstance() - const contentRoot = 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 = `
${message}
`; } + } } new App(); diff --git a/src/background.ts b/src/background.ts index 197f02f..635339b 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,12 +1,82 @@ -class Background { - - constructor() { - this.main(); - } - - async main(): Promise { - - } +interface ExtensionMessage { + type: string; + payload?: unknown; } -new Background(); \ No newline at end of file +class Background { + constructor() { + this.init(); + } + + private async init(): Promise { + 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 { + // 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 { + 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 { + 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 { + // Main background logic can be implemented here + // This method is called after initialization + } +} + +new Background(); diff --git a/src/classes/errorBoundary.ts b/src/classes/errorBoundary.ts new file mode 100644 index 0000000..8e46dd2 --- /dev/null +++ b/src/classes/errorBoundary.ts @@ -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): 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( + fn: (...args: T) => Promise + ): (...args: T) => Promise { + return async (...args: T): Promise => { + try { + return await fn(...args); + } catch (error) { + this.handleError(error instanceof Error ? error : new Error(String(error))); + throw error; + } + }; + } + + public wrapSync(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; + } + }; + } +} diff --git a/src/classes/session.ts b/src/classes/session.ts index 3443233..9c9c472 100644 --- a/src/classes/session.ts +++ b/src/classes/session.ts @@ -1,55 +1,109 @@ -export class Session { +interface SessionData { + sessionId: string; + contentTest: string; +} - private static instance: Session; +interface StorageService { + save(key: string, data: unknown): Promise; + load(key: string): Promise; + remove(key: string): Promise; +} - private constructor() { +class LocalStorageService implements StorageService { + async save(key: string, data: unknown): Promise { + try { + localStorage.setItem(key, JSON.stringify(data)); + } catch (error) { + console.error('Failed to save to localStorage:', error); + throw new Error('Storage operation failed'); } + } - static getInstance() { - if (!Session.instance && !Session.load()) { - Session.instance = new Session(); - } - if (!Session.instance && Session.load()) { - Session.instance = Session.load(); - } - Session.save(); - return Session.instance; + async load(key: string): Promise { + 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; } + } - public static save() { - localStorage.setItem('session', JSON.stringify(this.instance)); + async remove(key: string): Promise { + try { + localStorage.removeItem(key); + } catch (error) { + console.error('Failed to remove from localStorage:', error); + throw new Error('Storage operation failed'); } + } +} - public static load(): Session | null { - const session = localStorage.getItem('session'); - if (session) { - const obj = JSON.parse(session); - const result = new Session(); - result.contentTest = obj.contentTest; - return result; - } - return null; +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) { + this.sessionId = data?.sessionId ?? crypto.randomUUID(); + this.contentTest = data?.contentTest ?? 'This is a simple example of a web application'; + } + + public static async getInstance(): Promise { + if (!Session.instance) { + await Session.loadOrCreate(); } + return Session.instance!; + } - public static reloadSession() { - const session = localStorage.getItem('session'); - if (session) { - const obj = JSON.parse(session); - const result = new Session(); - result.contentTest = obj.contentTest; - Session.instance = result; - } + private static async loadOrCreate(): Promise { + try { + const savedData = await Session.storageService.load(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 static resetSession() { - localStorage.removeItem('session'); - sessionStorage.removeItem('session'); - this.instance = new Session(); - Session.save(); - location.reload(); + public async save(): Promise { + 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 readonly sessionId: string = crypto.randomUUID(); + public static async reset(): Promise { + try { + await Session.storageService.remove(Session.STORAGE_KEY); + Session.instance = new Session(); + await Session.instance.save(); - public contentTest: string = 'This is a simple example of a web application'; -} \ No newline at end of file + // 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, + }; + } +} diff --git a/src/components/button.ts b/src/components/button.ts index 33f8a28..b023eaa 100644 --- a/src/components/button.ts +++ b/src/components/button.ts @@ -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 = { + 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; + } } diff --git a/src/sass/_root.sass b/src/sass/_root.sass index 3a50f8d..e96d116 100644 --- a/src/sass/_root.sass +++ b/src/sass/_root.sass @@ -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) diff --git a/src/sass/app.sass b/src/sass/app.sass index f953809..c0c29d1 100644 --- a/src/sass/app.sass +++ b/src/sass/app.sass @@ -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 diff --git a/src/settings.ts b/src/settings.ts index 24d6bb8..0f08c73 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,32 +1,120 @@ -import { Session } from "./classes/session"; -import { BasicButton } from "./components/button"; +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 { + 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 { + if (!this.session) { + throw new Error('Session not initialized'); } - private async renderSettings(): Promise { - const settings = 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 = `
- - + +
+ ${saveButton} `; - settings.innerHTML += saveButton; - const saveSettings = document.getElementById('saveSettings'); - saveSettings.addEventListener('click', () => { - this.session.contentTest = (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 { + 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 = `
${message}
`; + } + } } new Settings(); diff --git a/src/types/buttonType.ts b/src/types/buttonType.ts index 660d7f9..efdfd5f 100644 --- a/src/types/buttonType.ts +++ b/src/types/buttonType.ts @@ -1 +1,10 @@ -export type customButton = "neutral" | "primary" | "secondary" | "success" | "danger" | "warning" | "info" | "light" | "dark"; \ No newline at end of file +export type customButton = + | 'neutral' + | 'primary' + | 'secondary' + | 'success' + | 'danger' + | 'warning' + | 'info' + | 'light' + | 'dark'; diff --git a/tooling.tsconfig.json b/tooling.tsconfig.json index 84ac8bc..72730da 100644 --- a/tooling.tsconfig.json +++ b/tooling.tsconfig.json @@ -1,6 +1,9 @@ { "compilerOptions": { "target": "ESNext", + "lib": [ + "ESNext" + ], "module": "ESNext", "outDir": "./tools/", "rootDir": "./tools/", diff --git a/tools/clean.ts b/tools/clean.ts deleted file mode 100644 index caba573..0000000 --- a/tools/clean.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as fs from "fs"; - -function removeExport(file: string) { - let content = fs.readFileSync(file, "utf8"); - content = content.replace("export {};", ""); - fs.writeFileSync(file, content); -} - -removeExport("./dist/js/background.js"); diff --git a/tools/deploy.ts b/tools/deploy.ts deleted file mode 100644 index a12bf54..0000000 --- a/tools/deploy.ts +++ /dev/null @@ -1,84 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -const appConfig = JSON.parse(fs.readFileSync('./app.config.json', 'utf8')); -const DEPLOY_ENTRY = "./public/"; -const DEPLOY_TARGET = "./dist/"; - -function deleteFolderRecursive(path: string) { - if (fs.existsSync(path)) { - fs.readdirSync(path).forEach(function (file: string) { - const curPath = path + "/" + file; - if (fs.lstatSync(curPath).isDirectory()) { - deleteFolderRecursive(curPath); - } else { - fs.unlinkSync(curPath); - } - }); - fs.rmdirSync(path); - } -} - -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; -} - -function replaceKeywordsInHtmlFile(file: string) { - let content = fs.readFileSync(file, 'utf8'); - const pairs = appConfig.htmlTemplatePairs; - pairs.forEach(function (pair: object) { - // @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); - }); -} - -function mkdirSync(path: string) { - try { - fs.mkdirSync(path); - } catch (e: any) { - if (e.code != 'EEXIST') throw e; - } -} - -function copyFiles(source: string, target: string) { - const files = fs.readdirSync(source); - files.forEach(function (file: string) { - const sourceFile = path.join(source, file); - const targetFile = path.join(target, file); - const stat = fs.lstatSync(sourceFile); - if (stat.isDirectory()) { - mkdirSync(targetFile); - copyFiles(sourceFile, targetFile); - } else { - fs.writeFileSync(targetFile, fs.readFileSync(sourceFile)); - } - }); -} - -deleteFolderRecursive(DEPLOY_TARGET); -mkdirSync(DEPLOY_TARGET); -copyFiles(DEPLOY_ENTRY, DEPLOY_TARGET); -buildHtmlFiles(DEPLOY_ENTRY); - -console.log("Deployed to " + DEPLOY_TARGET); \ No newline at end of file diff --git a/tools/parse.ts b/tools/parse.ts new file mode 100644 index 0000000..de3f6a1 --- /dev/null +++ b/tools/parse.ts @@ -0,0 +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/'; + +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; +} + +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; +} + +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); +} + +function buildHtmlFiles(source: string) { + 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( + '', + `\n` + ); + fs.writeFileSync(htmlFile, content); + }); +}); + +buildHtmlFiles(DEPLOY_TARGET); + +console.log('Parsed Files: ', findHtmlFilesRecursive(DEPLOY_TARGET)); diff --git a/tools/syncConfig.ts b/tools/syncConfig.ts index 22b800c..6c4bc41 100644 --- a/tools/syncConfig.ts +++ b/tools/syncConfig.ts @@ -1,22 +1,151 @@ 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 manifest = 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; +} -manifest.version = appConfig.AppData.version; -manifest.name = appConfig.AppData.name; -manifest.description = appConfig.AppData.description; -manifest.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(manifest, null, 2)); \ No newline at end of file +class ConfigSyncer { + private readonly appConfigPath = './app.config.json'; + private readonly packageJsonPath = './package.json'; + private readonly manifestJsonPath = './public/manifest.json'; + + public async sync(): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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); diff --git a/tools/v2.ts b/tools/v2.ts index dd42ddb..dcb4007 100644 --- a/tools/v2.ts +++ b/tools/v2.ts @@ -1,45 +1,46 @@ -import * as fs from 'fs'; +// @ts-ignore +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)); \ No newline at end of file +fs.writeFileSync('./dist/manifest.json', JSON.stringify(manifest, null, 2)); diff --git a/tsconfig.json b/tsconfig.json index 3e8038b..96013de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"], /* 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"] } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..fdf26ed --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,71 @@ +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', + 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, + }, +}); diff --git a/webpack.config.background.cjs b/webpack.config.background.cjs deleted file mode 100644 index 7102629..0000000 --- a/webpack.config.background.cjs +++ /dev/null @@ -1,28 +0,0 @@ -const path = require('path'); - -module.exports = { - entry: './src/background.ts', - mode: 'production', - devtool: 'source-map', - module: { - rules: [ - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: [ - "/node_modules/", - "/dist/", - "/src/app.ts", - "/src/settings.ts" - ], - }, - ], - }, - resolve: { - extensions: ['.tsx', '.ts'], - }, - output: { - filename: 'background.js', - path: path.resolve(__dirname, 'dist/js'), - }, -}; diff --git a/webpack.config.cjs b/webpack.config.cjs deleted file mode 100644 index b7bf57e..0000000 --- a/webpack.config.cjs +++ /dev/null @@ -1,28 +0,0 @@ -const path = require('path'); - -module.exports = { - entry: './src/app.ts', - mode: 'production', - devtool: 'source-map', - module: { - rules: [ - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: [ - "/node_modules/", - "/dist/", - "/src/background.ts", - "/src/settings.ts" - ], - }, - ], - }, - resolve: { - extensions: ['.tsx', '.ts'], - }, - output: { - filename: 'app.js', - path: path.resolve(__dirname, 'dist/js'), - }, -}; diff --git a/webpack.config.settings.cjs b/webpack.config.settings.cjs deleted file mode 100644 index e8e1821..0000000 --- a/webpack.config.settings.cjs +++ /dev/null @@ -1,28 +0,0 @@ -const path = require('path'); - -module.exports = { - entry: './src/settings.ts', - mode: 'production', - devtool: 'source-map', - module: { - rules: [ - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: [ - "/node_modules/", - "/dist/", - "/src/background.ts", - "/src/app.ts" - ], - }, - ], - }, - resolve: { - extensions: ['.tsx', '.ts'], - }, - output: { - filename: 'settings.js', - path: path.resolve(__dirname, 'dist/js'), - }, -};