├── .github └── workflows │ └── npm-publish.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc.cjs ├── FAQ.md ├── LICENCE ├── README.md ├── examples ├── basic │ ├── package.json │ ├── src │ │ ├── button.ts │ │ ├── index.ts │ │ ├── style.scss │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts └── jsx │ ├── package.json │ ├── src │ ├── App.tsx │ ├── Counter.tsx │ ├── Heading.tsx │ ├── List.tsx │ ├── Toggle.tsx │ ├── index.tsx │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── banner.ts ├── constants.ts ├── helpers.ts ├── index.ts ├── types.ts └── ws.ts ├── test ├── __snapshots__ │ ├── banner.test.ts.snap │ └── helpers.test.ts.snap ├── banner.test.ts └── helpers.test.ts ├── tsconfig.json ├── tsup.config.ts ├── turbo.json ├── types ├── README.md ├── greasemonkey.d.ts ├── tampermonkey.d.ts └── violentmonkey.d.ts └── vitest.config.ts /.github/workflows/npm-publish.yaml: -------------------------------------------------------------------------------- 1 | name: NPM Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | cache-and-install: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Install Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | 21 | - uses: pnpm/action-setup@v4 22 | name: Install pnpm 23 | id: pnpm-install 24 | with: 25 | version: "9.9.0" 26 | run_install: false 27 | 28 | - name: Get pnpm store directory 29 | id: pnpm-cache 30 | shell: bash 31 | run: | 32 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 33 | 34 | - uses: actions/cache@v4 35 | name: Setup pnpm cache 36 | with: 37 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 38 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 39 | restore-keys: | 40 | ${{ runner.os }}-pnpm-store- 41 | 42 | - name: Install dependencies 43 | run: pnpm install 44 | 45 | - name: Build 46 | run: pnpm build 47 | 48 | - name: Publish 49 | shell: bash 50 | run: | 51 | echo "//registry.npmjs.org/:_authToken="${{ secrets.NPM_TOKEN }}"" > ~/.npmrc 52 | pnpm publish --access public 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/dist 3 | **/.turbo 4 | *.log 5 | *.tgz 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | logs 5 | .next 6 | .turbo 7 | .github 8 | .angular 9 | .svelte-kit 10 | .vscode 11 | *.log* 12 | *.log 13 | *.lock 14 | *.yaml 15 | *.yml 16 | *.sh 17 | *.svg 18 | *rc.* 19 | *.htm 20 | *.html 21 | *.json 22 | *.md 23 | .*ignore 24 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require('@crashmax/prettier-config') 2 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Tampermonkey FAQ 2 | 3 | - [Scripts fail on FF because of CSP errors](https://github.com/Tampermonkey/tampermonkey/issues/952#issuecomment-638373937) 4 | - [Updating script from local file](https://github.com/Tampermonkey/tampermonkey/issues/977#issuecomment-657268478) 5 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Vitalij Ryndin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vite-userscript-plugin 2 | 3 | [![npm](https://img.shields.io/npm/v/vite-userscript-plugin)](https://npmjs.com/vite-userscript-plugin) 4 | [![license](https://img.shields.io/github/license/crashmax-dev/vite-userscript-plugin)](./LICENCE) 5 | [![template](https://img.shields.io/github/package-json/v/crashmax-dev/vite-userscript-template?label=vite-userscript-template)](https://github.com/crashmax-dev/vite-userscript-template) 6 | 7 | > ⚡️ A plugin for developing and building a Tampermonkey userscript based on [Vite](https://vitejs.dev). 8 | 9 | ## Table of contents 10 | 11 | - [Features](#features) 12 | - [Install](#install) 13 | - [Setup config](#setup-config) 14 | - [Using style modules](#using-style-modules) 15 | - [Plugin configuration](#plugin-configuration) 16 | 17 | ## Features 18 | 19 | - 🔥 Reloading page after changing any files. 20 | - 🔧 Configure Tampermonkey's Userscript header. 21 | - 💨 Import all [`grant`](https://www.tampermonkey.net/documentation.php#_grant)'s to the header by default in development mode. 22 | - 📝 Automatic addition of used [`grant`](https://www.tampermonkey.net/documentation.php#_grant)'s in the code when building for production. 23 | - 📦 Built-in Tampermonkey's TypeScript type definition. 24 | 25 | ## Install 26 | 27 | ``` 28 | npm install vite-userscript-plugin -D 29 | ``` 30 | 31 | ``` 32 | yarn add vite-userscript-plugin -D 33 | ``` 34 | 35 | ``` 36 | pnpm add vite-userscript-plugin -D 37 | ``` 38 | 39 | ### Setup config 40 | 41 | ```js 42 | import { defineConfig } from 'vite' 43 | import Userscript from 'vite-userscript-plugin' 44 | import { name, version } from './package.json' 45 | 46 | export default defineConfig((config) => { 47 | return { 48 | plugins: [ 49 | Userscript({ 50 | entry: 'src/index.ts', 51 | header: { 52 | name, 53 | version, 54 | match: [ 55 | 'https://example.com/', 56 | 'https://example.org/', 57 | 'https://example.edu/' 58 | ] 59 | }, 60 | server: { 61 | port: 3000 62 | } 63 | }) 64 | ] 65 | } 66 | }) 67 | ``` 68 | 69 | ### Setup NPM scripts 70 | 71 | ```json 72 | // package.json 73 | { 74 | "scripts": { 75 | "dev": "vite build --watch --mode development", 76 | "build": "vite build" 77 | } 78 | } 79 | ``` 80 | 81 | ### Setup TypeScript [types](https://www.typescriptlang.org/tsconfig#types) 82 | 83 | ```json 84 | // tsconfig.json 85 | { 86 | "compilerOptions": { 87 | "types": [ 88 | "vite-userscript-plugin/types/tampermonkey" 89 | ] 90 | } 91 | } 92 | ``` 93 | 94 | ### Using style modules 95 | 96 | ```js 97 | import style from './style.css?raw' 98 | 99 | // inject style element 100 | const styleElement = GM_addStyle(style) 101 | 102 | // remove style element 103 | styleElement.remove() 104 | ``` 105 | 106 | ## Plugin configuration 107 | 108 | ```ts 109 | interface ServerConfig { 110 | /** 111 | * {@link https://github.com/sindresorhus/get-port} 112 | */ 113 | port?: number; 114 | 115 | /** 116 | * @default false 117 | */ 118 | open?: boolean; 119 | } 120 | 121 | interface UserscriptPluginConfig { 122 | /** 123 | * Path of userscript entry. 124 | */ 125 | entry: string; 126 | 127 | /** 128 | * Userscript header config. 129 | * 130 | * @see https://www.tampermonkey.net/documentation.php 131 | */ 132 | header: HeaderConfig; 133 | 134 | /** 135 | * Server config. 136 | */ 137 | server?: ServerConfig; 138 | } 139 | ``` 140 | 141 | ## Examples 142 | 143 | See the [examples](https://github.com/crashmax-dev/vite-userscript-plugin/tree/master/examples) folder. 144 | 145 | ## License 146 | 147 | [MIT](./LICENCE) © [crashmax](https://github.com/crashmax-dev) 148 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite build --watch --mode development", 8 | "build": "vite build" 9 | }, 10 | "devDependencies": { 11 | "sass": "1.78.0", 12 | "vite-userscript-plugin": "workspace:*" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/basic/src/button.ts: -------------------------------------------------------------------------------- 1 | export function createButton() { 2 | const el = document.createElement('button') 3 | el.textContent = 'Button' 4 | el.addEventListener('click', () => console.log(import.meta.env)) 5 | return el 6 | } 7 | -------------------------------------------------------------------------------- /examples/basic/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createButton } from './button.js' 2 | import style from './style.scss?raw' 3 | 4 | GM_addStyle(style) 5 | 6 | const div = document.querySelector('div')! 7 | div.appendChild(createButton()) 8 | -------------------------------------------------------------------------------- /examples/basic/src/style.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | text-decoration: underline; 3 | } 4 | -------------------------------------------------------------------------------- /examples/basic/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@crashmax/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src/**/*" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/basic/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import Userscript from 'vite-userscript-plugin' 3 | 4 | import { name, version } from './package.json' 5 | 6 | export default defineConfig((config) => { 7 | return { 8 | plugins: [ 9 | Userscript({ 10 | entry: 'src/index.ts', 11 | header: { 12 | name, 13 | version, 14 | match: 'https://example.com/' 15 | }, 16 | server: { 17 | port: 2000 18 | }, 19 | esbuildTransformOptions: { 20 | minify: false 21 | } 22 | }) 23 | 24 | ] 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /examples/jsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsx", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite build --watch --mode development", 8 | "build": "vite build" 9 | }, 10 | "devDependencies": { 11 | "babel-plugin-transform-react-jsx": "6.24.1", 12 | "vite-redom-jsx": "2.1.1", 13 | "vite-userscript-plugin": "workspace:*" 14 | }, 15 | "dependencies": { 16 | "redom-jsx": "3.29.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/jsx/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { mount, unmount } from 'redom-jsx' 2 | import type { RedomComponent, RedomEl } from 'redom-jsx' 3 | 4 | import { Counter } from './Counter.js' 5 | 6 | export class App implements RedomComponent { 7 | el: RedomEl 8 | 9 | private counter: RedomComponent 10 | private button: HTMLElement 11 | 12 | constructor() { 13 | // prettier-ignore 14 | ;
15 | 19 | 31 |
32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/jsx/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import type { RedomComponent, RedomEl, RedomProps } from 'redom-jsx' 2 | 3 | interface Props { 4 | initialValue?: number 5 | } 6 | 7 | export class Counter implements RedomComponent { 8 | el: RedomEl 9 | 10 | private counter: number 11 | private initialCounter: number 12 | private interval: ReturnType 13 | 14 | constructor({ initialValue }: RedomProps) { 15 | this.initialCounter = initialValue ?? 0 16 | this.counter = this.initialCounter 17 | this.render() 18 | } 19 | 20 | update(): void { 21 | this.counter++ 22 | this.renderCounter() 23 | } 24 | 25 | onmount(): void { 26 | this.renderCounter() 27 | this.interval = setInterval(() => this.update(), 1000) 28 | } 29 | 30 | onunmount(): void { 31 | clearInterval(this.interval) 32 | this.counter = this.initialCounter 33 | } 34 | 35 | private renderCounter(): void { 36 | this.el.textContent = `Count: ${this.counter}` 37 | } 38 | 39 | private render(): void { 40 | // prettier-ignore 41 | ;

42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/jsx/src/Heading.tsx: -------------------------------------------------------------------------------- 1 | import { router } from 'redom-jsx' 2 | import type { RedomComponent, RedomEl, Router } from 'redom-jsx' 3 | 4 | class H1 implements RedomComponent { 5 | el: RedomEl 6 | 7 | constructor() { 8 | // prettier-ignore 9 | ;

Lorem ipsum dolor sit amet. (h1)

10 | } 11 | } 12 | 13 | class H2 implements RedomComponent { 14 | el: RedomEl 15 | 16 | constructor() { 17 | // prettier-ignore 18 | ;

Lorem ipsum dolor sit amet. (h2)

19 | } 20 | } 21 | 22 | export class Heading { 23 | el: Router 24 | toggle = true 25 | 26 | constructor() { 27 | this.el = router('span', { 28 | h1: H1, 29 | h2: H2 30 | }) 31 | 32 | this.el.update('h1') 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/jsx/src/List.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'redom-jsx' 2 | import type { 3 | RedomComponent, 4 | RedomEl, 5 | RedomElement, 6 | RedomProps 7 | } from 'redom-jsx' 8 | 9 | interface ItemsProps { 10 | count: number 11 | } 12 | 13 | class Items implements RedomComponent { 14 | el: RedomEl 15 | 16 | private count: number 17 | 18 | constructor({ count }: RedomProps) { 19 | this.count = count 20 | this.render() 21 | } 22 | 23 | update(): void { 24 | this.el.innerHTML = '' 25 | mount(this.el, <>{this.generateList()}) 26 | } 27 | 28 | private generateList(): RedomElement[] { 29 | const list = Array.from({ length: this.count }, () => 30 | Math.random().toString(16).slice(2) 31 | ) 32 | 33 | return list.map((value) => ( 34 | <> 35 |
36 |

{value}

37 | 38 | )) 39 | } 40 | 41 | private render(): void { 42 | // prettier-ignore 43 | ;{this.generateList()} 44 | } 45 | } 46 | 47 | export class List implements RedomComponent { 48 | el: RedomEl 49 | 50 | private items: Items 51 | 52 | constructor() { 53 | this.render() 54 | } 55 | 56 | private render(): void { 57 | // prettier-ignore 58 | ;
59 |

List

60 | 61 | 62 |
63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/jsx/src/Toggle.tsx: -------------------------------------------------------------------------------- 1 | import type { RedomComponent, RedomEl } from 'redom-jsx' 2 | 3 | import { Heading } from './Heading.js' 4 | 5 | export class Toggle implements RedomComponent { 6 | el: RedomEl 7 | 8 | private heading: Heading 9 | 10 | constructor() { 11 | // prettier-ignore 12 | ;
13 | 14 | 22 |
23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/jsx/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'redom-jsx' 2 | 3 | import { App } from './App.js' 4 | import { List } from './List.js' 5 | import { Toggle } from './Toggle.js' 6 | 7 | mount(document.body, ) 8 | mount(document.body, ) 9 | mount(document.body, ) 10 | -------------------------------------------------------------------------------- /examples/jsx/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/jsx/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@crashmax/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "jsx": "preserve", 6 | "types": [ 7 | "vite-userscript-plugin/types/tampermonkey" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/jsx/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import Redom from 'vite-redom-jsx' 3 | import Userscript from 'vite-userscript-plugin' 4 | 5 | import { name, version } from './package.json' 6 | 7 | export default defineConfig((config) => { 8 | return { 9 | plugins: [ 10 | Redom(), 11 | Userscript({ 12 | entry: 'src/index.tsx', 13 | header: { 14 | name, 15 | version, 16 | match: 'https://example.com/' 17 | }, 18 | server: { 19 | port: 4000 20 | }, 21 | esbuildTransformOptions: { 22 | minify: false 23 | } 24 | }) 25 | 26 | ] 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-userscript-plugin", 3 | "version": "1.11.0", 4 | "type": "module", 5 | "types": "./dist/index.d.ts", 6 | "exports": { 7 | ".": "./dist/index.js" 8 | }, 9 | "files": [ 10 | "dist", 11 | "types" 12 | ], 13 | "packageManager": "pnpm@9.9.0", 14 | "engines": { 15 | "node": ">=20" 16 | }, 17 | "scripts": { 18 | "dev": "tsup --watch", 19 | "build": "tsup", 20 | "test": "vitest", 21 | "test:ui": "vitest --ui --watch", 22 | "dev:examples": "turbo run dev --filter=./examples/*", 23 | "build:examples": "turbo run build --filter=./examples/*", 24 | "format": "prettier --write --ignore-unknown **" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/crashmax-dev/vite-userscript-plugin.git" 29 | }, 30 | "keywords": [ 31 | "vite", 32 | "vite-plugin", 33 | "userscript", 34 | "tampermonkey", 35 | "greasemonkey", 36 | "violentmonkey" 37 | ], 38 | "author": { 39 | "name": "Vitalij Ryndin", 40 | "email": "sys@crashmax.ru", 41 | "url": "https://crashmax.ru" 42 | }, 43 | "license": "MIT", 44 | "bugs": { 45 | "url": "https://github.com/crashmax-dev/vite-userscript-plugin/issues" 46 | }, 47 | "dependencies": { 48 | "get-port": "7.1.0", 49 | "open": "10.1.0", 50 | "picocolors": "1.1.0", 51 | "serve-handler": "6.1.5", 52 | "websocket": "1.0.35" 53 | }, 54 | "devDependencies": { 55 | "@crashmax/prettier-config": "5.0.2", 56 | "@crashmax/tsconfig": "2.1.0", 57 | "@types/node": "22.5.4", 58 | "@types/serve-handler": "6.1.4", 59 | "@types/websocket": "1.0.10", 60 | "@vitest/ui": "2.0.5", 61 | "tsup": "8.2.4", 62 | "turbo": "2.1.1", 63 | "typescript": "5.5.4", 64 | "vite": "5.4.3", 65 | "vite-plugin-dts": "4.1.1", 66 | "vitest": "2.0.5" 67 | }, 68 | "peerDependencies": { 69 | "vite": ">=3.0.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'examples/*' 3 | -------------------------------------------------------------------------------- /src/banner.ts: -------------------------------------------------------------------------------- 1 | import type { HeaderConfig } from './types.js' 2 | 3 | export class Banner { 4 | private header: string[] = [] 5 | private maxKeyLength: number 6 | 7 | constructor(private readonly config: HeaderConfig) { 8 | this.addHomepageMeta() 9 | this.maxKeyLength = 10 | Math.max(...Object.keys(this.config).map((key) => key.length)) + 1 11 | } 12 | 13 | private addHomepageMeta(): void { 14 | const homePage = this.config.homepage ?? this.config.homepageURL 15 | if (homePage) { 16 | this.config.updateURL = new URL( 17 | `${this.config.name}.meta.js`, 18 | homePage 19 | ).href 20 | this.config.downloadURL = new URL( 21 | `${this.config.name}.user.js`, 22 | homePage 23 | ).href 24 | } 25 | } 26 | 27 | private addSpaces(str: string): string { 28 | return ' '.repeat(this.maxKeyLength - str.length) 29 | } 30 | 31 | private addMetadata( 32 | key: string, 33 | value: string | string[] | number | boolean 34 | ): void { 35 | value = Array.isArray(value) ? value.join(' ') : value === true ? '' : value 36 | this.header.push(`// @${key}${this.addSpaces(key)}${value}`) 37 | } 38 | 39 | generate(): string { 40 | for (const [key, value] of Object.entries(this.config)) { 41 | if (Array.isArray(value)) { 42 | value.forEach((value) => this.addMetadata(key, value)) 43 | } else { 44 | if (value === undefined) continue 45 | this.addMetadata(key, value) 46 | } 47 | } 48 | 49 | return [ 50 | '// ==UserScript==', 51 | ...this.header, 52 | '// ==/UserScript==' 53 | ].join('\n') 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | import type { Grants } from './types.js' 5 | 6 | export const pluginDir = dirname(fileURLToPath(import.meta.url)) 7 | export const pluginName = 'vite-userscript-plugin' 8 | export const regexpScripts = new RegExp(/\.(t|j)sx?$/) 9 | 10 | export const GM = [ 11 | 'setValue', 12 | 'getValue', 13 | 'deleteValue', 14 | 'listValues', 15 | 'setClipboard', 16 | 'addStyle', 17 | 'addElement', 18 | 'addValueChangeListener', 19 | 'removeValueChangeListener', 20 | 'registerMenuCommand', 21 | 'unregisterMenuCommand', 22 | 'download', 23 | 'getTab', 24 | 'getTabs', 25 | 'saveTab', 26 | 'openInTab', 27 | 'notification', 28 | 'getResourceURL', 29 | 'getResourceText', 30 | 'xmlhttpRequest', 31 | 'log', 32 | 'info' 33 | ] as const 34 | 35 | export const GMwindow = [ 36 | 'unsafeWindow', 37 | 'window.onurlchange', 38 | 'window.focus', 39 | 'window.close' 40 | ] as const 41 | 42 | export const grants = GM.map((grant) => [ 43 | `GM_${grant}`, 44 | `GM.${grant}` 45 | ]).flat() 46 | 47 | grants.push(...GMwindow) 48 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { transformWithEsbuild } from 'vite' 2 | import type { EsbuildTransformOptions } from 'vite' 3 | 4 | import { grants } from './constants.js' 5 | import type { Grants, Transform } from './types.js' 6 | 7 | export function removeDuplicates(arr: any): any[] { 8 | return [...new Set(Array.isArray(arr) ? arr : arr ? [arr] : [])] 9 | } 10 | 11 | export async function transform( 12 | { minify, file, name, loader }: Transform, 13 | transformOptions?: EsbuildTransformOptions 14 | ): Promise { 15 | const { code } = await transformWithEsbuild(file, name, { 16 | minify, 17 | loader, 18 | sourcemap: false, 19 | legalComments: 'none', 20 | ...transformOptions 21 | }) 22 | 23 | return code 24 | } 25 | 26 | export function defineGrants(code: string): Grants[] { 27 | const definedGrants: Grants[] = [] 28 | 29 | for (const grant of grants) { 30 | if (code.indexOf(grant) !== -1) { 31 | definedGrants.push(grant) 32 | } 33 | } 34 | 35 | return definedGrants 36 | } 37 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'node:fs' 2 | import { createServer } from 'node:http' 3 | import { resolve } from 'node:path' 4 | import getPort from 'get-port' 5 | import openLink from 'open' 6 | import colors from 'picocolors' 7 | import serveHandler from 'serve-handler' 8 | import { createLogger } from 'vite' 9 | import { server } from 'websocket' 10 | import type { PluginOption, ResolvedConfig } from 'vite' 11 | import type { connection } from 'websocket' 12 | 13 | import { Banner } from './banner.js' 14 | import { grants, pluginDir, pluginName, regexpScripts } from './constants.js' 15 | import { defineGrants, removeDuplicates, transform } from './helpers.js' 16 | import type { UserscriptPluginConfig } from './types.js' 17 | 18 | export type { UserscriptPluginConfig } 19 | 20 | export default function UserscriptPlugin( 21 | config: UserscriptPluginConfig 22 | ): PluginOption { 23 | try { 24 | let pluginConfig: ResolvedConfig 25 | let isBuildWatch: boolean 26 | let socketConnection: connection | null = null 27 | 28 | const fileName = config.fileName ?? config.header.name 29 | 30 | const logger = createLogger('info', { 31 | prefix: `[${pluginName}]`, 32 | allowClearScreen: true 33 | }) 34 | 35 | const httpServer = createServer((req, res) => { 36 | return serveHandler(req, res, { 37 | public: pluginConfig.build.outDir 38 | }) 39 | }) 40 | 41 | const WebSocketServer = server 42 | const ws = new WebSocketServer({ httpServer }) 43 | ws.on('request', (request) => { 44 | socketConnection = request.accept(null, request.origin) 45 | }) 46 | 47 | return { 48 | name: pluginName, 49 | apply: 'build', 50 | config() { 51 | return { 52 | build: { 53 | target: 'esnext', 54 | minify: false, 55 | lib: { 56 | name: fileName, 57 | entry: config.entry, 58 | formats: ['iife'], 59 | fileName: () => `${fileName}.js` 60 | }, 61 | rollupOptions: { 62 | output: { 63 | extend: true 64 | } 65 | } 66 | } 67 | } 68 | }, 69 | async configResolved(userConfig) { 70 | pluginConfig = userConfig 71 | isBuildWatch = (userConfig.build.watch ?? false) as boolean 72 | config.entry = resolve(userConfig.root, config.entry) 73 | 74 | Array.from([ 75 | 'match', 76 | 'require', 77 | 'include', 78 | 'exclude', 79 | 'resource', 80 | 'connect' 81 | ]).forEach((key) => { 82 | const value = config.header[key] 83 | config.header[key] = removeDuplicates(value) 84 | }) 85 | 86 | config.server = { 87 | port: await getPort(), 88 | open: false, 89 | ...config.server 90 | } 91 | }, 92 | async writeBundle(output, bundle) { 93 | const { open, port } = config.server! 94 | const sanitizedFilename = output.sanitizeFileName(fileName) 95 | const userFilename = `${sanitizedFilename}.user.js` 96 | const proxyFilename = `${sanitizedFilename}.proxy.user.js` 97 | const metaFilename = `${sanitizedFilename}.meta.js` 98 | 99 | for (const [fileName] of Object.entries(bundle)) { 100 | if (regexpScripts.test(fileName)) { 101 | const rootDir = pluginConfig.root 102 | const outDir = pluginConfig.build.outDir 103 | 104 | const outPath = resolve(rootDir, outDir, fileName) 105 | const userFilePath = resolve(rootDir, outDir, userFilename) 106 | const proxyFilePath = resolve(rootDir, outDir, proxyFilename) 107 | const metaFilePath = resolve(rootDir, outDir, metaFilename) 108 | const wsPath = resolve(pluginDir, `ws-${sanitizedFilename}.js`) 109 | 110 | try { 111 | let source = readFileSync(outPath, 'utf8') 112 | source = await transform( 113 | { 114 | minify: !isBuildWatch, 115 | file: source, 116 | name: fileName, 117 | loader: 'js' 118 | }, 119 | config.esbuildTransformOptions 120 | ) 121 | 122 | config.header.grant = removeDuplicates( 123 | isBuildWatch 124 | ? grants 125 | : [...defineGrants(source), ...(config.header.grant ?? [])] 126 | ) 127 | 128 | if (isBuildWatch) { 129 | const wsFile = readFileSync(resolve(pluginDir, 'ws.js'), 'utf8') 130 | 131 | const wsScript = await transform( 132 | { 133 | minify: !isBuildWatch, 134 | file: wsFile.replace('__WS__', `ws://localhost:${port}`), 135 | name: wsPath, 136 | loader: 'js' 137 | }, 138 | config.esbuildTransformOptions 139 | ) 140 | 141 | writeFileSync(wsPath, wsScript) 142 | writeFileSync( 143 | proxyFilePath, 144 | new Banner({ 145 | ...config.header, 146 | require: [ 147 | ...(config.header.require ?? []), 148 | 'file://' + wsPath, 149 | 'file://' + outPath 150 | ] 151 | }).generate() 152 | ) 153 | } 154 | 155 | const banner = new Banner(config.header).generate() 156 | writeFileSync(outPath, source) 157 | writeFileSync(metaFilePath, banner) 158 | writeFileSync(userFilePath, `${banner}\n\n${source}`) 159 | } catch (err) { 160 | console.log(err) 161 | } 162 | } 163 | } 164 | 165 | if (isBuildWatch && !httpServer.listening) { 166 | const link = `http://localhost:${port}` 167 | httpServer.listen(port, () => { 168 | logger.clearScreen('info') 169 | logger.info( 170 | colors.bold( 171 | `${colors.cyan('>>> [vite-userscript-plugin]')} ${colors.gray( 172 | link 173 | )}` 174 | ) 175 | ) 176 | }) 177 | 178 | if (open) { 179 | await openLink(`${link}/${proxyFilename}`) 180 | } 181 | } else if (!isBuildWatch) { 182 | httpServer.close() 183 | process.exit(0) 184 | } 185 | }, 186 | buildEnd() { 187 | if (isBuildWatch) { 188 | logger.clearScreen('info') 189 | 190 | if (socketConnection) { 191 | socketConnection.sendUTF( 192 | JSON.stringify({ 193 | message: 'reload' 194 | }) 195 | ) 196 | } 197 | } 198 | } 199 | } 200 | } catch (err) { 201 | console.error(err) 202 | return { 203 | name: pluginName 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { EsbuildTransformOptions } from 'vite' 2 | 3 | import { GM, GMwindow } from './constants.js' 4 | 5 | export interface Transform { 6 | minify: boolean 7 | file: string 8 | name: string 9 | loader: 'js' | 'css' 10 | } 11 | 12 | export type RunAt = 13 | | 'document-start' 14 | | 'document-body' 15 | | 'document-end' 16 | | 'document-idle' 17 | | 'context-menu' 18 | 19 | export type GMLiterals = [`GM_${T}` | `GM.${T}`] 20 | export type GMWindow = (typeof GMwindow)[number] 21 | export type Grants = GMWindow | GMLiterals<(typeof GM)[number]>[number] 22 | 23 | export type HeaderConfig = { 24 | [property: string]: any 25 | 26 | /** 27 | * @see https://www.tampermonkey.net/documentation.php#meta:name 28 | */ 29 | name: string 30 | 31 | /** 32 | * @see https://www.tampermonkey.net/documentation.php#meta:namespace 33 | */ 34 | namespace?: string 35 | 36 | /** 37 | * @see https://www.tampermonkey.net/documentation.php#meta:copyright 38 | */ 39 | copyright?: string 40 | 41 | /** 42 | * @see https://www.tampermonkey.net/documentation.php#meta:version 43 | */ 44 | version: string 45 | 46 | /** 47 | * @see https://www.tampermonkey.net/documentation.php#meta:description 48 | */ 49 | description?: string 50 | 51 | /** 52 | * @see https://www.tampermonkey.net/documentation.php#meta:icon 53 | */ 54 | icon?: string 55 | 56 | /** 57 | * @see https://www.tampermonkey.net/documentation.php#meta:icon 58 | */ 59 | iconURL?: string 60 | 61 | /** 62 | * @see https://www.tampermonkey.net/documentation.php#meta:icon 63 | */ 64 | defaulticon?: string 65 | 66 | /** 67 | * @see https://www.tampermonkey.net/documentation.php#meta:icon64 68 | */ 69 | icon64?: string 70 | 71 | /** 72 | * @see https://www.tampermonkey.net/documentation.php#meta:icon64 73 | */ 74 | icon64URL?: string 75 | 76 | /** 77 | * @see https://www.tampermonkey.net/documentation.php#meta:grant 78 | */ 79 | grant?: Grants[] 80 | 81 | /** 82 | * @see https://www.tampermonkey.net/documentation.php#meta:author 83 | */ 84 | author?: string 85 | 86 | /** 87 | * @see https://www.tampermonkey.net/documentation.php#meta:homepage 88 | */ 89 | homepage?: string 90 | 91 | /** 92 | * @see https://www.tampermonkey.net/documentation.php#meta:homepage 93 | */ 94 | homepageURL?: string 95 | 96 | /** 97 | * @see https://www.tampermonkey.net/documentation.php#meta:homepage 98 | */ 99 | website?: string 100 | 101 | /** 102 | * @see https://www.tampermonkey.net/documentation.php#meta:homepage 103 | */ 104 | source?: string 105 | 106 | /** 107 | * @see https://www.tampermonkey.net/documentation.phpmeta:antifeature 108 | */ 109 | antifeature?: [type: string, description: string][] 110 | 111 | /** 112 | * @see https://www.tampermonkey.net/documentation.php#meta:require 113 | */ 114 | require?: string[] | string 115 | 116 | /** 117 | * @see https://www.tampermonkey.net/documentation.php#meta:resource 118 | */ 119 | resource?: [key: string, value: string][] 120 | 121 | /** 122 | * @see https://www.tampermonkey.net/documentation.php#meta:include 123 | */ 124 | include?: string[] | string 125 | 126 | /** 127 | * @see https://www.tampermonkey.net/documentation.php#meta:match 128 | * @see https://violentmonkey.github.io/api/metadata-block/#match--exclude-match 129 | */ 130 | match: string[] | string 131 | 132 | /** 133 | * @see https://violentmonkey.github.io/api/metadata-block/#match--exclude-match 134 | */ 135 | 'exclude-match'?: string[] | string 136 | 137 | /** 138 | * @see https://www.tampermonkey.net/documentation.php#meta:exclude 139 | */ 140 | exclude?: string[] | string 141 | 142 | /** 143 | * @see https://www.tampermonkey.net/documentation.php#meta:run_at 144 | */ 145 | 'run-at'?: RunAt 146 | 147 | /** 148 | * @see https://www.tampermonkey.net/documentation.phpmeta:sandbox 149 | */ 150 | sandbox?: string 151 | 152 | /** 153 | * @see https://www.tampermonkey.net/documentation.php#meta:connect 154 | */ 155 | connect?: string[] | string 156 | 157 | /** 158 | * @see https://www.tampermonkey.net/documentation.php#meta:noframes 159 | */ 160 | noframes?: boolean 161 | 162 | /** 163 | * @see https://www.tampermonkey.net/documentation.php#meta:updateURL 164 | */ 165 | updateURL?: string 166 | 167 | /** 168 | * @see https://www.tampermonkey.net/documentation.php#meta:downloadURL 169 | */ 170 | downloadURL?: string 171 | 172 | /** 173 | * @see https://www.tampermonkey.net/documentation.php#meta:supportURL 174 | */ 175 | supportURL?: string 176 | 177 | /** 178 | * @see https://www.tampermonkey.net/documentation.php#meta:webRequest 179 | */ 180 | webRequest?: string[] 181 | 182 | /** 183 | * @see https://www.tampermonkey.net/documentation.php#meta:unwrap 184 | */ 185 | unwrap?: boolean 186 | } 187 | 188 | export interface ServerConfig { 189 | /** 190 | * {@link https://github.com/sindresorhus/get-port} 191 | */ 192 | port?: number 193 | 194 | /** 195 | * @default false 196 | */ 197 | open?: boolean 198 | } 199 | 200 | export interface UserscriptPluginConfig { 201 | /** 202 | * Path of userscript entry. 203 | */ 204 | entry: string 205 | 206 | /** 207 | * Userscript file name. 208 | */ 209 | fileName?: string 210 | 211 | /** 212 | * Userscript header config. 213 | * 214 | * @see https://www.tampermonkey.net/documentation.php 215 | */ 216 | header: HeaderConfig 217 | 218 | /** 219 | * Server config. 220 | */ 221 | server?: ServerConfig 222 | 223 | /** 224 | * Override default esbuild transform options. 225 | * 226 | * @default 227 | * ```json 228 | * { 229 | * "minify": true, 230 | * "legalComments": "none" 231 | * } 232 | * ``` 233 | */ 234 | esbuildTransformOptions?: Omit< 235 | EsbuildTransformOptions, 236 | 'format' | 'target' | 'loader' | 'sourcemap' 237 | > 238 | } 239 | -------------------------------------------------------------------------------- /src/ws.ts: -------------------------------------------------------------------------------- 1 | function connection() { 2 | const ws = new WebSocket('__WS__') 3 | 4 | ws.addEventListener('close', () => { 5 | setTimeout(connection, 1000) 6 | }) 7 | 8 | ws.addEventListener('error', () => { 9 | ws.close() 10 | }) 11 | 12 | ws.addEventListener('message', () => { 13 | location.reload() 14 | }) 15 | } 16 | 17 | connection() 18 | -------------------------------------------------------------------------------- /test/__snapshots__/banner.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`banner default snapshot 1`] = ` 4 | "// ==UserScript== 5 | // @name vitest 6 | // @version 1.0.0 7 | // @author John Doe 8 | // @description vitest 9 | // @namespace vitest 10 | // @connect vitest.dev 11 | // @license MIT 12 | // @noframes 13 | // @icon https://vitest.dev/favicon.ico 14 | // @icon64 https://vitest.dev/favicon.ico 15 | // @exclude https://vitest.dev/guide/* 16 | // @exclude https://vitest.dev/api/* 17 | // @include https://vitest.dev 18 | // @homepage https://github.com/vitest-dev/vitest 19 | // @downloadURL https://github.com/vitest-dev/vitest.user.js 20 | // @supportURL https://vitest.dev 21 | // @updateURL https://github.com/vitest-dev/vitest.meta.js 22 | // @resource vitest https://vitest.dev 23 | // @require https://example.com/index.js 24 | // @grant GM_setValue 25 | // @grant GM.setValue 26 | // @grant GM_getValue 27 | // @grant GM.getValue 28 | // @grant GM_deleteValue 29 | // @grant GM.deleteValue 30 | // @grant GM_listValues 31 | // @grant GM.listValues 32 | // @grant GM_setClipboard 33 | // @grant GM.setClipboard 34 | // @grant GM_addStyle 35 | // @grant GM.addStyle 36 | // @grant GM_addElement 37 | // @grant GM.addElement 38 | // @grant GM_addValueChangeListener 39 | // @grant GM.addValueChangeListener 40 | // @grant GM_removeValueChangeListener 41 | // @grant GM.removeValueChangeListener 42 | // @grant GM_registerMenuCommand 43 | // @grant GM.registerMenuCommand 44 | // @grant GM_unregisterMenuCommand 45 | // @grant GM.unregisterMenuCommand 46 | // @grant GM_download 47 | // @grant GM.download 48 | // @grant GM_getTab 49 | // @grant GM.getTab 50 | // @grant GM_getTabs 51 | // @grant GM.getTabs 52 | // @grant GM_saveTab 53 | // @grant GM.saveTab 54 | // @grant GM_openInTab 55 | // @grant GM.openInTab 56 | // @grant GM_notification 57 | // @grant GM.notification 58 | // @grant GM_getResourceURL 59 | // @grant GM.getResourceURL 60 | // @grant GM_getResourceText 61 | // @grant GM.getResourceText 62 | // @grant GM_xmlhttpRequest 63 | // @grant GM.xmlhttpRequest 64 | // @grant GM_log 65 | // @grant GM.log 66 | // @grant GM_info 67 | // @grant GM.info 68 | // @grant unsafeWindow 69 | // @grant window.onurlchange 70 | // @grant window.focus 71 | // @grant window.close 72 | // @match https://vitest.dev 73 | // @run-at document-start 74 | // ==/UserScript==" 75 | `; 76 | 77 | exports[`banner meta snapshot 1`] = ` 78 | "// ==UserScript== 79 | // @name vitest 80 | // @version 1.0.0 81 | // @match https://example.com 82 | // @homepage https://crashmax-dev.github.io/jsx/ 83 | // @updateURL https://crashmax-dev.github.io/jsx/vitest.meta.js 84 | // @downloadURL https://crashmax-dev.github.io/jsx/vitest.user.js 85 | // ==/UserScript==" 86 | `; 87 | -------------------------------------------------------------------------------- /test/__snapshots__/helpers.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`defineGrants snapshot 1`] = ` 4 | [ 5 | "GM_addStyle", 6 | "GM_notification", 7 | "GM_info", 8 | ] 9 | `; 10 | 11 | exports[`removeDuplicates snapshot 1`] = ` 12 | [ 13 | "GM_addElement", 14 | "GM_addStyle", 15 | "GM_download", 16 | ] 17 | `; 18 | -------------------------------------------------------------------------------- /test/banner.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | 3 | import { Banner } from '../src/banner.js' 4 | import { grants } from '../src/constants.js' 5 | import type { HeaderConfig } from '../src/types.js' 6 | 7 | const defaultBanner: HeaderConfig = { 8 | name: 'vitest', 9 | version: '1.0.0', 10 | author: 'John Doe', 11 | description: 'vitest', 12 | namespace: 'vitest', 13 | connect: 'vitest.dev', 14 | license: 'MIT', 15 | noframes: true, 16 | icon: 'https://vitest.dev/favicon.ico', 17 | icon64: 'https://vitest.dev/favicon.ico', 18 | exclude: ['https://vitest.dev/guide/*', 'https://vitest.dev/api/*'], 19 | include: 'https://vitest.dev', 20 | homepage: 'https://github.com/vitest-dev/vitest', 21 | downloadURL: 'https://vitest.dev', 22 | supportURL: 'https://vitest.dev', 23 | updateURL: 'https://vitest.dev', 24 | resource: [['vitest', 'https://vitest.dev']], 25 | require: 'https://example.com/index.js', 26 | grant: [...grants], 27 | match: 'https://vitest.dev', 28 | 'run-at': 'document-start' 29 | } 30 | 31 | test('banner default snapshot', () => { 32 | const banner = new Banner(defaultBanner).generate() 33 | expect(banner).toMatchSnapshot() 34 | }) 35 | 36 | const metaBanner: HeaderConfig = { 37 | name: 'vitest', 38 | version: '1.0.0', 39 | match: 'https://example.com', 40 | homepage: 'https://crashmax-dev.github.io/jsx/' 41 | } 42 | 43 | test('banner meta snapshot', () => { 44 | const banner = new Banner(metaBanner).generate() 45 | expect(banner).toMatchSnapshot() 46 | }) 47 | -------------------------------------------------------------------------------- /test/helpers.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | 3 | import { defineGrants, removeDuplicates } from '../src/helpers.js' 4 | import type { Grants } from '../src/types.js' 5 | 6 | test('defineGrants snapshot', () => { 7 | const code = `(function(){"use strict";function e(){const t=document.createElement("button");return t.textContent="Button",t.addEventListener("click",()=>{GM_notification({text:"Hello"})}),t}document.querySelector("div").appendChild(e()),console.log(GM_info),GM_addStyle("button{border:none;background-color:tomato;padding:1rem;font-size:1rem;font-weight:600;border-radius:1rem}")})();` 8 | const grants = defineGrants(code) 9 | expect(grants).toMatchSnapshot() 10 | }) 11 | 12 | test('removeDuplicates snapshot', () => { 13 | const grants: Grants[] = [ 14 | 'GM_addElement', 15 | 'GM_addElement', 16 | 'GM_addStyle', 17 | 'GM_download', 18 | 'GM_addStyle' 19 | ] 20 | 21 | expect(removeDuplicates(grants)).toMatchSnapshot() 22 | }) 23 | 24 | test('removeDuplicates insert string to array', () => { 25 | const str = 'hello' 26 | expect(removeDuplicates(str)).toEqual([str]) 27 | }) 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@crashmax/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "emitDeclarationOnly": true, 6 | "verbatimModuleSyntax": true 7 | }, 8 | "include": [ 9 | "src", 10 | "types" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig((option) => { 4 | return [ 5 | { 6 | entry: ['src/index.ts'], 7 | format: 'esm', 8 | external: ['vite'], 9 | target: 'node20', 10 | dts: true, 11 | clean: true, 12 | watch: option.watch 13 | }, 14 | { 15 | entry: ['src/ws.ts'], 16 | format: 'esm', 17 | target: 'esnext', 18 | platform: 'browser', 19 | clean: true, 20 | watch: option.watch 21 | } 22 | ] 23 | }) 24 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "tasks": { 4 | "dev": { 5 | "dependsOn": [ 6 | "^build" 7 | ], 8 | "cache": false 9 | }, 10 | "build": { 11 | "dependsOn": [ 12 | "//#build" 13 | ], 14 | "outputs": [ 15 | "dist/**" 16 | ] 17 | }, 18 | "//#dev": { 19 | "outputs": [ 20 | "dist/**" 21 | ] 22 | }, 23 | "//#build": { 24 | "outputs": [ 25 | "dist/**" 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /types/README.md: -------------------------------------------------------------------------------- 1 | # Userscript type definitions 2 | 3 | [![tampermonkey](https://img.shields.io/npm/v/@types/tampermonkey?label=%40types%2Ftampermonkey)](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/tampermonkey) 4 | [![greasemonkey](https://img.shields.io/npm/v/@types/greasemonkey?label=%40types%2Fgreasemonkey)](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/greasemonkey) 5 | [![violentmonkey](https://img.shields.io/npm/v/@violentmonkey/types?label=%40violentmonkey%2Ftypes)](https://github.com/violentmonkey/types) 6 | 7 | > Type declaration for `GM_*`, `GM.*` APIs in Tampermonkey, Greasemonkey and Violentmonkey. 8 | 9 | ## Installation 10 | 11 | ```sh 12 | npm install --save @types/tampermonkey 13 | ``` 14 | 15 | ```sh 16 | npm install --save @types/greasemonkey 17 | ``` 18 | 19 | ```sh 20 | npm install --save @violentmonkey/types 21 | ``` 22 | -------------------------------------------------------------------------------- /types/greasemonkey.d.ts: -------------------------------------------------------------------------------- 1 | // This definition is based on the API reference of Greasemonkey 2 | // https://wiki.greasespot.net/Greasemonkey_Manual:API 3 | // TypeScript Version: 3.2 4 | 5 | declare namespace GM { 6 | interface ScriptInfo { 7 | /** Possibly empty string. */ 8 | description: string 9 | excludes: string[] 10 | includes: string[] 11 | matches: string[] 12 | name: string 13 | /** Possibly empty string. */ 14 | namespace: string 15 | /** 16 | * An object keyed by resource name. 17 | * Each value is an object with keys `name` and `mimetype` and `url` 18 | * with string values. 19 | */ 20 | resources: { 21 | [resourceName: string]: { 22 | name: string 23 | mimetype: string 24 | url: string 25 | } 26 | } 27 | /** @default 'end' */ 28 | runAt: 'start' | 'end' | 'idle' 29 | uuid: string 30 | /** Possibly empty string. */ 31 | version: string 32 | } 33 | 34 | type Value = string | boolean | number 35 | 36 | interface Response { 37 | readonly responseHeaders: string 38 | readonly finalUrl: string 39 | /** The same object passed into the original request */ 40 | readonly context?: TContext | undefined 41 | 42 | readonly readyState: 1 | 2 | 3 | 4 43 | readonly response: any 44 | readonly responseText: string 45 | readonly responseXML: Document | false 46 | readonly status: number 47 | readonly statusText: string 48 | } 49 | 50 | interface ProgressResponse extends Response { 51 | lengthComputable: boolean 52 | loaded: number 53 | total: number 54 | } 55 | 56 | interface Request { 57 | // Fields 58 | 59 | /** 60 | * The URL to make the request to. Must be an absolute URL, beginning 61 | * with the scheme. May be relative to the current page. 62 | */ 63 | url: string 64 | /** String type of HTTP request to make (E.G. "GET", "POST") */ 65 | method: 66 | | 'GET' 67 | | 'POST' 68 | | 'PUT' 69 | | 'DELETE' 70 | | 'PATCH' 71 | | 'HEAD' 72 | | 'TRACE' 73 | | 'OPTIONS' 74 | | 'CONNECT' 75 | /** 76 | * When true, the data is sent as a Blob 77 | * @default false 78 | */ 79 | binary?: boolean | undefined 80 | /** 81 | * Any object (Compatibility: 1.10+). This object will also be the 82 | * context property of the Response Object. 83 | */ 84 | context?: TContext | undefined 85 | /** 86 | * Data to send in the request body. Usually for POST method requests. 87 | * If the data field contains form-encoded data, you usually must also 88 | * set the header `'Content-Type': 'application/x-www-form-urlencoded'` 89 | * in the `headers` field. 90 | */ 91 | data?: string | undefined 92 | /** A set of headers to include in the request */ 93 | headers?: 94 | | { 95 | [header: string]: string 96 | } 97 | | undefined 98 | /** 99 | * A MIME type to specify with the request (e.g. 100 | * "text/html; charset=ISO-8859-1") 101 | */ 102 | overrideMimeType?: string | undefined 103 | /** User name to use for authentication purposes. */ 104 | user?: string | undefined 105 | /** Password to use for authentication purposes */ 106 | password?: string | undefined 107 | /** Decode the response as specified type. Default value is "text" */ 108 | responseType?: XMLHttpRequestResponseType | undefined 109 | /** 110 | * When `true`, this is a synchronous request. 111 | * Be careful: The entire Firefox UI will be locked and frozen until the 112 | * request completes.In this mode, more data will be available in the 113 | * return value. 114 | */ 115 | synchronous?: boolean | undefined 116 | /** 117 | * The number of milliseconds to wait before terminating the call. Zero 118 | * (the default) means wait forever. 119 | */ 120 | timeout?: number | undefined 121 | /** 122 | * Object containing optional function callbacks to monitor the upload 123 | * of data. 124 | */ 125 | upload?: 126 | | { 127 | onabort?(response: Response): void 128 | onerror?(response: Response): void 129 | onload?(response: Response): void 130 | onprogress?(response: ProgressResponse): void 131 | } 132 | | undefined 133 | 134 | // Event handlers 135 | 136 | /** Will be called when the request is aborted */ 137 | onabort?(response: Response): void 138 | /** Will be called if an error occurs while processing the request */ 139 | onerror?(response: Response): void 140 | /** Will be called when the request has completed successfully */ 141 | onload?(response: Response): void 142 | /** Will be called when the request progress changes */ 143 | onprogress?(response: ProgressResponse): void 144 | /** Will be called repeatedly while the request is in progress */ 145 | onreadystatechange?(response: Response): void 146 | /** Will be called if/when the request times out */ 147 | ontimeout?(response: Response): void 148 | } 149 | } 150 | 151 | /** 152 | * Window object of the content page where the user script is running on. 153 | * @see {@link http://wiki.greasespot.net/UnsafeWindow} 154 | */ 155 | declare var unsafeWindow: Window 156 | 157 | declare var GM: { 158 | // Headers 159 | 160 | /** 161 | * Meta data about the running user script. 162 | * @see {@link https://wiki.greasespot.net/GM.info} 163 | */ 164 | info: { 165 | /** An object containing data about the currently running script */ 166 | script: GM.ScriptInfo 167 | /** 168 | * A string, the entire literal Metadata Block (without the delimiters) 169 | * for the currently running script 170 | */ 171 | scriptMetaStr: string 172 | /** 173 | * The name of the user script engine handling this script's execution. 174 | * The string `Greasemonkey` 175 | */ 176 | scriptHandler: string 177 | /** The version of Greasemonkey, a string e.g. `4.0` */ 178 | version: string 179 | } 180 | 181 | // Values 182 | 183 | /** 184 | * Allows user script authors to persist simple values across page loads and 185 | * across origins. 186 | * Strings, booleans, and integers are currently the only allowed data types. 187 | * @see {@link https://wiki.greasespot.net/GM.setValue} 188 | * @param name The unique (within this script) name for this value. 189 | * Should be restricted to valid Javascript identifier characters. 190 | * @param value Any valid value of these types. Any other type may cause 191 | * undefined behavior, including crashes 192 | * @returns A Promise, resolved successfully with no value on success, 193 | * rejected with no value on failure 194 | */ 195 | setValue(name: string, value: GM.Value): Promise 196 | 197 | /** 198 | * Retrieves a value that was set with `GM.setValue` 199 | * @see {@link https://wiki.greasespot.net/GM.getValue} 200 | * @param name The property name to get 201 | * @param defaultValue The default value to be returned when none has 202 | * previously been set 203 | * @returns A Promise, rejected in case of error and otherwise resolved with: 204 | * - When this name has been set - `string`, `integer` or `boolean` as 205 | * previously set 206 | * - When this name has not been set, and default is provided - The value 207 | * passed as default 208 | * - When this name has not been set, and default is not provided - 209 | * `undefined` 210 | * @example 211 | * // Retrieving the value associated with the name 'timezoneOffset' with a default value defined: 212 | * const timezoneOffset = await GM.getValue("timezoneOffset", -5) 213 | * @example 214 | * // For structured data used `JSON.stringify()` to place an object into storage and then `JSON.parse()` to convert it back 215 | * const storedObject = JSON.parse(await GM.getValue('foo', '{}')); 216 | */ 217 | getValue(name: string): Promise 218 | getValue( 219 | name: string, 220 | defaultValue: TValue 221 | ): Promise 222 | 223 | /** 224 | * Deletes an existing name / value pair from storage. 225 | * @see {@link https://wiki.greasespot.net/GM.deleteValue} 226 | * @param name Property name to delete 227 | * @returns A Promise, resolved successfully with no value on success, 228 | * rejected with no value on failure. 229 | */ 230 | deleteValue(name: string): Promise 231 | 232 | /** 233 | * Retrieves an array of preference names that this script has stored 234 | * @see {@link https://wiki.greasespot.net/GM.listValues} 235 | * @returns A Promise, rejected in case of error and otherwise resolved with 236 | * an string[] for previously set values 237 | */ 238 | listValues(): Promise 239 | 240 | // Resources 241 | 242 | /** 243 | * Given a defined `@resource`, this method returns it as a URL 244 | * @see {@link https://wiki.greasespot.net/GM.getResourceUrl} 245 | * @param resourceName The name provided when the `@resource` was defined 246 | * @returns A Promise, rejected on failure and resolved with a string URL on 247 | * success. 248 | * Treat the result as opaque string. It will work where you need a URL 249 | * (for a `` or `