mirror of
https://github.com/JosunLP/UserScriptProjectTemplate.git
synced 2025-10-14 09:00:11 +00:00
feat: Enhance UserScript structure and functionality
- Updated package.json to include new scripts for development, production builds, linting, formatting, and cleaning. - Added ESLint and Prettier for code quality and formatting. - Refactored main application class to extend EventEmitter and manage modules. - Introduced ExampleModule to demonstrate module structure and functionality. - Created utility classes for DOM manipulation, event handling, and persistent storage. - Added TypeScript definitions for UserScript environment. - Improved TypeScript configuration with stricter checks and path aliases. - Updated Vite configuration to handle development and production builds more effectively. - Enhanced user script header generation to support environment-specific configurations.
This commit is contained in:
parent
8089771d41
commit
88aeab8f29
17 changed files with 4064 additions and 595 deletions
39
.eslintrc.js
Normal file
39
.eslintrc.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
extends: ['eslint:recommended'],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es6: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
// Greasemonkey/Tampermonkey globals
|
||||||
|
GM_setValue: 'readonly',
|
||||||
|
GM_getValue: 'readonly',
|
||||||
|
GM_deleteValue: 'readonly',
|
||||||
|
GM_listValues: 'readonly',
|
||||||
|
GM_log: 'readonly',
|
||||||
|
GM_notification: 'readonly',
|
||||||
|
GM_openInTab: 'readonly',
|
||||||
|
GM_registerMenuCommand: 'readonly',
|
||||||
|
GM_unregisterMenuCommand: 'readonly',
|
||||||
|
GM_xmlhttpRequest: 'readonly',
|
||||||
|
GM_info: 'readonly',
|
||||||
|
unsafeWindow: 'readonly',
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-debugger': 'warn',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
'no-var': 'error',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'no-undef': 'off',
|
||||||
|
},
|
||||||
|
ignorePatterns: ['dist/', 'node_modules/', 'tools/**/*.js'],
|
||||||
|
};
|
20
.gitignore
vendored
20
.gitignore
vendored
|
@ -643,5 +643,23 @@ FodyWeavers.xsd
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,webstorm,intellij,sublimetext
|
# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,webstorm,intellij,sublimetext
|
||||||
|
|
||||||
dist
|
# Project specific
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
tools/userScriptHeader.js
|
tools/userScriptHeader.js
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
11
.prettierrc.json
Normal file
11
.prettierrc.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
205
README.md
205
README.md
|
@ -1,3 +1,206 @@
|
||||||
# UserScript Project Template
|
# UserScript Project Template
|
||||||
|
|
||||||
A user script project template to create large and structured TypeScript projects for Tampermonkey or Greasemonkey. It is intended to form a scalable base and is primarily aimed at the Ingress community.
|
A modern, scalable UserScript project template for building large and structured TypeScript projects for Tampermonkey or Greasemonkey. Originally designed for the Ingress community, but adaptable for any web-based project.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🔧 **Modern TypeScript** with strict type checking
|
||||||
|
- 🚀 **Vite** for fast builds and development
|
||||||
|
- 📦 **Modular Architecture** with utilities and event system
|
||||||
|
- 🎯 **Environment-specific** configurations (dev/prod)
|
||||||
|
- 🧹 **ESLint + Prettier** for code quality
|
||||||
|
- 💾 **Storage utilities** for persistent data
|
||||||
|
- 🎨 **DOM utilities** for easier manipulation
|
||||||
|
- 📡 **Event system** for modular communication
|
||||||
|
- 🔒 **TypeScript definitions** for UserScript APIs
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
1. **Clone or use this template:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/JosunLP/UserScriptProjectTemplate.git my-userscript
|
||||||
|
cd my-userscript
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure your script:**
|
||||||
|
- Edit `header.config.json` to set your target domains
|
||||||
|
- Update `package.json` with your script details
|
||||||
|
|
||||||
|
4. **Start development:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Build for production:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
src/
|
||||||
|
├── index.ts # Main application entry point
|
||||||
|
├── types/ # TypeScript definitions
|
||||||
|
│ └── userscript.d.ts
|
||||||
|
├── utils/ # Utility functions
|
||||||
|
│ ├── storage.ts # Persistent storage helpers
|
||||||
|
│ ├── dom.ts # DOM manipulation utilities
|
||||||
|
│ └── events.ts # Event system
|
||||||
|
├── core/ # Core application logic
|
||||||
|
└── modules/ # Feature modules
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Available Scripts
|
||||||
|
|
||||||
|
- `npm run dev` - Build with watch mode for development
|
||||||
|
- `npm run build` - Build for production
|
||||||
|
- `npm run build:prod` - Build for production (explicit)
|
||||||
|
- `npm run lint` - Run ESLint
|
||||||
|
- `npm run lint:fix` - Fix ESLint issues
|
||||||
|
- `npm run format` - Format code with Prettier
|
||||||
|
- `npm run type-check` - Run TypeScript type checking
|
||||||
|
- `npm run clean` - Clean dist folder
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
### Header Configuration
|
||||||
|
|
||||||
|
Edit `header.config.json` to configure your UserScript metadata:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"environments": {
|
||||||
|
"development": {
|
||||||
|
"includes": ["http://localhost:*/*"],
|
||||||
|
"grants": ["GM_setValue", "GM_getValue", "GM_log"]
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"includes": ["https://your-domain.com/*"],
|
||||||
|
"grants": ["GM_setValue", "GM_getValue"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
The build system automatically detects the environment:
|
||||||
|
|
||||||
|
- `NODE_ENV=development` for development builds
|
||||||
|
- `NODE_ENV=production` for production builds
|
||||||
|
|
||||||
|
## 🧰 Utilities
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Storage } from '@/utils/storage';
|
||||||
|
|
||||||
|
// Store data
|
||||||
|
Storage.set('key', { some: 'data' });
|
||||||
|
|
||||||
|
// Retrieve data
|
||||||
|
const data = Storage.get('key', defaultValue);
|
||||||
|
|
||||||
|
// Check existence
|
||||||
|
if (Storage.has('key')) {
|
||||||
|
// Key exists
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DOM Utilities
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DOMUtils } from '@/utils/dom';
|
||||||
|
|
||||||
|
// Wait for element
|
||||||
|
const element = await DOMUtils.waitForElement('.my-selector');
|
||||||
|
|
||||||
|
// Add styles
|
||||||
|
DOMUtils.addStyles(`
|
||||||
|
.my-class { color: red; }
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create element
|
||||||
|
const button = DOMUtils.createElement('button', {
|
||||||
|
textContent: 'Click me',
|
||||||
|
onclick: () => console.log('Clicked!')
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EventEmitter } from '@/utils/events';
|
||||||
|
|
||||||
|
interface MyEvents {
|
||||||
|
userAction: { userId: string };
|
||||||
|
dataLoaded: { count: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitter = new EventEmitter<MyEvents>();
|
||||||
|
|
||||||
|
// Listen for events
|
||||||
|
emitter.on('userAction', ({ userId }) => {
|
||||||
|
console.log(`User ${userId} performed an action`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit events
|
||||||
|
emitter.emit('userAction', { userId: '123' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Extending the Template
|
||||||
|
|
||||||
|
1. **Add new modules** in `src/modules/`
|
||||||
|
2. **Register modules** in your main App class
|
||||||
|
3. **Use the event system** for module communication
|
||||||
|
4. **Add utilities** in `src/utils/` for reusable functionality
|
||||||
|
|
||||||
|
Example module:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/modules/example.ts
|
||||||
|
import { EventEmitter } from '@/utils/events';
|
||||||
|
import { Storage } from '@/utils/storage';
|
||||||
|
|
||||||
|
export class ExampleModule extends EventEmitter {
|
||||||
|
initialize() {
|
||||||
|
console.log('Example module initialized');
|
||||||
|
// Your module logic here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Development Tips
|
||||||
|
|
||||||
|
- Use `console.log()` freely - it's enabled in UserScripts
|
||||||
|
- Test in development mode with localhost includes
|
||||||
|
- Use TypeScript strict mode for better code quality
|
||||||
|
- Leverage the storage utilities for persistent data
|
||||||
|
- Use the event system for loose coupling between modules
|
||||||
|
|
||||||
|
## 📝 License
|
||||||
|
|
||||||
|
MIT License - see [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Run tests and linting
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## 💡 Originally for Ingress
|
||||||
|
|
||||||
|
This template was originally designed for the Ingress community but is versatile enough for any web-based UserScript project. The modular architecture makes it easy to build complex scripts while maintaining code quality and organization.
|
||||||
|
|
BIN
assets/icon.png
BIN
assets/icon.png
Binary file not shown.
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 110 KiB |
|
@ -3,24 +3,47 @@
|
||||||
"downloadUrl": "",
|
"downloadUrl": "",
|
||||||
"supportUrl": "",
|
"supportUrl": "",
|
||||||
"iconUrl": "./assets/icon.png",
|
"iconUrl": "./assets/icon.png",
|
||||||
|
"environments": {
|
||||||
|
"development": {
|
||||||
|
"includes": [
|
||||||
|
"http://localhost:*/*",
|
||||||
|
"https://localhost:*/*",
|
||||||
|
"https://*.josunlp.de/*",
|
||||||
|
"https://josunlp.de/*"
|
||||||
|
],
|
||||||
|
"excludes": [],
|
||||||
|
"grants": [
|
||||||
|
"GM_setValue",
|
||||||
|
"GM_getValue",
|
||||||
|
"GM_deleteValue",
|
||||||
|
"GM_listValues",
|
||||||
|
"GM_log",
|
||||||
|
"GM_notification",
|
||||||
|
"GM_registerMenuCommand",
|
||||||
|
"GM_unregisterMenuCommand"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
"includes": [
|
"includes": [
|
||||||
"https://*.josunlp.de/*",
|
"https://*.josunlp.de/*",
|
||||||
"https://josunlp.de/*"
|
"https://josunlp.de/*"
|
||||||
],
|
],
|
||||||
"excludes": [],
|
"excludes": [],
|
||||||
|
"grants": [
|
||||||
|
"GM_setValue",
|
||||||
|
"GM_getValue",
|
||||||
|
"GM_deleteValue",
|
||||||
|
"GM_listValues",
|
||||||
|
"GM_registerMenuCommand"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"requires": [],
|
"requires": [],
|
||||||
"resources": [],
|
"resources": [],
|
||||||
"connecters": [],
|
"connecters": [],
|
||||||
"matches": [
|
"matches": [],
|
||||||
"https://*.josunlp.de/*",
|
|
||||||
"https://josunlp.de/*"
|
|
||||||
],
|
|
||||||
"matchAllFrames": false,
|
"matchAllFrames": false,
|
||||||
"runAt": "document-start",
|
"runAt": "document-start",
|
||||||
"grants": [
|
|
||||||
"GM_setValue",
|
|
||||||
"GM_getValue"
|
|
||||||
],
|
|
||||||
"antifeatures": [],
|
"antifeatures": [],
|
||||||
"noframes": false,
|
"noframes": false,
|
||||||
"unwrap": false
|
"unwrap": false
|
||||||
|
|
3457
package-lock.json
generated
3457
package-lock.json
generated
File diff suppressed because it is too large
Load diff
20
package.json
20
package.json
|
@ -5,9 +5,16 @@
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"module": "node",
|
"module": "node",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "vite build --watch --mode development",
|
||||||
"build": "vite build && npm run build-userScriptHeader",
|
"build": "vite build && npm run build-userScriptHeader",
|
||||||
|
"build:prod": "vite build --mode production && npm run build-userScriptHeader",
|
||||||
"build-tooling": "tsc ./tools/userScriptHeader.ts --resolveJsonModule --esModuleInterop",
|
"build-tooling": "tsc ./tools/userScriptHeader.ts --resolveJsonModule --esModuleInterop",
|
||||||
"build-userScriptHeader": "npm run build-tooling && node ./tools/userScriptHeader.js"
|
"build-userScriptHeader": "npm run build-tooling && node ./tools/userScriptHeader.js",
|
||||||
|
"lint": "eslint src/ --ext .ts,.tsx",
|
||||||
|
"lint:fix": "eslint src/ --ext .ts,.tsx --fix",
|
||||||
|
"format": "prettier --write \"src/**/*.{ts,tsx,json}\"",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"clean": "rimraf dist"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -27,11 +34,18 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/JosunLP/UserScriptProjectTemplate#readme",
|
"homepage": "https://github.com/JosunLP/UserScriptProjectTemplate#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||||
|
"@typescript-eslint/parser": "^7.18.0",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.5.1",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"rimraf": "^5.0.10",
|
||||||
"ts-loader": "^9.3.1",
|
"ts-loader": "^9.3.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^5.4.0",
|
||||||
"undici-types": "^6.18.2",
|
"undici-types": "^6.18.2",
|
||||||
"vite": "^5.2.12",
|
"vite": "^7.0.4",
|
||||||
"vite-tsconfig-paths": "^4.3.2",
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
"webpack": "^5.74.0",
|
"webpack": "^5.74.0",
|
||||||
"webpack-cli": "^4.10.0"
|
"webpack-cli": "^4.10.0"
|
||||||
|
|
142
src/index.ts
142
src/index.ts
|
@ -1,15 +1,147 @@
|
||||||
|
import { ExampleModule } from '@/modules/example';
|
||||||
|
import { DOMUtils } from '@/utils/dom';
|
||||||
|
import { EventEmitter } from '@/utils/events';
|
||||||
|
import { Storage } from '@/utils/storage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App
|
* Application events interface
|
||||||
*/
|
*/
|
||||||
class App {
|
interface AppEvents {
|
||||||
|
ready: void;
|
||||||
|
error: Error;
|
||||||
|
moduleLoaded: { name: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main Application Class
|
||||||
|
*
|
||||||
|
* This is the entry point for your UserScript.
|
||||||
|
* Extend this class to build your specific functionality.
|
||||||
|
*/
|
||||||
|
class App extends EventEmitter<AppEvents> {
|
||||||
|
private isInitialized = false;
|
||||||
|
private modules: Map<string, any> = new Map();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.main();
|
super();
|
||||||
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
private main() {
|
/**
|
||||||
console.log('Hello World!');
|
* Initialize the application
|
||||||
|
*/
|
||||||
|
private async initialize(): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('🚀 UserScript starting...');
|
||||||
|
|
||||||
|
// Wait for DOM to be ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
document.addEventListener('DOMContentLoaded', resolve, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.main();
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
this.emit('ready', undefined);
|
||||||
|
|
||||||
|
console.log('✅ UserScript initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ UserScript initialization failed:', error);
|
||||||
|
this.emit('error', error as Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main application logic
|
||||||
|
* Override this method in your implementation
|
||||||
|
*/
|
||||||
|
protected async main(): Promise<void> {
|
||||||
|
console.log('👋 Hello from UserScript Template!');
|
||||||
|
|
||||||
|
// Example: Add some basic functionality
|
||||||
|
this.addExampleFeatures();
|
||||||
|
|
||||||
|
// Initialize modules
|
||||||
|
await this.initializeModules();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize application modules
|
||||||
|
*/
|
||||||
|
private async initializeModules(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Initialize example module
|
||||||
|
const exampleModule = new ExampleModule();
|
||||||
|
await exampleModule.initialize();
|
||||||
|
|
||||||
|
// Register the module
|
||||||
|
this.registerModule('example', exampleModule);
|
||||||
|
|
||||||
|
// Listen to module events
|
||||||
|
exampleModule.on('actionPerformed', ({ action, timestamp }) => {
|
||||||
|
console.log(
|
||||||
|
`📡 Module action received: ${action} at ${new Date(timestamp).toLocaleString()}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to initialize modules:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example features to demonstrate the template
|
||||||
|
*/
|
||||||
|
private addExampleFeatures(): void {
|
||||||
|
// Example: Add custom styles
|
||||||
|
DOMUtils.addStyles(
|
||||||
|
`
|
||||||
|
.userscript-highlight {
|
||||||
|
background-color: yellow !important;
|
||||||
|
border: 2px solid red !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'userscript-styles'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Example: Storage usage
|
||||||
|
const visitCount = (Storage.get<number>('visitCount', 0) || 0) + 1;
|
||||||
|
Storage.set('visitCount', visitCount);
|
||||||
|
console.log(`📊 This is visit #${visitCount}`);
|
||||||
|
|
||||||
|
// Example: Add menu command
|
||||||
|
if (typeof GM_registerMenuCommand !== 'undefined') {
|
||||||
|
GM_registerMenuCommand('Show Visit Count', () => {
|
||||||
|
const count = Storage.get<number>('visitCount', 0);
|
||||||
|
alert(`You have visited this page ${count} times!`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a module
|
||||||
|
*/
|
||||||
|
protected registerModule(name: string, module: any): void {
|
||||||
|
this.modules.set(name, module);
|
||||||
|
this.emit('moduleLoaded', { name });
|
||||||
|
console.log(`📦 Module "${name}" loaded`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a registered module
|
||||||
|
*/
|
||||||
|
protected getModule<T = any>(name: string): T | undefined {
|
||||||
|
return this.modules.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if app is initialized
|
||||||
|
*/
|
||||||
|
public get ready(): boolean {
|
||||||
|
return this.isInitialized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the application
|
||||||
new App();
|
new App();
|
251
src/modules/example.ts
Normal file
251
src/modules/example.ts
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
import { DOMUtils } from '@/utils/dom';
|
||||||
|
import { EventEmitter } from '@/utils/events';
|
||||||
|
import { Storage } from '@/utils/storage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example module events
|
||||||
|
*/
|
||||||
|
interface ExampleModuleEvents {
|
||||||
|
initialized: void;
|
||||||
|
actionPerformed: { action: string; timestamp: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example module to demonstrate the template structure
|
||||||
|
*
|
||||||
|
* This module shows how to:
|
||||||
|
* - Extend the EventEmitter for module communication
|
||||||
|
* - Use storage for persistent data
|
||||||
|
* - Manipulate DOM elements
|
||||||
|
* - Register menu commands
|
||||||
|
*/
|
||||||
|
export class ExampleModule extends EventEmitter<ExampleModuleEvents> {
|
||||||
|
private isInitialized = false;
|
||||||
|
private actionCount = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the module
|
||||||
|
*/
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('📦 Initializing ExampleModule...');
|
||||||
|
|
||||||
|
// Load persistent data
|
||||||
|
this.actionCount = Storage.get<number>('exampleModule.actionCount', 0) || 0;
|
||||||
|
|
||||||
|
// Wait for required DOM elements (example)
|
||||||
|
await this.waitForPageElements();
|
||||||
|
|
||||||
|
// Add custom styles
|
||||||
|
this.addModuleStyles();
|
||||||
|
|
||||||
|
// Register menu commands
|
||||||
|
this.registerMenuCommands();
|
||||||
|
|
||||||
|
// Set up event listeners
|
||||||
|
this.setupEventListeners();
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
this.emit('initialized', undefined);
|
||||||
|
|
||||||
|
console.log('✅ ExampleModule initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ ExampleModule initialization failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for required page elements
|
||||||
|
*/
|
||||||
|
private async waitForPageElements(): Promise<void> {
|
||||||
|
// Example: Wait for body to be available
|
||||||
|
await DOMUtils.waitForElement('body', 5000);
|
||||||
|
console.log('📄 Page body is ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add module-specific styles
|
||||||
|
*/
|
||||||
|
private addModuleStyles(): void {
|
||||||
|
DOMUtils.addStyles(
|
||||||
|
`
|
||||||
|
.example-module-button {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 9999;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: #007cba;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-module-button:hover {
|
||||||
|
background: #005a87;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-module-notification {
|
||||||
|
position: fixed;
|
||||||
|
top: 70px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 9998;
|
||||||
|
padding: 10px;
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-module-notification.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'example-module-styles'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register UserScript menu commands
|
||||||
|
*/
|
||||||
|
private registerMenuCommands(): void {
|
||||||
|
if (typeof GM_registerMenuCommand !== 'undefined') {
|
||||||
|
GM_registerMenuCommand('🎯 Perform Example Action', () => {
|
||||||
|
this.performAction('menu-command');
|
||||||
|
});
|
||||||
|
|
||||||
|
GM_registerMenuCommand('📊 Show Module Stats', () => {
|
||||||
|
this.showStats();
|
||||||
|
});
|
||||||
|
|
||||||
|
GM_registerMenuCommand('🧹 Reset Module Data', () => {
|
||||||
|
this.resetData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up event listeners
|
||||||
|
*/
|
||||||
|
private setupEventListeners(): void {
|
||||||
|
// Add a floating button for demonstration
|
||||||
|
const button = DOMUtils.createElement('button', {
|
||||||
|
className: 'example-module-button',
|
||||||
|
textContent: '🎯 Example Action',
|
||||||
|
onclick: () => this.performAction('button-click'),
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(button);
|
||||||
|
|
||||||
|
// Listen to keyboard shortcuts
|
||||||
|
document.addEventListener('keydown', event => {
|
||||||
|
// Ctrl+Shift+E to perform action
|
||||||
|
if (event.ctrlKey && event.shiftKey && event.key === 'E') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.performAction('keyboard-shortcut');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform an example action
|
||||||
|
*/
|
||||||
|
public performAction(trigger: string): void {
|
||||||
|
this.actionCount++;
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
// Store the updated count
|
||||||
|
Storage.set('exampleModule.actionCount', this.actionCount);
|
||||||
|
|
||||||
|
// Emit event
|
||||||
|
this.emit('actionPerformed', { action: trigger, timestamp });
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
this.showNotification(`Action performed via ${trigger}! (Total: ${this.actionCount})`);
|
||||||
|
|
||||||
|
console.log(`🎯 Example action performed via ${trigger} (count: ${this.actionCount})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a temporary notification
|
||||||
|
*/
|
||||||
|
private showNotification(message: string, duration = 3000): void {
|
||||||
|
const notification = DOMUtils.createElement('div', {
|
||||||
|
className: 'example-module-notification',
|
||||||
|
textContent: message,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Trigger animation
|
||||||
|
setTimeout(() => notification.classList.add('show'), 10);
|
||||||
|
|
||||||
|
// Remove after duration
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
setTimeout(() => DOMUtils.removeElement(notification), 300);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show module statistics
|
||||||
|
*/
|
||||||
|
private showStats(): void {
|
||||||
|
const stats = {
|
||||||
|
initialized: this.isInitialized,
|
||||||
|
actionCount: this.actionCount,
|
||||||
|
lastAction: Storage.get<number>('exampleModule.lastActionTime', 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
const message = [
|
||||||
|
'Example Module Statistics:',
|
||||||
|
`• Initialized: ${stats.initialized ? 'Yes' : 'No'}`,
|
||||||
|
`• Actions performed: ${stats.actionCount}`,
|
||||||
|
`• Last action: ${stats.lastAction ? new Date(stats.lastAction).toLocaleString() : 'Never'}`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
if (typeof GM_notification !== 'undefined') {
|
||||||
|
GM_notification(message, 'Module Stats');
|
||||||
|
} else {
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset module data
|
||||||
|
*/
|
||||||
|
private resetData(): void {
|
||||||
|
if (confirm('Are you sure you want to reset all module data?')) {
|
||||||
|
Storage.remove('exampleModule.actionCount');
|
||||||
|
Storage.remove('exampleModule.lastActionTime');
|
||||||
|
this.actionCount = 0;
|
||||||
|
console.log('🧹 Example module data reset');
|
||||||
|
this.showNotification('Module data reset!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get module status
|
||||||
|
*/
|
||||||
|
public get initialized(): boolean {
|
||||||
|
return this.isInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get action count
|
||||||
|
*/
|
||||||
|
public get totalActions(): number {
|
||||||
|
return this.actionCount;
|
||||||
|
}
|
||||||
|
}
|
99
src/types/userscript.d.ts
vendored
Normal file
99
src/types/userscript.d.ts
vendored
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
/**
|
||||||
|
* TypeScript definitions for UserScript environment
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
/**
|
||||||
|
* Greasemonkey/Tampermonkey API
|
||||||
|
*/
|
||||||
|
function GM_setValue(key: string, value: any): void;
|
||||||
|
function GM_getValue(key: string, defaultValue?: any): any;
|
||||||
|
function GM_deleteValue(key: string): void;
|
||||||
|
function GM_listValues(): string[];
|
||||||
|
|
||||||
|
function GM_log(message: string): void;
|
||||||
|
function GM_notification(
|
||||||
|
text: string,
|
||||||
|
title?: string,
|
||||||
|
image?: string,
|
||||||
|
onclick?: () => void
|
||||||
|
): void;
|
||||||
|
|
||||||
|
function GM_openInTab(
|
||||||
|
url: string,
|
||||||
|
options?: { active?: boolean; insert?: boolean; setParent?: boolean }
|
||||||
|
): void;
|
||||||
|
function GM_registerMenuCommand(name: string, fn: () => void, accessKey?: string): number;
|
||||||
|
function GM_unregisterMenuCommand(menuCmdId: number): void;
|
||||||
|
|
||||||
|
function GM_xmlhttpRequest(details: GM_XHRDetails): GM_XHRResponse;
|
||||||
|
|
||||||
|
interface GM_XHRDetails {
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
||||||
|
url: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
data?: string | FormData | Blob;
|
||||||
|
binary?: boolean;
|
||||||
|
timeout?: number;
|
||||||
|
context?: any;
|
||||||
|
responseType?: 'text' | 'json' | 'blob' | 'arraybuffer' | 'document';
|
||||||
|
overrideMimeType?: string;
|
||||||
|
anonymous?: boolean;
|
||||||
|
fetch?: boolean;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
onload?: (response: GM_XHRResponse) => void;
|
||||||
|
onerror?: (response: GM_XHRResponse) => void;
|
||||||
|
onabort?: () => void;
|
||||||
|
ontimeout?: () => void;
|
||||||
|
onprogress?: (response: GM_XHRProgressResponse) => void;
|
||||||
|
onreadystatechange?: (response: GM_XHRResponse) => void;
|
||||||
|
onloadstart?: (response: GM_XHRResponse) => void;
|
||||||
|
onloadend?: (response: GM_XHRResponse) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GM_XHRResponse {
|
||||||
|
readyState: number;
|
||||||
|
responseHeaders: string;
|
||||||
|
responseText: string;
|
||||||
|
response: any;
|
||||||
|
status: number;
|
||||||
|
statusText: string;
|
||||||
|
finalUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GM_XHRProgressResponse extends GM_XHRResponse {
|
||||||
|
lengthComputable: boolean;
|
||||||
|
loaded: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserScript Info
|
||||||
|
*/
|
||||||
|
interface GM_Info {
|
||||||
|
script: {
|
||||||
|
description: string;
|
||||||
|
excludes: string[];
|
||||||
|
includes: string[];
|
||||||
|
matches: string[];
|
||||||
|
name: string;
|
||||||
|
namespace: string;
|
||||||
|
resources: Record<string, string>;
|
||||||
|
runAt: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
scriptMetaStr: string;
|
||||||
|
scriptWillUpdate: boolean;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GM_info: GM_Info;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UnsafeWindow for accessing page's global scope
|
||||||
|
*/
|
||||||
|
const unsafeWindow: Window & typeof globalThis;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
126
src/utils/dom.ts
Normal file
126
src/utils/dom.ts
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
/**
|
||||||
|
* DOM utility functions for UserScript development
|
||||||
|
*/
|
||||||
|
export class DOMUtils {
|
||||||
|
/**
|
||||||
|
* Wait for element to appear in DOM
|
||||||
|
*/
|
||||||
|
static waitForElement(
|
||||||
|
selector: string,
|
||||||
|
timeout = 10000,
|
||||||
|
root: Document | Element = document
|
||||||
|
): Promise<Element> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const element = root.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
resolve(element);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(mutations => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
const nodes = Array.from(mutation.addedNodes);
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const element =
|
||||||
|
(node as Element).querySelector?.(selector) || (node as Element).matches?.(selector)
|
||||||
|
? (node as Element)
|
||||||
|
: null;
|
||||||
|
if (element) {
|
||||||
|
observer.disconnect();
|
||||||
|
resolve(element);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(root, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
observer.disconnect();
|
||||||
|
reject(new Error(`Element "${selector}" not found within ${timeout}ms`));
|
||||||
|
}, timeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for multiple elements
|
||||||
|
*/
|
||||||
|
static waitForElements(
|
||||||
|
selectors: string[],
|
||||||
|
timeout = 10000,
|
||||||
|
root: Document | Element = document
|
||||||
|
): Promise<Element[]> {
|
||||||
|
return Promise.all(selectors.map(selector => this.waitForElement(selector, timeout, root)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create element with attributes and content
|
||||||
|
*/
|
||||||
|
static createElement<K extends keyof HTMLElementTagNameMap>(
|
||||||
|
tagName: K,
|
||||||
|
attributes: Partial<HTMLElementTagNameMap[K]> = {},
|
||||||
|
content?: string | Node
|
||||||
|
): HTMLElementTagNameMap[K] {
|
||||||
|
const element = document.createElement(tagName);
|
||||||
|
|
||||||
|
Object.assign(element, attributes);
|
||||||
|
|
||||||
|
if (content !== undefined) {
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
element.textContent = content;
|
||||||
|
} else {
|
||||||
|
element.appendChild(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add CSS styles to page
|
||||||
|
*/
|
||||||
|
static addStyles(css: string, id?: string): HTMLStyleElement {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = css;
|
||||||
|
if (id) {
|
||||||
|
style.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
(document.head || document.documentElement).appendChild(style);
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove element safely
|
||||||
|
*/
|
||||||
|
static removeElement(element: Element | string): boolean {
|
||||||
|
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
||||||
|
if (el && el.parentNode) {
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if element is visible
|
||||||
|
*/
|
||||||
|
static isVisible(element: Element): boolean {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
|
||||||
|
return (
|
||||||
|
rect.width > 0 &&
|
||||||
|
rect.height > 0 &&
|
||||||
|
style.visibility !== 'hidden' &&
|
||||||
|
style.display !== 'none' &&
|
||||||
|
style.opacity !== '0'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
78
src/utils/events.ts
Normal file
78
src/utils/events.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
/**
|
||||||
|
* Type-safe event emitter for UserScript modules
|
||||||
|
*/
|
||||||
|
export interface EventMap {
|
||||||
|
[event: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventEmitter<T extends EventMap = EventMap> {
|
||||||
|
private listeners: { [K in keyof T]?: Array<(data: T[K]) => void> } = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add event listener
|
||||||
|
*/
|
||||||
|
on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
|
||||||
|
if (!this.listeners[event]) {
|
||||||
|
this.listeners[event] = [];
|
||||||
|
}
|
||||||
|
this.listeners[event]!.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add one-time event listener
|
||||||
|
*/
|
||||||
|
once<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
|
||||||
|
const onceListener = (data: T[K]) => {
|
||||||
|
listener(data);
|
||||||
|
this.off(event, onceListener);
|
||||||
|
};
|
||||||
|
this.on(event, onceListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove event listener
|
||||||
|
*/
|
||||||
|
off<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
|
||||||
|
const eventListeners = this.listeners[event];
|
||||||
|
if (eventListeners) {
|
||||||
|
const index = eventListeners.indexOf(listener);
|
||||||
|
if (index > -1) {
|
||||||
|
eventListeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit event
|
||||||
|
*/
|
||||||
|
emit<K extends keyof T>(event: K, data: T[K]): void {
|
||||||
|
const eventListeners = this.listeners[event];
|
||||||
|
if (eventListeners) {
|
||||||
|
eventListeners.forEach(listener => {
|
||||||
|
try {
|
||||||
|
listener(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in event listener for "${String(event)}":`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all listeners for an event
|
||||||
|
*/
|
||||||
|
removeAllListeners<K extends keyof T>(event?: K): void {
|
||||||
|
if (event) {
|
||||||
|
delete this.listeners[event];
|
||||||
|
} else {
|
||||||
|
this.listeners = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get listener count for an event
|
||||||
|
*/
|
||||||
|
listenerCount<K extends keyof T>(event: K): number {
|
||||||
|
return this.listeners[event]?.length || 0;
|
||||||
|
}
|
||||||
|
}
|
61
src/utils/storage.ts
Normal file
61
src/utils/storage.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* Storage utility for UserScript persistent data
|
||||||
|
*/
|
||||||
|
export class Storage {
|
||||||
|
/**
|
||||||
|
* Set a value in persistent storage
|
||||||
|
*/
|
||||||
|
static set<T>(key: string, value: T): void {
|
||||||
|
try {
|
||||||
|
const serialized = JSON.stringify(value);
|
||||||
|
GM_setValue(key, serialized);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to store value for key "${key}":`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a value from persistent storage
|
||||||
|
*/
|
||||||
|
static get<T>(key: string, defaultValue?: T): T | undefined {
|
||||||
|
try {
|
||||||
|
const stored = GM_getValue(key);
|
||||||
|
if (stored === undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return JSON.parse(stored) as T;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to retrieve value for key "${key}":`, error);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a value from persistent storage
|
||||||
|
*/
|
||||||
|
static remove(key: string): void {
|
||||||
|
GM_deleteValue(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a key exists in storage
|
||||||
|
*/
|
||||||
|
static has(key: string): boolean {
|
||||||
|
return GM_getValue(key) !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all keys from storage
|
||||||
|
*/
|
||||||
|
static getAllKeys(): string[] {
|
||||||
|
return GM_listValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all storage (use with caution!)
|
||||||
|
*/
|
||||||
|
static clear(): void {
|
||||||
|
const keys = GM_listValues();
|
||||||
|
keys.forEach(key => GM_deleteValue(key));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import pkg from "../package.json";
|
|
||||||
import config from "../header.config.json";
|
import config from "../header.config.json";
|
||||||
|
import pkg from "../package.json";
|
||||||
|
|
||||||
const targetFile = "./dist/" + pkg.name + ".user.js";
|
const targetFile = "./dist/" + pkg.name + ".user.js";
|
||||||
|
|
||||||
|
@ -60,13 +60,24 @@ function removeEmptyLinesFromString(string: string): string {
|
||||||
* Generates user script header
|
* Generates user script header
|
||||||
*/
|
*/
|
||||||
async function generateUserScriptHeader() {
|
async function generateUserScriptHeader() {
|
||||||
const excludes = generateMultipleEntries("@exclude", config.excludes);
|
// Determine environment from NODE_ENV or default to production
|
||||||
|
const environment = process.env.NODE_ENV === 'development' ? 'development' : 'production';
|
||||||
|
console.log(`🔧 Building UserScript header for environment: ${environment}`);
|
||||||
|
|
||||||
|
// Get environment-specific config or fallback to empty arrays
|
||||||
|
const envConfig = config.environments?.[environment] || {
|
||||||
|
includes: [],
|
||||||
|
excludes: [],
|
||||||
|
grants: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const excludes = generateMultipleEntries("@exclude", envConfig.excludes);
|
||||||
const requires = generateMultipleEntries("@require", config.requires);
|
const requires = generateMultipleEntries("@require", config.requires);
|
||||||
const resources = generateMultipleEntries("@resource", config.resources);
|
const resources = generateMultipleEntries("@resource", config.resources);
|
||||||
const connecters = generateMultipleEntries("@connect", config.connecters);
|
const connecters = generateMultipleEntries("@connect", config.connecters);
|
||||||
const grants = generateMultipleEntries("@grant", config.grants);
|
const grants = generateMultipleEntries("@grant", envConfig.grants);
|
||||||
const matches = generateMultipleEntries("@match", config.matches);
|
const matches = generateMultipleEntries("@match", config.matches);
|
||||||
const includes = generateMultipleEntries("@match", config.includes);
|
const includes = generateMultipleEntries("@match", envConfig.includes);
|
||||||
const antifeatures = generateMultipleEntries("@antifeature", config.antifeatures);
|
const antifeatures = generateMultipleEntries("@antifeature", config.antifeatures);
|
||||||
const base64url = await buildBase64UrlFromFile(config.iconUrl);
|
const base64url = await buildBase64UrlFromFile(config.iconUrl);
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,32 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"target": "ES6",
|
"target": "ES2020",
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
"allowJs": false,
|
"allowJs": false,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"baseUrl": "./src",
|
||||||
"paths": {
|
"paths": {
|
||||||
"undici-types": ["./node_modules/undici/types/index.d.ts"]
|
"@/*": ["*"],
|
||||||
}
|
"@/types/*": ["types/*"],
|
||||||
|
"@/utils/*": ["utils/*"],
|
||||||
|
"@/core/*": ["core/*"],
|
||||||
|
"undici-types": ["../node_modules/undici/types/index.d.ts"]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "tools/**/*.js"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,33 @@
|
||||||
import { defineConfig } from "vite";
|
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
import pkgjsn from "./package.json";
|
import pkgjsn from "./package.json";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
|
const isDev = mode === 'development';
|
||||||
|
|
||||||
|
return {
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: resolve(__dirname, "src/index.ts"),
|
input: resolve(__dirname, "src/index.ts"),
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: `${pkgjsn.name}.user.js`,
|
entryFileNames: `${pkgjsn.name}${isDev ? '.dev' : ''}.user.js`,
|
||||||
dir: resolve(__dirname, "dist"),
|
dir: resolve(__dirname, "dist"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sourcemap: "inline", // Equivalent to 'inline-source-map'
|
sourcemap: isDev ? "inline" : false,
|
||||||
|
minify: isDev ? false : 'terser',
|
||||||
},
|
},
|
||||||
plugins: [tsconfigPaths()],
|
plugins: [tsconfigPaths()],
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".tsx", ".ts", ".js"],
|
extensions: [".tsx", ".ts", ".js"],
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src'),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
__DEV__: isDev,
|
||||||
|
__VERSION__: JSON.stringify(pkgjsn.version),
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue