├── .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 | [](https://npmjs.com/vite-userscript-plugin)
4 | [](./LICENCE)
5 | [](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 | [](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/tampermonkey)
4 | [](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/greasemonkey)
5 | [](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 `