A userscript development framework for Tampermonkey, Violentmonkey, and ScriptCat
Makoo is a userscript development framework for building maintainable Vue / React injection apps for browser script managers such as Tampermonkey, Violentmonkey, and ScriptCat.
It focuses on the parts of userscript development that tend to get messy: waiting for target DOM nodes, mounting components, handling page redraws, managing injection modules, and keeping structural changes hot-updated during development. Build output, userscript metadata, and install flows are still handled by lisonge/vite-plugin-monkey; Makoo adds a structured framework layer for component-driven userscript projects.
Makoo is not meant for simple userscripts. If your script only changes a button, hides an element, or injects a small style block, plain userscript code is often enough.
Makoo is a better fit for userscripts that start behaving like small frontend applications:
- building injected UI with Vue or React to modify existing web pages
- multiple injection points or feature modules on the same page
- host pages that redraw or partially refresh, requiring stable remount behavior
- modules that need to be enabled by URL or page state
- growing codebases that need clear structure, configuration, and development workflow
Makoo becomes useful when lifecycle, module boundaries, and long-term maintenance start to matter.
- When To Use Makoo
- Quick Start
- Core Concepts
- Project Structure
- Configuration Overview
- Manifest Reference
- HMR Behavior
- Recipes
- Packages
- Special Thanks
- Development
- License
Create a project with the scaffold:
pnpm dlx @makoojs/create-makooThen enter the project and start the dev server:
pnpm install
pnpm devA minimal project usually looks like this:
.
├─ vite.config.ts
└─ injections
├─ manifest.ts
└─ hello-world
└─ app.vueInjector is Makoo's runtime scheduler. It registers injection tasks, waits for target nodes, asks the matching adapter to mount components, and handles reinjection when needed.
Injection Module is a single injection unit. A module usually maps to a component under injections/<module-name>/, and it may also provide its own module-level manifest.ts.
Manifest is the declarative injection configuration. The top-level injections/manifest.ts describes which modules should be injected; module-level files such as injections/foo/manifest.ts can override a single module.
Adapter is the component mounting bridge. Makoo supports Vue and React through @makoojs/vue and @makoojs/react, and the adapter model can support other mountable artifacts later.
The recommended structure keeps all injection modules under injections/:
injections
├─ manifest.ts
├─ profile-card
│ ├─ app.vue
│ └─ manifest.ts
└─ react-badge
├─ app.tsx
└─ manifest.tsUse the top-level manifest.ts as the project entry configuration. Use module-level manifest.ts files when a module should own fields such as injectAt, framework, match, or hooks.
Makoo's Vite plugin config has four main areas:
makoo({
app: {
name: 'my-script',
version: '0.0.1'
},
source: {
include: ['*'],
exclude: []
},
monkey: {
userscript: {
match: ['https://example.com/*']
}
}
});app is used to generate userscript metadata such as name, version, and description.
source controls where Makoo scans injection modules. Its current include and exclude fields filter module directories, not page URLs.
Most monkey options are passed through to lisonge/vite-plugin-monkey for userscript metadata, dev server behavior, and build behavior. Makoo manages clientAlias and server.mountGmApi internally, so those options are not user-configurable.
The top-level manifest supports both object and array forms.
injectionDefaults defines shared injection runtime defaults for the current manifest. Modules inherit alive, scope, timeout, and hooks from it unless they override those fields themselves.
Object form is recommended for most projects:
import { defineInjections } from '@makoojs/cli';
export default defineInjections({
injectionDefaults: {
alive: false,
scope: 'local'
},
injections: {
header: {
injectAt: '#header',
component: './header/app.vue',
framework: 'Vue'
},
badge: {
injectAt: 'body',
component: './badge/app.tsx',
framework: 'React',
match: {
include: ['https://example.com/profile/*'],
exclude: ['https://example.com/profile/settings']
}
}
}
});Array form is useful when entries are generated or need an explicit name:
import { defineInjections } from '@makoojs/cli';
export default defineInjections({
injections: [
{
name: 'header',
injectAt: '#header',
component: './header/app.vue',
framework: 'Vue'
}
]
});Common module fields:
| Field | Description |
|---|---|
injectAt |
Target selector for injection |
component |
Component path relative to injections/manifest.ts or the module directory |
framework |
Vue, React, or auto; when omitted, Makoo infers it from the component extension |
enabled |
Whether the module is enabled, defaults to true |
alive |
Whether to retry injection after target DOM changes |
scope |
Reinjection observation scope, supports local and global |
timeout |
Timeout for waiting for the target node |
hooks |
Lifecycle hooks for the current module |
match |
URL matching rule for the current module |
Module-level URL match supports shorthand and object forms:
match: ['https://example.com/*']match: {
include: ['https://example.com/*'],
exclude: ['https://example.com/admin/*']
}When match is omitted, the module is registered on pages where the userscript itself runs. When match is provided, Makoo checks location.href at runtime before registering that module.
Makoo separates structural changes from regular component updates in dev mode.
| Change | Behavior |
|---|---|
Top-level injections/manifest.ts changes |
Rescan and update the virtual entry |
Module-level injections/foo/manifest.ts changes |
Rescan and update the virtual entry |
| Local helper or hooks imported by a manifest changes | Recursively track the local dependency and rescan |
Module-level manifest.ts is added or removed |
Trigger a structural update |
| Regular component file changes | Let Vite handle native HMR |
| Third-party package dependency changes | Not tracked by Makoo structural scanning |
When splitting hooks into a separate file, prefer static relative imports:
import { hooks } from './hooks';Makoo tracks local chains such as manifest -> hooks -> helper. Dynamic import(), path aliases, and third-party packages are not part of Makoo's structural dependency tracking.
import { defineInjections } from '@makoojs/cli';
export default defineInjections({
injections: {
profile: {
injectAt: '#app',
component: './profile/app.vue',
match: {
include: ['https://example.com/users/*'],
exclude: ['https://example.com/users/settings']
}
}
}
});import { defineInjections } from '@makoojs/cli';
export default defineInjections({
injections: {
panel: {
injectAt: 'body',
component: './panel/app.vue',
framework: 'Vue'
}
}
});// injections/hooks.ts
export const hooks = {
'run:start': () => {
console.log('[makoo] injector started');
}
};// injections/manifest.ts
import { hooks } from './hooks';
import { defineInjections } from '@makoojs/cli';
export default defineInjections({
injectionDefaults: {
hooks
},
injections: {
'hello-world': {
injectAt: 'body',
component: './hello-world/app.vue'
}
}
});monkey.build.externalGlobals and externalResource are passed through to lisonge/vite-plugin-monkey:
import { defineConfig } from 'vite';
import { cdn, makoo } from '@makoojs/cli';
export default defineConfig({
plugins: makoo({
app: {
name: 'my-script',
version: '0.0.1'
},
monkey: {
build: {
externalGlobals: {
vue: cdn.jsdelivr('Vue', 'dist/vue.global.prod.js')
}
}
}
})
});Makoo exposes @makoojs/cli/monkey as a stable entry for lisonge/vite-plugin-monkey GM APIs. Prefer capability-level imports so the final userscript only references the GM APIs it actually uses:
import { gmRequest, gmStorage, gmStyle } from '@makoojs/cli/monkey';
gmStyle.add('.makoo-panel { z-index: 999999; }');
gmStorage.set('token', 'abc');
const token = gmStorage.get<string>('token');
gmRequest.get('https://api.example.com/data', {
responseType: 'json',
onload(event) {
console.log(event.response);
}
});You can also use the grouped entry. Prefer capability-level imports when you want the smallest generated @grant surface; GMapi is a convenience entry for shared or exploratory code:
import { GMapi } from '@makoojs/cli/monkey';
GMapi.storage.set('enabled', true);When monkey.build.autoGrant is enabled, which is the default, @grant is still generated by lisonge/vite-plugin-monkey from the final code. Development does not require manually mounting global GM_* APIs.
| Package | Responsibility |
|---|---|
@makoojs/core |
Framework-agnostic injection runtime |
@makoojs/vue |
Vue mount adapter |
@makoojs/react |
React mount adapter |
@makoojs/cli |
Vite plugin, config resolution, scanning, and code generation |
@makoojs/create-makoo |
Project scaffold |
Most userscript projects should start with @makoojs/cli. You usually only need to touch @makoojs/core, @makoojs/vue, or @makoojs/react for custom runtime integrations.
Makoo is built on top of these excellent open-source projects:
| Project | What it provides |
|---|---|
| Vite | Modern frontend development and build tooling |
| lisonge/vite-plugin-monkey | Userscript build, metadata generation, and dev workflow |
| Vue | Vue component ecosystem and runtime |
| React | React component ecosystem and runtime |
| Vitest | Test runner |
| jiti | TypeScript manifest loading |
| picomatch | Module directory matching |
pnpm install
pnpm build
pnpm testCommon commands:
| Command | Description |
|---|---|
pnpm build |
Build all packages |
pnpm test |
Run tests |
pnpm docs:dev |
Start the documentation site |
pnpm docs:build |
Build the documentation site |
pnpm lint:fix |
Run Biome checks and fixes |