diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5c6f66e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,35 @@ +root = true + +[*] +indent_style = tab +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +charset = utf-8 + +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{js,ts,svg,md}] +charset = utf-8 + +[*.{ts,js}] +indent_style = tab +indent_size = 4 + +[package.json] +indent_style = space +indent_size = 2 + +[*.{sass,scss}] +indent_style = space +indent_size = 2 + +[*.{css,less}] +indent_style = space +indent_size = 2 + +[*.{json,yml,yaml}] +indent_style = space 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/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..bfcc659 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: ["main"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["main"] + schedule: + - cron: "37 13 * * 4" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["javascript"] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.gitignore b/.gitignore index e0400f6..7a21a8d 100644 --- a/.gitignore +++ b/.gitignore @@ -688,5 +688,7 @@ FodyWeavers.xsd dist tools/syncConfig.js -tools/deploy.js -tools/v2.js \ No newline at end of file +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-lock.json b/package-lock.json deleted file mode 100644 index 9c490ed..0000000 --- a/package-lock.json +++ /dev/null @@ -1,459 +0,0 @@ -{ - "name": "browser_extension_template", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "browser_extension_template", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "friendly-helper": "^1.7.1" - }, - "devDependencies": { - "@types/node": "^18.7.4", - "sass": "^1.39.0", - "typescript": "^4.2.4" - } - }, - "node_modules/@types/aes-js": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/aes-js/-/aes-js-3.1.1.tgz", - "integrity": "sha512-SDSGgXT3LRCH6qMWk8OHT1vLSVNuHNvCpKCx2/TYtQMbMGGgxJC9fspwSkQjqzRagrWnCrxuLL3jMNXLXHHvSw==" - }, - "node_modules/@types/node": { - "version": "18.7.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.4.tgz", - "integrity": "sha512-RzRcw8c0B8LzryWOR4Wj7YOTFXvdYKwvrb6xQQyuDfnlTxwYXGCV5RZ/TEbq5L5kn+w3rliHAUyRcG1RtbmTFg==", - "dev": true - }, - "node_modules/aes-js": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", - "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==" - }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/friendly-helper": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/friendly-helper/-/friendly-helper-1.7.1.tgz", - "integrity": "sha512-6X9baO2FZ/EReoJBNOP55fOO21Bu0Lnkr6cM97OHXPBgn+V0LF3rS1Nmxeb/WcQGomSkAO3kBi73HDcrEpUCiA==", - "dependencies": { - "@types/aes-js": "^3.1.1", - "aes-js": "^3.1.2" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/immutable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", - "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", - "dev": true - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/sass": { - "version": "1.54.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.4.tgz", - "integrity": "sha512-3tmF16yvnBwtlPrNBHw/H907j8MlOX8aTBnlNX1yrKx24RKcJGPyLhFUwkoKBKesR3unP93/2z14Ll8NicwQUA==", - "dev": true, - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - } - }, - "dependencies": { - "@types/aes-js": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/aes-js/-/aes-js-3.1.1.tgz", - "integrity": "sha512-SDSGgXT3LRCH6qMWk8OHT1vLSVNuHNvCpKCx2/TYtQMbMGGgxJC9fspwSkQjqzRagrWnCrxuLL3jMNXLXHHvSw==" - }, - "@types/node": { - "version": "18.7.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.4.tgz", - "integrity": "sha512-RzRcw8c0B8LzryWOR4Wj7YOTFXvdYKwvrb6xQQyuDfnlTxwYXGCV5RZ/TEbq5L5kn+w3rliHAUyRcG1RtbmTFg==", - "dev": true - }, - "aes-js": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", - "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==" - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "friendly-helper": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/friendly-helper/-/friendly-helper-1.7.1.tgz", - "integrity": "sha512-6X9baO2FZ/EReoJBNOP55fOO21Bu0Lnkr6cM97OHXPBgn+V0LF3rS1Nmxeb/WcQGomSkAO3kBi73HDcrEpUCiA==", - "requires": { - "@types/aes-js": "^3.1.1", - "aes-js": "^3.1.2" - } - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "immutable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", - "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", - "dev": true - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "sass": { - "version": "1.54.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.4.tgz", - "integrity": "sha512-3tmF16yvnBwtlPrNBHw/H907j8MlOX8aTBnlNX1yrKx24RKcJGPyLhFUwkoKBKesR3unP93/2z14Ll8NicwQUA==", - "dev": true, - "requires": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - } - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "dev": true - } - } -} diff --git a/package.json b/package.json index 76ea2e9..1bfa990 100644 --- a/package.json +++ b/package.json @@ -4,28 +4,44 @@ "private": true, "type": "module", "scripts": { - "deploy-v3": "npm run build-tooling && npm run sync && node ./tools/deploy.js && npm run build-js && npm run build-css", + "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": "tsc -p tsconfig.json", - "build-css": "sass ./src/sass/:./dist/css/", - "build-tooling": "tsc ./tools/v2.ts --target esnext --module esnext --lib ESNext && tsc ./tools/syncConfig.ts --target esnext --module esnext --lib ESNext && tsc ./tools/deploy.ts --target esnext --module esnext --lib ESNext", - "watch-ts": "tsc -w -p tsconfig.json", - "watch-sass": "sass --watch ./src/sass/:./dist/css/", - "sync": "npm run build-tooling && node ./tools/syncConfig.js" + "build": "vite build", + "build-tooling": "tsc --project ./tooling.tsconfig.json", + "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/node": "^18.7.4", - "sass": "^1.39.0", - "typescript": "^4.2.4" + "@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%", "last 2 versions", "not dead" ], - "dependencies": { - "friendly-helper": "^1.7.1" - }, "authors": [ { "name": "Jonas Pfalzgraf", @@ -41,5 +57,9 @@ }, "bugs": { "url": "https://github.com/JosunLP/BrowserExtensionTemplate/issues" + }, + "dependencies": { + "@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 7b2997d..6d17cfa 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -14,19 +14,28 @@ "default_title": "BrowserExtensionTemplate", "default_popup": "popup.html" }, + "options_ui": { + "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 new file mode 100644 index 0000000..c9a0e6b --- /dev/null +++ b/public/options.html @@ -0,0 +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 27095ba..8157802 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,24 +1,60 @@ +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 { - + 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 contentRoot = document.getElementById(App.contentEntry) - const body = document.createElement("div") - const title = document.createElement("h1") - title.innerText = "Hello World" - body.appendChild(title) - 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(); \ No newline at end of file +new App(); diff --git a/src/background.ts b/src/background.ts index f3af3fa..635339b 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,13 +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 new file mode 100644 index 0000000..9c9c472 --- /dev/null +++ b/src/classes/session.ts @@ -0,0 +1,109 @@ +interface SessionData { + sessionId: string; + contentTest: string; +} + +interface StorageService { + save(key: string, data: unknown): Promise; + load(key: string): Promise; + remove(key: string): Promise; +} + +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'); + } + } + + 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; + } + } + + 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'); + } + } +} + +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!; + } + + 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 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 static async reset(): Promise { + 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, + }; + } +} diff --git a/src/components/button.ts b/src/components/button.ts new file mode 100644 index 0000000..b023eaa --- /dev/null +++ b/src/components/button.ts @@ -0,0 +1,96 @@ +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; + + constructor(type: customButton, text: string, id?: string, className?: string) { + this.config = { + type, + text, + id, + className, + }; + } + + public render(): string { + 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; + } + + 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/_mixin.sass b/src/sass/_mixin.sass index 0e4c93a..ddd1d41 100644 --- a/src/sass/_mixin.sass +++ b/src/sass/_mixin.sass @@ -1,84 +1,93 @@ @import "root" @mixin respond-to($media) - @if $media == handhelds - @media only screen and (max-device-width: 40rem) - @content + @if $media == handhelds + @media only screen and (max-device-width: 40rem) + @content - @else if $media == medium-screens - @media only screen and (min-device-width: 40rem) - @content + @else if $media == medium-screens + @media only screen and (min-device-width: 40rem) + @content - @else if $media == wide-screens - @media only screen and (min-width: 1000px) - @content + @else if $media == wide-screens + @media only screen and (min-width: 1000px) + @content @mixin partialButton - button - width: 100% !important - height: 100% !important - text-align: center !important - margin: 0 !important + width: 5rem !important + height: 2rem !important + text-align: center !important + margin: 0.5rem !important + border-color: $seccond-color !important + border-radius: 0.5rem !important - @include respond-to(handhelds) - font-size: 3rem + @include respond-to(handhelds) + font-size: 3rem - @include respond-to(medium-screens) - font-size: 1.5rem + @include respond-to(medium-screens) + font-size: 1.5rem @mixin hoverMe - &:hover - button - color: grey !important + &:hover + button + color: grey !important @mixin shadow - box-shadow: 0px 0px 30px silver + box-shadow: 0px 0px 30px silver @mixin noselect - -webkit-touch-callout: none - -webkit-user-select: none - -khtml-user-select: none - -moz-user-select: none - -ms-user-select: none - user-select: none - pointer-events: none + -webkit-touch-callout: none + -webkit-user-select: none + -khtml-user-select: none + -moz-user-select: none + -ms-user-select: none + user-select: none + pointer-events: none + +.form-group + margin-left: 2rem + margin-right: 2rem + margin-bottom: 0.5rem + flex-wrap: wrap + justify-content: center + display: flex @mixin formBasic - display: block - padding: 2rem - background: $background-color-content - border-radius: 0.7rem - min-height: 20rem - margin: auto - margin-top: 2rem - margin-bottom: 2rem - @include shadow + display: block + padding: 2rem + background: $background-color-content + border-radius: 0.7rem + min-height: 20rem + margin: auto + margin-top: 2rem + margin-bottom: 2rem + @include shadow - @include respond-to(handhelds) - font-size: 2.5em - margin-left: -0.8em - margin-right: -0.8em - border-radius: 0 + @include respond-to(handhelds) + font-size: 2.5em + margin-left: -0.8em + margin-right: -0.8em + border-radius: 0 - @include respond-to(medium-screens) - max-width: 40rem + @include respond-to(medium-screens) + max-width: 40rem - @include respond-to(wide-screens) - max-width: 40rem + @include respond-to(wide-screens) + max-width: 40rem - input - @include respond-to(handhelds) - font-size: 3rem - border-radius: 0.5rem + input + @include respond-to(handhelds) + font-size: 3rem + border-radius: 0.5rem - .check - position: static - @include respond-to(handhelds) - width: 2rem !important - height: 2rem !important + .check + position: static + @include respond-to(handhelds) + width: 2rem !important + height: 2rem !important - button - @include respond-to(handhelds) - font-size: 3rem - padding: 1rem - border-radius: 1rem + button + @include respond-to(handhelds) + font-size: 3rem + padding: 1rem + border-radius: 1rem diff --git a/src/sass/_root.sass b/src/sass/_root.sass index edabad7..e96d116 100644 --- a/src/sass/_root.sass +++ b/src/sass/_root.sass @@ -1,8 +1,64 @@ -$main-font: 'Ubuntu', 'Staatliches' -$main-font-color: white -$main-font-color-hover: lightgrey -$main-uschrift-font: 'Ubuntu', Arial -$seccond-color: lightgrey -$background-color: rgb(119, 178, 255) -$background-color-content: 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 0b05a95..c0c29d1 100644 --- a/src/sass/app.sass +++ b/src/sass/app.sass @@ -2,40 +2,50 @@ @import 'mixin' @import 'content' +// Bootstrap with legacy import (suppressed warnings via Vite config) +@import "../../node_modules/bootstrap/scss/bootstrap" + body - height: 30rem - width: 30rem - background-color: $background-color - text-align: center + height: 30rem + width: 30rem + background-color: $background-color + text-align: center + margin: auto + padding: 1rem + color: $main-font-color + + p + font-size: 1rem + font-weight: bold margin: auto padding: auto color: $main-font-color - - @include partialButton + text-align: center + font-family: 'Roboto', sans-serif h1, h2 - @include noselect + @include noselect form - @include formBasic + @include formBasic .logo - width: 5rem - height: auto - padding-top: 2rem - @include noselect + width: 5rem + height: auto + padding-top: 2rem + @include noselect svg - @include noselect + @include noselect table - color: $main-font-color !important - - th - @include noselect + color: $main-font-color !important + + th + @include noselect a - color: $main-font-color + color: $main-font-color - &:hover - color: $background-color-content \ No newline at end of file + &:hover + color: $background-color-content diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..0f08c73 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,120 @@ +import { Session } from './classes/session'; +import { BasicButton } from './components/button'; +import './sass/app.sass'; + +class Settings { + private session: Session | null = null; + + constructor() { + this.init(); + } + + 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'); + } + + 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} + `; + + 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 new file mode 100644 index 0000000..efdfd5f --- /dev/null +++ b/src/types/buttonType.ts @@ -0,0 +1,10 @@ +export type customButton = + | 'neutral' + | 'primary' + | 'secondary' + | 'success' + | 'danger' + | 'warning' + | 'info' + | 'light' + | 'dark'; diff --git a/tooling.tsconfig.json b/tooling.tsconfig.json new file mode 100644 index 0000000..72730da --- /dev/null +++ b/tooling.tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "ESNext", + "outDir": "./tools/", + "rootDir": "./tools/", + "removeComments": true, + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": [ + "node", + "chrome" + ] + }, + "include": [ + "./tools/*.ts" + ], + "exclude": [ + "node_modules", + "./src/*.ts", + "./src/**/*.ts", + "./dist/*.ts", + "./dist/*.js" + ] +} diff --git a/tools/deploy.ts b/tools/deploy.ts deleted file mode 100644 index 8b6b7b0..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) { - const 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 fe9081d..96013de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,81 +1,49 @@ { "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ + /* 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. */ + /* Completeness */ + "skipLibCheck": true }, - "include": [ - "./src/**/*.ts", - "./src/*.ts" - ], - "exclude": [ - "./tools/*.ts", - "./dist/*.ts", - "./dist/*.js" - ] + "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, + }, +});