vite-plugin-monkey
一个用于为用户脚本引擎(如Tampermonkey、Violentmonkey、Greasemonkey和ScriptCat)开发和构建你的user.js的vite插件。
特性
- 支持Tampermonkey、Violentmonkey、Greasemonkey、ScriptCat等
- 将用户脚本注释注入到构建包中
- 当用户脚本发生变化时自动在默认浏览器中打开*.user.js
- 将外部CDN URL注入到用户脚本的@require中
- 将外部模块注入到用户脚本的@resource中
- 通过ESM导入使用GM_api,并带有类型提示
- 智能收集使用的GM_api并自动配置用户脚本的@grant注释
- 在单个文件中支持"顶级await"和"动态导入"
- 在vite预览时,自动打开浏览器安装dist.user.js
- 完全支持TypeScript和vite特性
快速开始
就像使用vite创建一样
pnpm create monkey
# npm create monkey
# yarn create monkey
然后你可以选择以下模板
JavaScript | TypeScript |
---|---|
empty (仅js) | empty-ts (仅ts) |
vanilla (js + css) | vanilla-ts (ts + css) |
vue | vue-ts |
react | react-ts |
preact | preact-ts |
svelte | svelte-ts |
solid | solid-ts |
示例:初始化模板
示例:热模块替换
示例:构建和预览
安装
pnpm add -D vite-plugin-monkey
# npm i -D vite-plugin-monkey
# yarn add -D vite-plugin-monkey
注意:vite-plugin-monkey必须是插件列表中的最后一项
graph LR;
A(你的代码) -- "其他插件/vite构建" -->B(esm)
B -- "vite-plugin-monkey/vite构建库模式" --> C{有动态导入?}
C -- 是 --> D(systemjs)
C -- 否 --> E(iife)
配置
MonkeyOption类型
export type MonkeyOption = {
/**
* 用户脚本入口文件路径
*/
entry: string;
userscript?: MonkeyUserScript;
format?: Format;
/**
* vite-plugin-monkey/dist/client的别名
* @default '$'
* @example
* // vite-env.d.ts 用于类型提示
*
* // 如果你使用默认值 `$`
* /// <reference types="vite-plugin-monkey/client" />
*
* // 如果你使用其他别名
* declare module other_alias {
* export * from 'vite-plugin-monkey/dist/client';
* }
*/
clientAlias?: string;
server?: {
/**
* 当用户脚本注释改变时,自动在默认浏览器中打开安装URL
*
* 并设置 `viteConfig.server.open ??= monkeyConfig.server.open`
* @default
* process.platform == 'win32' || process.platform == 'darwin' // 如果平台是Win/Mac
*/
open?: boolean;
/**
* 名称前缀,用于在猴子扩展安装列表中区分server.user.js和build.user.js,如果你不想要前缀,设置为false
* @default 'server:'
*/
prefix?: string | ((name: string) => string) | false;
/**
* 将GM_api挂载到unsafeWindow,不推荐这样做,你应该通过ESM导入使用GM_api,或使用[unplugin-auto-import](https://github.com/antfu/unplugin-auto-import)
* @default false
* @example
* // 如果设置为true,你可以使用`vite-plugin-monkey/global`进行类型提示
* // vite-env.d.ts
* /// <reference types="vite-plugin-monkey/global" />
*/
mountGmApi?: boolean;
};
build?: {
/**
* 构建的用户脚本文件名
*
* 应以'.user.js'结尾
* @default (package.json.name??'monkey')+'.user.js'
*/
fileName?: string;
/**
* 构建的用户脚本注释文件名,此文件仅包含注释
*
* 可用于userscript.updateURL,检查更新时只需下载这个小文件,而不是下载整个脚本
*
* 应以'.meta.js'结尾,如果设置为false,则不生成此文件
*
* 如果设置为true,将等同于fileName.replace(/\\.user\\.js$/,'.meta.js')
*
* @default false
*/
metaFileName?: string | boolean | ((fileName: string) => string);
/**
- 此配置可以是数组或对象,数组=Object.entries(对象)
- 如果值是字符串或函数,它或其返回值就是exportVarName
- 如果值是数组,第一个[项或其返回值]是exportVarName,之后的项都是[require url]
- 如果模块未导入,插件不会将require url添加到userscript中
- @example
- { // 映射结构
- vue:'Vue',
- // 如果设置这个
- // 当
vite build
时,你需要手动设置userscript.require = ['https://unpkg.com/vue@3.0.0/dist/vue.global.js'] - vuex:['Vuex', (version, name)=>
https://unpkg.com/${name}@${version}/dist/vuex.global.js
], - // 插件会自动将此url添加到userscript.require中
- 'prettier/parser-babel': [
- 'prettierPlugins.babel',
- (version, name, importName) => {
-
// name == `prettier`
-
// importName == `prettier/parser-babel`
-
const subpath = `${importName.split('/').at(-1)}.js`;
-
return `https://cdn.jsdelivr.net/npm/${name}@${version}/${subpath}`;
- },
- ],
- // 有时importName与包名不同
- }
- @example
- [ // 数组结构,此示例来自 playground/ex-vue-demi
- [
-
'vue',
-
cdn
-
.jsdelivr('Vue', 'dist/vue.global.prod.js')
-
.concat('https://unpkg.com/vue-demi@latest/lib/index.iife.js')
-
.concat(
-
await util.fn2dataUrl(() => {
-
window.Vue = Vue;
-
}),
-
),
- ],
- ['pinia', cdn.jsdelivr('Pinia', 'dist/pinia.iife.prod.js')],
- [
-
'element-plus',
-
cdn.jsdelivr('ElementPlus', 'dist/index.full.min.js'),
- ],
- ] */ externalGlobals?: ExternalGlobals;
/**
- 根据最终代码包,自动注入GM_*或GM.*到userscript注释grant中
- 对代码进行树摇,然后如果code.includes('GM_xxx'),就添加@grant GM_xxx到userscript中
- @default true */ autoGrant?: boolean;
/**
- @deprecated 在vite>=4.2.0中使用viteConfig.build.cssMinify
- 现在minifyCss将不起作用 */ minifyCss?: boolean;
/**
- @example
- { // resourceName默认值是pkg.importName
- 'element-plus/dist/index.css': pkg=>
https://unpkg.com/${pkg.name}@${pkg.version}/${pkg.resolveName}
, - 'element-plus/dist/index.css': {
-
resourceName: pkg=>pkg.importName,
-
resourceUrl: pkg=>`https://unpkg.com/${pkg.name}@${pkg.version}/${pkg.resolveName}`,
-
loader: pkg=>{ // 有默认加载器支持[css, json, vite支持的资源, ?url, ?raw]文件/名称后缀
-
const css = GM_getResourceText(pkg.resourceName);
-
GM_addStyle(css);
-
return css;
-
},
-
nodeLoader: pkg=>{
-
return [
-
`export default (()=>{`,
-
`const css = GM_getResourceText(${JSON.stringify(pkg.resourceName)});`,
-
`GM_addStyle(css);`,
-
`return css;`,
-
`})();`
-
].join('');
-
},
- },
- 'element-plus/dist/index.css': [
-
(version, name, importName, resolveName)=>importName,
-
(version, name, importName, resolveName)=>`https://unpkg.com/${name}@${version}/${resolveName}`,
-
// 为了兼容externalGlobals cdn函数,如果(version/name/importName/resolveName) == '',插件将使用它们自己的默认值
- ],
- 'element-plus/dist/index.css': cdn.jsdelivr(),
- } */ externalResource?: ExternalResource;
/**
- 使用动态导入时,插件将使用systemjs构建你的代码
cdn.jsdelivr()[1]
示例 -> dynamic-import.user.js'inline'
示例 -> test-v3.user.js- @default
- cdn.jsdelivr()[1] */ systemjs?: 'inline' | ModuleToUrlFc;
/**
- @default
- const defaultFc = () => {
- return (e: string) => {
-
if (typeof GM_addStyle == 'function') {
-
GM_addStyle(e);
-
return;
-
}
-
const o = document.createElement('style');
-
o.textContent = e;
-
document.head.append(o);
- };
- };
- @example
- const defaultFc1 = () => {
- return (e: string) => {
-
const o = document.createElement('style');
-
o.textContent = e;
-
document.head.append(o);
- };
- };
- const defaultFc2 = (css:string)=>{
- const t = JSON.stringify(css)
- return
(e=>{const o=document.createElement("style");o.textContent=e,document.head.append(o)})(${t})
- } */ cssSideEffects?: ( css: string, ) => IPromise<string | ((css: string) => void)>; }; };
用于外部资源的CDN工具
import { defineConfig } from 'vite';
import monkey, { cdn } from 'vite-plugin-monkey';
export default defineConfig({
plugins: [
monkey({
build: {
externalGlobals: {
react: cdn.jsdelivr('React', 'umd/react.production.min.js'),
},
externalResource: {
'element-plus/dist/index.css': cdn.jsdelivr(),
},
},
}),
],
});
可以使用以下CDN,完整详情见cdn.ts
如果你想使用其他CDN,可以参考external-scripts
压缩
由于greasyfork的代码规则
发布到Greasy Fork的代码不得被混淆或压缩
因此插件会将viteConfig.build.minify的默认值更改为false
如果你想启用压缩,只需设置viteConfig.build.minify=true
GM_api使用
ESM使用
我们可以通过esm模块使用GM_api
// main.ts
import { GM_cookie, unsafeWindow, monkeyWindow, GM_addElement } from '$';
// $是vite-plugin-monkey/dist/client的默认别名
// 如果你想使用'others',设置monkeyConfig.clientAlias='others'
// 无论是serve还是build模式,monkeyWindow始终是[UserScript Scope]的window
console.log(monkeyWindow);
GM_addElement(document.body, 'div', { innerHTML: 'hello' });
// 无论是serve还是build模式,unsafeWindow始终是宿主window
if (unsafeWindow == window) {
console.log('scope->host, host esm scope');
} else {
console.log('scope->monkey, userscript scope');
}
GM_cookie.list({}, (cookies, error) => {
if (error) {
console.log(error);
} else {
const [cookie] = cookies;
if (cookie) {
console.log(cookie);
}
}
});
全局变量使用
设置 monkeyConfig.server.mountGmApi=true
// vite.config.ts
import { defineConfig } from 'vite';
import monkey from 'vite-plugin-monkey';
export default defineConfig({
plugins: [
monkey({
// ...
server: { mountGmApi: true },
}),
],
});
GM_api 将挂载到 host window/globalThis
的属性上
// main.ts
console.log(GM_cookie == globalThis.GM_cookie);
console.log({ GM_cookie, unsafeWindow, monkeyWindow, GM_addElement });
自动导入用法
// vite.config.ts
import { defineConfig } from 'vite';
import monkey, { util } from 'vite-plugin-monkey';
import AutoImport from 'unplugin-auto-import/vite';
export default defineConfig({
plugins: [
AutoImport({
imports: [util.unimportPreset],
}),
monkey({
// ...
}),
],
});
// main.ts
// 自动导入示例
console.log({ GM_cookie, unsafeWindow, monkeyWindow, GM_addElement });
示例
测试示例,请参见 /playground
以及 preact/react/svelte/vanilla/vue/solid 示例,请参见 create-monkey
一些注意事项
与其他插件一起使用
插件将通过 generateBundle 钩子重新构建你的代码
请确保插件的顺序是最后一个
CSP
在 vite serve
模式下,代码入口会作为脚本添加到目标主机的 document.head 中,代码需要在两个源之间工作
但是浏览器会根据 CSP 策略阻止这个脚本的执行
目前只能使用浏览器扩展 Disable-CSP
@require 中混合使用 IIFE 和 UMD
来自 iife-cdn 的通过 var
声明的变量在 monkeyWindow 作用域中不会成为 window 的属性,因为 monkeyWindow 作用域不是全局作用域
所以如果一个 umd 库依赖于一个 iife 库,比如 element-plus
依赖于 vue
,element-plus
cdn 将无法正常工作
详情请参见 issues/5 或 greasyfork#1084
解决方案是在 iife-cdn 之后附加一个数据 URL 脚本,该脚本将 iife-变量设置为 window 的属性
import { defineConfig } from 'vite';
import monkey, { cdn, util } from 'vite-plugin-monkey';
export default defineConfig(async ({ command, mode }) => ({
plugins: [
monkey({
// ...
build: {
externalGlobals: {
vue: cdn
.jsdelivr('Vue', 'dist/vue.global.prod.js')
.concat(util.dataUrl(';window.Vue=Vue;')),
'element-plus': cdn.jsdelivr('ElementPlus', 'dist/index.full.min.js'),
},
},
}),
],
}));
兼容性处理
当插件与 vite legacy 一起使用时,需要设置 renderLegacyChunks=false
// vite.config.ts
import legacy from '@vitejs/plugin-legacy';
import { defineConfig } from 'vite';
import monkey from 'vite-plugin-monkey';
export default defineConfig({
plugins: [
legacy({
renderLegacyChunks: false,
modernPolyfills: true,
}),
monkey({
entry: './src/main.ts',
}),
],
});
如何正确地使用 GM_api 构建库
如果你想封装 GM_api 来构建一个供他人使用的库
之前的做法通常是在库代码中直接将 GM_api 作为全局变量访问,然后在用户脚本中通过 @require
引用和加载。
然而,这种方法不允许我们通过 npm 或其他包管理器来管理这个依赖,也不兼容 vite-plugin-monkey 中 ESM GM_api 的使用方式。
现在,你只需要在库代码中正常从 vite-plugin-monkey/dist/client
导入 GM_api。修改你的构建配置,排除 vite-plugin-monkey/dist/client
。
这样,你就可以构建一个可以在 vite-plugin-monkey 中使用的库。这个库的使用者只需要通过 npm 安装它,然后正常使用 import
即可。
当然,如果你直接将 vite-plugin-monkey/dist/client
打包到构建产物中,那么这个库也可以直接通过 @require
引用。
但是为了让构建产物更加精简,建议你在构建时将 vite-plugin-monkey/dist/client
重定向到 vite-plugin-monkey/dist/native
。
下面是一个使用 tsup 同时打包 ESM 和 IIFE 格式的示例。ESM 提供给 vite-plugin-monkey 用户使用,IIFE 提供给想通过 @require
引用的用户使用。
另外,IIFE 格式还可以作为 vite-plugin-monkey 的 externalGlobals 的配置,以减小构建产物的大小。
// /src/index.ts
import { GM_setValue } from 'vite-plugin-monkey/dist/client';
export const setValue = (name: string, value: unknown) => {
console.log('你调用了 setValue', name, value);
GM_setValue(name, value);
};
// tsup.config.ts
import { defineConfig } from 'tsup';
const outExtension = (ctx: { format: 'esm' | 'cjs' | 'iife' }) => ({
js: { esm: '.mjs', cjs: '.cjs', iife: '.iife.js' }[ctx.format],
});
export default defineConfig([
{
// 用于 vite 导入
entry: ['src/index.ts'],
outDir: 'dist',
sourcemap: true,
platform: 'browser',
outExtension,
dts: true,
format: ['esm'],
external: ['vite-plugin-monkey/dist/client'],
},
{
// 用于用户脚本 @require
entry: ['src/index.ts'],
outDir: 'dist',
sourcemap: true,
platform: 'browser',
outExtension,
dts: false,
format: ['iife'],
minify: true,
globalName: `GmExtra`,
target: 'es2015',
esbuildOptions: (options) => {
options.alias = {
'vite-plugin-monkey/dist/client': 'vite-plugin-monkey/dist/native',
};
},
},
]);
贡献
请将你的更改提交到 dev 分支