create-tampermonkey-typescript 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +62 -0
- package/dist/cli.js +282 -0
- package/package.json +49 -0
- package/templates/base/README.md +213 -0
- package/templates/base/src/script.ts +6 -0
- package/templates/base/src/styles.css +1 -0
- package/templates/base/userscript.txt +7 -0
- package/templates/base/vite.config.ts +55 -0
- package/templates/github-workflows/.github/workflows/auto-tag.yml +42 -0
- package/templates/github-workflows/.github/workflows/release.yml +56 -0
- package/templates/react/src/global.d.ts +14 -0
- package/templates/react/src/util/react-util.ts +24 -0
- package/templates/tsconfig.json +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 neth392
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# create-tampermonkey-typescript
|
|
2
|
+
|
|
3
|
+
> A CLI scaffolding tool for creating minimal TamperMonkey userscript projects written in TypeScript.
|
|
4
|
+
|
|
5
|
+
Write your userscripts in TypeScript, organize code across multiple files, and let the toolchain handle the rest
|
|
6
|
+
— bundling, CSS injection, and userscript header generation are all taken care of. The result is a single `.user.js`
|
|
7
|
+
file ready to install in TamperMonkey.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
📦 **TypeScript with multi-file support** — Structure your project however you like. All source files are bundled into
|
|
14
|
+
a single IIFE-format JavaScript file via [Vite](https://vite.dev/), fully compatible with TamperMonkey.
|
|
15
|
+
|
|
16
|
+
🏷️ **Automatic userscript header** — The `// ==UserScript==` block is generated from a template (`userscript.txt`) and
|
|
17
|
+
prepended to every build. Metadata fields such as name, version, and description are pulled directly from `package.json`,
|
|
18
|
+
keeping a single source of truth.
|
|
19
|
+
|
|
20
|
+
🎨 **CSS injection** — Import `.css` files from TypeScript as you normally would. At build time, styles are extracted
|
|
21
|
+
and bundled into the output script, then injected into the page at runtime — no manual DOM manipulation required.
|
|
22
|
+
|
|
23
|
+
⚛️ **Optional React support** — Scaffold the project with React pre-configured, including type declarations for
|
|
24
|
+
`unsafeWindow.React` and `unsafeWindow.ReactDOM`, and utility functions for accessing them at runtime.
|
|
25
|
+
|
|
26
|
+
🚀 **GitHub Actions workflows** — Optionally include CI/CD workflows that automatically create Git tags when
|
|
27
|
+
`package.json` version changes, and publish GitHub Releases with the built script attached.
|
|
28
|
+
|
|
29
|
+
🔧 **Package manager agnostic** — Generated projects are standard Node.js projects. Use npm, yarn, or pnpm —
|
|
30
|
+
whichever you prefer.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### npm
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx create-tampermonkey-typescript
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### yarn
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
yarn create tampermonkey-typescript
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### pnpm
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpm create tampermonkey-typescript
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The CLI will walk you through the project configuration — name, version, description, author — and let you
|
|
55
|
+
select optional features (React, Git). The output is your new project folder containing its own `README.md`
|
|
56
|
+
to get you started. You can preview that file here: [Generated Project README](templates/base/README.md)
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
[MIT](LICENSE)
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import prompts from 'prompts';
|
|
5
|
+
import kleur from 'kleur';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
const __FILENAME = fileURLToPath(import.meta.url);
|
|
9
|
+
const __DIRNAME = path.dirname(__FILENAME);
|
|
10
|
+
const TEMPLATES_DIR = path.join(__DIRNAME, '../templates');
|
|
11
|
+
const defaultDevDeps = [
|
|
12
|
+
'typescript', //
|
|
13
|
+
'vite',
|
|
14
|
+
'vite-plugin-banner',
|
|
15
|
+
'vite-plugin-css-injected-by-js',
|
|
16
|
+
'ts-node',
|
|
17
|
+
'@types/tampermonkey',
|
|
18
|
+
];
|
|
19
|
+
const defaultDeps = [];
|
|
20
|
+
const baseTsConfig = {
|
|
21
|
+
compilerOptions: {
|
|
22
|
+
types: ['vite/client', 'node'],
|
|
23
|
+
target: 'ES2022',
|
|
24
|
+
module: 'ESNext',
|
|
25
|
+
moduleResolution: 'NodeNext',
|
|
26
|
+
strict: true,
|
|
27
|
+
esModuleInterop: true,
|
|
28
|
+
forceConsistentCasingInFileNames: true,
|
|
29
|
+
skipLibCheck: true,
|
|
30
|
+
resolveJsonModule: true,
|
|
31
|
+
outDir: 'dist',
|
|
32
|
+
baseUrl: '.',
|
|
33
|
+
paths: {
|
|
34
|
+
'@/*': ['src/*'],
|
|
35
|
+
},
|
|
36
|
+
lib: ['ES2022', 'dom', 'dom.iterable'],
|
|
37
|
+
},
|
|
38
|
+
include: ['src'],
|
|
39
|
+
};
|
|
40
|
+
const availableFeatures = [
|
|
41
|
+
{
|
|
42
|
+
name: 'React',
|
|
43
|
+
description: 'Adds react support to the project',
|
|
44
|
+
directory: path.join(TEMPLATES_DIR, 'react'),
|
|
45
|
+
tsConfigModifier: (config) => (config.compilerOptions.jsx = 'react-jsx'),
|
|
46
|
+
devDependencies: ['react', 'react-dom', '@types/react', '@types/react-dom'],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'Git',
|
|
50
|
+
description: 'Initializes the new project as a git repository',
|
|
51
|
+
directory: path.join(TEMPLATES_DIR, 'git'),
|
|
52
|
+
hook: (params) => execInProjectDir(`git init && git add . && git commit -m "Initial commit"`, params),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'Github Workflows',
|
|
56
|
+
description: 'Includes 2 workflows; one for tagging releases with current version, and another for building & creating a release',
|
|
57
|
+
directory: path.join(TEMPLATES_DIR, 'github-workflows'),
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
const availablePackageManagers = [
|
|
61
|
+
{
|
|
62
|
+
name: 'npm',
|
|
63
|
+
exists: () => commandExists('npm --version'),
|
|
64
|
+
installCmd: 'npm install',
|
|
65
|
+
addDependencyCmd: 'npm install',
|
|
66
|
+
runCmd: 'npm run',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'yarn',
|
|
70
|
+
exists: () => commandExists('yarn --version'),
|
|
71
|
+
installCmd: 'yarn install',
|
|
72
|
+
addDependencyCmd: 'yarn add',
|
|
73
|
+
runCmd: 'yarn run',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'pnpm',
|
|
77
|
+
exists: () => commandExists('pnpm --version'),
|
|
78
|
+
installCmd: 'pnpm install',
|
|
79
|
+
addDependencyCmd: 'pnpm add',
|
|
80
|
+
runCmd: 'pnpm run',
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
async function main() {
|
|
84
|
+
console.log(kleur.green('create-tampermonkey-typescript'));
|
|
85
|
+
if (!checkCwdAccess())
|
|
86
|
+
return;
|
|
87
|
+
if (!findValidPackageManager())
|
|
88
|
+
return;
|
|
89
|
+
// Prompt for project parameters
|
|
90
|
+
const promptResults = await promptForParams();
|
|
91
|
+
const params = {
|
|
92
|
+
...promptResults,
|
|
93
|
+
projectPath: path.join(process.cwd(), promptResults.projectName),
|
|
94
|
+
features: promptResults.featureNames.map((featureName) => availableFeatures.find((a) => a.name === featureName)),
|
|
95
|
+
packageManager: availablePackageManagers.find((pm) => pm.name === promptResults.packageManagerName),
|
|
96
|
+
};
|
|
97
|
+
// Create the directory
|
|
98
|
+
logWithPrefix(`Creating directory ${kleur.yellow(params.projectPath)}`);
|
|
99
|
+
const createDirResult = await createProjectDirectory(params.projectPath);
|
|
100
|
+
if (typeof createDirResult === 'string') {
|
|
101
|
+
console.log(kleur.red('An occurred while attempting to create the project directory:'));
|
|
102
|
+
console.log(kleur.red(createDirResult));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Initialize package.json
|
|
106
|
+
logWithPrefix(`Creating ${kleur.yellow('package.json')}`);
|
|
107
|
+
const packageJson = createPackageJson(params);
|
|
108
|
+
writeObjectToFile(packageJson, 'package.json', params);
|
|
109
|
+
// Initialize the lock file
|
|
110
|
+
logWithPrefix(`Initializing lock file`);
|
|
111
|
+
execInProjectDir(`${params.packageManager.installCmd}`, params);
|
|
112
|
+
// Gather dependencies
|
|
113
|
+
const devDeps = [...defaultDevDeps, ...params.features.flatMap((f) => f.devDependencies ?? [])];
|
|
114
|
+
const deps = [...defaultDeps, ...params.features.flatMap((f) => f.dependencies ?? [])];
|
|
115
|
+
// Install dev dependencies
|
|
116
|
+
if (devDeps.length > 0) {
|
|
117
|
+
logWithPrefix('Installing dev dependencies');
|
|
118
|
+
console.log(` ${kleur.dim(devDeps.join(', '))}`);
|
|
119
|
+
execInProjectDir(`${params.packageManager.addDependencyCmd} -D ${devDeps.join(' ')}`, params);
|
|
120
|
+
}
|
|
121
|
+
// Install dependencies
|
|
122
|
+
if (deps.length > 0) {
|
|
123
|
+
logWithPrefix('Installing dependencies');
|
|
124
|
+
console.log(` ${kleur.dim(deps.join(', '))}`);
|
|
125
|
+
execInProjectDir(`${params.packageManager.addDependencyCmd} ${deps.join(' ')}`, params);
|
|
126
|
+
}
|
|
127
|
+
// Handle tsconfig.json
|
|
128
|
+
const tsConfig = { ...baseTsConfig };
|
|
129
|
+
for (const feature of params.features) {
|
|
130
|
+
if (feature.tsConfigModifier) {
|
|
131
|
+
feature.tsConfigModifier(tsConfig);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
logWithPrefix(`Creating ${kleur.yellow('tsconfig.json')}`);
|
|
135
|
+
writeObjectToFile(tsConfig, 'tsconfig.json', params);
|
|
136
|
+
// Copy files
|
|
137
|
+
logWithPrefix('Copying project files');
|
|
138
|
+
// Base files
|
|
139
|
+
fs.cpSync(path.join(TEMPLATES_DIR, 'base'), params.projectPath, { recursive: true, force: true });
|
|
140
|
+
// README.md replacements
|
|
141
|
+
const readmePath = path.join(params.projectPath, 'README.md');
|
|
142
|
+
let content = fs.readFileSync(readmePath, 'utf-8');
|
|
143
|
+
content = content.replaceAll('{{PROJECT_NAME}}', params.projectName);
|
|
144
|
+
content = content.replaceAll('{{PROJECT_PATH}}', params.projectPath);
|
|
145
|
+
content = content.replaceAll('{{PM_ADD_DEPENDENCY}}', params.packageManager.addDependencyCmd);
|
|
146
|
+
content = content.replaceAll('{{PM_RUN_SCRIPT}}', params.packageManager.runCmd);
|
|
147
|
+
fs.writeFileSync(readmePath, content);
|
|
148
|
+
// Feature files
|
|
149
|
+
params.features
|
|
150
|
+
.filter((f) => f.directory)
|
|
151
|
+
.forEach((f) => fs.cpSync(f.directory, params.projectPath, { recursive: true, force: true }));
|
|
152
|
+
// Handle feature hooks
|
|
153
|
+
params.features
|
|
154
|
+
.filter((f) => f.hook)
|
|
155
|
+
.forEach((f) => {
|
|
156
|
+
logWithPrefix(`Running feature hook: ${kleur.yellow(f.name)}`);
|
|
157
|
+
f.hook(params);
|
|
158
|
+
});
|
|
159
|
+
console.log(`${kleur.green('Done!')} Project created at ${kleur.yellow(params.projectPath)}`);
|
|
160
|
+
}
|
|
161
|
+
function logWithPrefix(message) {
|
|
162
|
+
console.log(`${kleur.cyan('-')} ${kleur.white(message)}`);
|
|
163
|
+
}
|
|
164
|
+
function commandExists(cmd) {
|
|
165
|
+
try {
|
|
166
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function checkCwdAccess() {
|
|
174
|
+
try {
|
|
175
|
+
fs.accessSync(process.cwd(), fs.constants.W_OK | fs.constants.R_OK);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
console.log(kleur.red('Read & write access to current working directory is required.'));
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
function findValidPackageManager() {
|
|
184
|
+
if (!availablePackageManagers.find((pm) => pm.exists())) {
|
|
185
|
+
console.log(kleur.red('Could not find any valid package manager: '));
|
|
186
|
+
console.log(kleur.yellow(`(${availablePackageManagers.map((pm) => pm.name).join(', ')})`));
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
function validateProjectName(projectName) {
|
|
192
|
+
if (!projectName || projectName.length === 0)
|
|
193
|
+
return 'Project name cannot be empty';
|
|
194
|
+
if (projectName.includes(' '))
|
|
195
|
+
return 'Project name cannot contain spaces';
|
|
196
|
+
const projectPath = path.join(process.cwd(), projectName);
|
|
197
|
+
try {
|
|
198
|
+
const stats = fs.statSync(projectPath);
|
|
199
|
+
if (!stats.isDirectory()) {
|
|
200
|
+
return `Path ${projectPath} already exists and is not a directory.`;
|
|
201
|
+
}
|
|
202
|
+
const files = fs.readdirSync(projectPath);
|
|
203
|
+
if (files.length > 0)
|
|
204
|
+
return `Directory ${projectPath} already exists and is not empty.`;
|
|
205
|
+
}
|
|
206
|
+
catch { }
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
async function promptForParams() {
|
|
210
|
+
return await prompts([
|
|
211
|
+
{
|
|
212
|
+
type: 'text',
|
|
213
|
+
name: 'projectName',
|
|
214
|
+
message: 'Project name:',
|
|
215
|
+
format: (s) => s.trim(),
|
|
216
|
+
validate: (s) => validateProjectName(s.trim()),
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: 'text',
|
|
220
|
+
name: 'version',
|
|
221
|
+
message: 'Version:',
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
type: 'text',
|
|
225
|
+
name: 'description',
|
|
226
|
+
message: 'Description:',
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
type: 'text',
|
|
230
|
+
name: 'author',
|
|
231
|
+
message: 'Author:',
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
type: 'select',
|
|
235
|
+
name: 'packageManagerName',
|
|
236
|
+
message: 'Package manager:',
|
|
237
|
+
choices: availablePackageManagers.filter((pm) => pm.exists()).map((pm) => ({ title: pm.name, value: pm.name })),
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
type: 'multiselect',
|
|
241
|
+
name: 'featureNames',
|
|
242
|
+
message: 'Select features:',
|
|
243
|
+
choices: availableFeatures.map((f) => ({ title: f.name, value: f.name, description: f.description })),
|
|
244
|
+
},
|
|
245
|
+
], {
|
|
246
|
+
onCancel: () => process.exit(0),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
async function createProjectDirectory(path) {
|
|
250
|
+
try {
|
|
251
|
+
fs.mkdirSync(path, { recursive: true });
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (error instanceof Error) {
|
|
256
|
+
return error.message;
|
|
257
|
+
}
|
|
258
|
+
return 'Unknown error.';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function createPackageJson(params) {
|
|
262
|
+
return {
|
|
263
|
+
name: params.projectName,
|
|
264
|
+
description: params.description,
|
|
265
|
+
version: params.version,
|
|
266
|
+
author: params.author,
|
|
267
|
+
main: 'dist/script.user.js',
|
|
268
|
+
type: 'module',
|
|
269
|
+
scripts: {
|
|
270
|
+
build: 'vite build',
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function writeObjectToFile(object, fileName, params) {
|
|
275
|
+
const filePath = path.join(params.projectPath, fileName);
|
|
276
|
+
const jsonString = JSON.stringify(object, null, 2);
|
|
277
|
+
fs.writeFileSync(filePath, jsonString);
|
|
278
|
+
}
|
|
279
|
+
function execInProjectDir(command, params) {
|
|
280
|
+
execSync(command, { stdio: 'inherit', cwd: params.projectPath });
|
|
281
|
+
}
|
|
282
|
+
main().then(() => { }, (e) => console.error(kleur.red(e.message || e)));
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-tampermonkey-typescript",
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "CLI tool for generating a TamperMonkey script project written in typescript which is transpiled to JavaScript to be ran in the browser",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/neth392/create-tampermonkey-typescript",
|
|
7
|
+
"author": "neth392",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"tampermonkey",
|
|
10
|
+
"typescript",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public",
|
|
15
|
+
"registry": "https://registry.npmjs.org/"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"templates"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"bin": {
|
|
23
|
+
"create-tampermonkey-typescript": "dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"dev": "ts-node src/cli.ts"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^24.0.10",
|
|
31
|
+
"@types/prompts": "^2.4.9",
|
|
32
|
+
"@types/react": "^19.1.8",
|
|
33
|
+
"@types/react-dom": "^19.1.6",
|
|
34
|
+
"@types/tampermonkey": "^5.0.4",
|
|
35
|
+
"prettier": "^3.6.1",
|
|
36
|
+
"react": "^19.1.0",
|
|
37
|
+
"react-dom": "^19.1.0",
|
|
38
|
+
"ts-node": "^10.9.2",
|
|
39
|
+
"type-fest": "^5.4.4",
|
|
40
|
+
"typescript": "^5.8.3",
|
|
41
|
+
"vite": "^7.0.0",
|
|
42
|
+
"vite-plugin-banner": "^0.8.1",
|
|
43
|
+
"vite-plugin-css-injected-by-js": "^3.5.2"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"kleur": "^4.1.5",
|
|
47
|
+
"prompts": "^2.4.2"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- 🚀 [Quick Start](#quick-start)
|
|
6
|
+
- 📁 [Project Structure](#project-structure)
|
|
7
|
+
- 🏷️ [Userscript Header](#userscript-header)
|
|
8
|
+
- 📦 [Build Output](#build-output)
|
|
9
|
+
- 🎨 [CSS](#css)
|
|
10
|
+
- 🔗 [Module Resolution](#module-resolution)
|
|
11
|
+
- 📚 [Dependencies](#dependencies)
|
|
12
|
+
- 🐒 [TamperMonkey API](#tampermonkey-api)
|
|
13
|
+
- ⚛️ [React](#react)
|
|
14
|
+
- ⚙️ [GitHub Workflows](#github-workflows)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
{{PM_RUN_SCRIPT}} build
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The built script is output to `dist/script.user.js`
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Project Structure
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
{{PROJECT_NAME}}/
|
|
31
|
+
├── src/
|
|
32
|
+
│ ├── script.ts # Entry point
|
|
33
|
+
│ ├── styles.css # Stylesheet (imported by script.ts)
|
|
34
|
+
│ └── ... # Your source files
|
|
35
|
+
├── userscript.txt # TamperMonkey header template
|
|
36
|
+
├── vite.config.ts # Vite build configuration
|
|
37
|
+
├── tsconfig.json
|
|
38
|
+
└── package.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
#### Added With React
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
src/
|
|
45
|
+
├── global.d.ts # Type declarations for unsafeWindow.React / ReactDOM
|
|
46
|
+
└── util/
|
|
47
|
+
└── react-util.ts # getReact() and getReactDOM() helpers
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### Added With Git / GitHub Workflows
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
.github/workflows/
|
|
54
|
+
├── auto-tag.yml # Creates a Git tag when package.json version changes
|
|
55
|
+
└── release.yml # Builds and publishes a GitHub Release
|
|
56
|
+
.gitignore
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Userscript Header
|
|
62
|
+
|
|
63
|
+
The file `userscript.txt` defines your script's TamperMonkey metadata. It is automatically prepended to the built script, with placeholders replaced from `package.json` to maintain a single source of truth:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
// ==UserScript==
|
|
67
|
+
// @name <name> ← package.json "name"
|
|
68
|
+
// @version <version> ← package.json "version"
|
|
69
|
+
// @description <description> ← package.json "description"
|
|
70
|
+
// @author <author> ← package.json "author"
|
|
71
|
+
// @homepage <homepage> ← package.json "homepage"
|
|
72
|
+
// ==/UserScript==
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
You should customize this file to include directives specific to your script, such as `@match`, `@grant`, `@require`, and so on. See the [TamperMonkey documentation](https://www.tampermonkey.net/documentation.php) for the full list of supported tags.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Build Output
|
|
80
|
+
|
|
81
|
+
[Vite](https://vitejs.dev/) bundles everything into a single IIFE-format JavaScript file:
|
|
82
|
+
|
|
83
|
+
- All TypeScript is compiled to JavaScript
|
|
84
|
+
- All imported CSS is combined and injected as an inline `<style>` element at runtime
|
|
85
|
+
- The userscript header from `userscript.txt` is prepended
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## CSS
|
|
90
|
+
|
|
91
|
+
Import `.css` files from anywhere in your source. All stylesheets are combined into a single style node and injected into the page at runtime — no manual DOM manipulation needed.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import '@/styles.css'
|
|
95
|
+
import './MyComponent.css'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Module Resolution
|
|
101
|
+
|
|
102
|
+
The `@/` path alias points to `src/`, giving you cleaner imports:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { helper } from '@/utils/helper' // → src/utils/helper.ts
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Dependencies
|
|
111
|
+
|
|
112
|
+
Any imported dependency will be included in the bundled script. To keep the output small, the recommended approach is to load libraries via TamperMonkey's [`@require`](https://www.tampermonkey.net/documentation.php?locale=en#meta:require) tag and install them as dev dependencies so they aren't bundled.
|
|
113
|
+
|
|
114
|
+
**Example with jQuery:**
|
|
115
|
+
|
|
116
|
+
Install as a dev dependency:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
{{PM_ADD_DEPENDENCY}} -D jquery
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Then add the `@require` tag to `userscript.txt`:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
// @require https://code.jquery.com/jquery-2.1.4.min.js
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
jQuery is now available at runtime without being bundled into your script.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## TamperMonkey API
|
|
133
|
+
|
|
134
|
+
TypeScript types for `GM_` functions are included out of the box. Use them directly:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
GM_getValue('key', defaultValue)
|
|
138
|
+
GM_setValue('key', value)
|
|
139
|
+
|
|
140
|
+
GM_xmlhttpRequest({
|
|
141
|
+
method: 'GET',
|
|
142
|
+
url: 'https://api.example.com',
|
|
143
|
+
onload: (res) => console.log(res.responseText),
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
GM_notification('Hello!')
|
|
147
|
+
GM_addStyle('body { color: red; }')
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Remember to declare each function you use as a grant in `userscript.txt`:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
// @grant GM_getValue
|
|
154
|
+
// @grant GM_setValue
|
|
155
|
+
// @grant GM_xmlhttpRequest
|
|
156
|
+
// @grant GM_notification
|
|
157
|
+
// @grant GM_addStyle
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## React
|
|
163
|
+
|
|
164
|
+
> This section only applies if React was enabled during project creation.
|
|
165
|
+
|
|
166
|
+
React and ReactDOM are **not bundled** into the output script. They are expected to be available on the host page via `unsafeWindow`. The generated project includes type declarations (`src/global.d.ts`) and helper functions for accessing them at runtime:
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
import { getReact, getReactDOM } from '@/util/react-util'
|
|
170
|
+
|
|
171
|
+
const React = getReact()
|
|
172
|
+
const ReactDOM = getReactDOM()
|
|
173
|
+
|
|
174
|
+
const App = () => <div>Hello World</div>
|
|
175
|
+
|
|
176
|
+
const container = document.createElement('div')
|
|
177
|
+
document.body.appendChild(container)
|
|
178
|
+
|
|
179
|
+
const root = ReactDOM.createRoot(container)
|
|
180
|
+
root.render(<App />)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## GitHub Workflows
|
|
186
|
+
|
|
187
|
+
> This section only applies if Git was enabled during project creation.
|
|
188
|
+
|
|
189
|
+
Two GitHub Actions workflows are included to automate your release process.
|
|
190
|
+
|
|
191
|
+
### Auto-Tagging (`auto-tag.yml`)
|
|
192
|
+
|
|
193
|
+
Runs on every push to `master`. Reads the version from `package.json` and creates a Git tag (e.g., `v1.2.3`) if one doesn't already exist.
|
|
194
|
+
|
|
195
|
+
### Release (`release.yml`)
|
|
196
|
+
|
|
197
|
+
Triggers when the auto-tag workflow completes successfully. Runs the build, then creates a GitHub Release with `dist/script.user.js` attached.
|
|
198
|
+
|
|
199
|
+
### Publishing Workflow
|
|
200
|
+
|
|
201
|
+
1. Update `version` in `package.json`
|
|
202
|
+
2. Commit and push to `master`
|
|
203
|
+
3. The auto-tag workflow creates a new tag
|
|
204
|
+
4. The release workflow builds and publishes the script
|
|
205
|
+
|
|
206
|
+
Users can install directly from your releases by adding these tags to `userscript.txt`:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
// @updateURL https://github.com/user/repo/releases/latest/download/script.user.js
|
|
210
|
+
// @downloadURL https://github.com/user/repo/releases/latest/download/script.user.js
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* Add all styles here */
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configured by create-tampermonkey-typescript to combine your entire codebase into a single TamperMonkey-ready script.
|
|
3
|
+
* You can modify it as you like.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { defineConfig } from 'vite'
|
|
7
|
+
import { resolve, dirname } from 'path'
|
|
8
|
+
import { fileURLToPath } from 'url'
|
|
9
|
+
import fs from 'fs'
|
|
10
|
+
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
|
11
|
+
import banner from 'vite-plugin-banner'
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
14
|
+
const __dirname = dirname(__filename)
|
|
15
|
+
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(resolve(__dirname, 'package.json'), 'utf8'))
|
|
17
|
+
const hasReact = pkg.devDependencies?.react || pkg.dependencies?.react
|
|
18
|
+
|
|
19
|
+
// Replaced in the script's header to keep package.json as the source of truth.
|
|
20
|
+
const metaTags = {
|
|
21
|
+
'<name>': pkg.name,
|
|
22
|
+
'<version>': pkg.version,
|
|
23
|
+
'<description>': pkg.description,
|
|
24
|
+
'<author>': pkg.author,
|
|
25
|
+
'<homepage>': pkg.homepage,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let meta = fs.readFileSync(resolve(__dirname, 'userscript.txt'), 'utf8')
|
|
29
|
+
|
|
30
|
+
for (const [tagName, tagValue] of Object.entries(metaTags).filter(([_, v]) => v)) {
|
|
31
|
+
meta = meta.replace(tagName, tagValue)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default defineConfig({
|
|
35
|
+
resolve: {
|
|
36
|
+
alias: {
|
|
37
|
+
'@': resolve(__dirname, 'src'),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
plugins: [cssInjectedByJsPlugin({ topExecutionPriority: true }), banner({ content: meta, verify: false })],
|
|
41
|
+
build: {
|
|
42
|
+
cssCodeSplit: false,
|
|
43
|
+
lib: {
|
|
44
|
+
entry: resolve(__dirname, 'src/script.ts'),
|
|
45
|
+
name: pkg.name.replace(/[^a-zA-Z0-9]/g, '_'),
|
|
46
|
+
formats: ['iife'],
|
|
47
|
+
fileName: () => `script.user.js`,
|
|
48
|
+
},
|
|
49
|
+
rollupOptions: {
|
|
50
|
+
...(hasReact ? { external: ['react', 'react-dom/client'] } : {}),
|
|
51
|
+
},
|
|
52
|
+
outDir: 'dist',
|
|
53
|
+
minify: false,
|
|
54
|
+
},
|
|
55
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# .github/workflows/auto-tag.yml
|
|
2
|
+
name: Auto Tag from package.json
|
|
3
|
+
|
|
4
|
+
permissions:
|
|
5
|
+
contents: write
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches:
|
|
10
|
+
- master
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
tag:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout code
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0 # Required to push tags
|
|
20
|
+
|
|
21
|
+
- name: Setup Node
|
|
22
|
+
uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: '20'
|
|
25
|
+
|
|
26
|
+
- name: Read version from package.json
|
|
27
|
+
id: pkg
|
|
28
|
+
run: |
|
|
29
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
30
|
+
echo "PACKAGE_VERSION=$VERSION" >> $GITHUB_ENV
|
|
31
|
+
echo "TAG_NAME=v$VERSION" >> $GITHUB_ENV
|
|
32
|
+
|
|
33
|
+
- name: Create tag if not exists
|
|
34
|
+
run: |
|
|
35
|
+
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
|
|
36
|
+
echo "Tag $TAG_NAME already exists. Skipping."
|
|
37
|
+
else
|
|
38
|
+
git config user.name "github-actions[bot]"
|
|
39
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
40
|
+
git tag $TAG_NAME
|
|
41
|
+
git push origin $TAG_NAME
|
|
42
|
+
fi
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: Build & Release Tampermonkey Script
|
|
2
|
+
|
|
3
|
+
permissions:
|
|
4
|
+
contents: write
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
workflow_run:
|
|
8
|
+
workflows: ["Auto Tag from package.json"]
|
|
9
|
+
types:
|
|
10
|
+
- completed
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release:
|
|
14
|
+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout code
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
fetch-tags: true
|
|
23
|
+
|
|
24
|
+
- name: Set up Node.js
|
|
25
|
+
uses: actions/setup-node@v4
|
|
26
|
+
with:
|
|
27
|
+
check-latest: true
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: npm ci
|
|
31
|
+
|
|
32
|
+
- name: Build script
|
|
33
|
+
run: npm run build
|
|
34
|
+
|
|
35
|
+
- name: Get package name
|
|
36
|
+
id: pkg
|
|
37
|
+
run: |
|
|
38
|
+
NAME=$(node -p "require('./package.json').name")
|
|
39
|
+
echo "PACKAGE_NAME=$NAME" >> $GITHUB_ENV
|
|
40
|
+
|
|
41
|
+
- name: Get latest tag from Git
|
|
42
|
+
id: tag
|
|
43
|
+
run: |
|
|
44
|
+
git fetch --tags
|
|
45
|
+
TAG=$(git describe --tags --abbrev=0)
|
|
46
|
+
echo "TAG_NAME=$TAG" >> $GITHUB_ENV
|
|
47
|
+
echo "Latest tag: $TAG"
|
|
48
|
+
|
|
49
|
+
- name: Create GitHub Release
|
|
50
|
+
uses: softprops/action-gh-release@v2
|
|
51
|
+
with:
|
|
52
|
+
name: ${{ env.PACKAGE_NAME }} ${{ env.TAG_NAME }}
|
|
53
|
+
tag_name: ${{ env.TAG_NAME }}
|
|
54
|
+
files: dist/script.user.js
|
|
55
|
+
env:
|
|
56
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A reference to the global `window` object, extended with additional properties
|
|
3
|
+
* for React and ReactDOM.
|
|
4
|
+
*
|
|
5
|
+
* @type {Window & {React: typeof import('react'), ReactDOM: typeof import('react-dom/client')}}
|
|
6
|
+
* @property {typeof import('react')} React - The React library, providing tools
|
|
7
|
+
* for building user interfaces.
|
|
8
|
+
* @property {typeof import('react-dom/client')} ReactDOM - The ReactDOM library,
|
|
9
|
+
* allowing React components to be rendered into the DOM.
|
|
10
|
+
*/
|
|
11
|
+
declare const unsafeWindow: Window & {
|
|
12
|
+
React: typeof import('react')
|
|
13
|
+
ReactDOM: typeof import('react-dom/client')
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional utility file for easily accessing the React & ReactDOM objects at runtime.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type React from 'react'
|
|
6
|
+
import type ReactDOM from 'react-dom/client'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Retrieves the React object from the global `unsafeWindow` scope.
|
|
10
|
+
*
|
|
11
|
+
* @return {typeof React} The React object from the global scope.
|
|
12
|
+
*/
|
|
13
|
+
export function getReact(): typeof React {
|
|
14
|
+
return unsafeWindow.React
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Retrieves the ReactDOM object from the global `unsafeWindow` object.
|
|
19
|
+
*
|
|
20
|
+
* @return {typeof ReactDOM} The ReactDOM reference from the global scope.
|
|
21
|
+
*/
|
|
22
|
+
export function getReactDOM(): typeof ReactDOM {
|
|
23
|
+
return unsafeWindow.ReactDOM
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"jsx": "react-jsx",
|
|
4
|
+
"types": ["vite/client", "node"],
|
|
5
|
+
"target": "ES2022",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "nodenext",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"lib": ["ES2022", "dom", "dom.iterable"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["template/src", "react"]
|
|
16
|
+
}
|