Project Icon

vite-plugin-monkey

Vite插件助力用户脚本开发 支持多引擎及热更新

vite-plugin-monkey是一款为Tampermonkey、Violentmonkey等用户脚本引擎提供开发支持的Vite插件。该插件实现了自动注入用户脚本注释、热模块替换、外部CDN资源注入和GM API的ESM导入等功能。它能够智能收集使用的GM API并自动配置@grant注释,同时支持顶级await和动态导入。通过提供完整的TypeScript支持和Vite特性,vite-plugin-monkey简化了用户脚本的开发流程。

vite-plugin-monkey

npm包 节点兼容性

英文文档 | 中文文档

一个用于为用户脚本引擎(如TampermonkeyViolentmonkeyGreasemonkeyScriptCat)开发和构建你的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

然后你可以选择以下模板

示例:初始化模板

vue-ts

示例:热模块替换

hmr

示例:构建和预览

build&preview

安装

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

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;

/**

/**

  • @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 });

自动导入用法

使用 unplugin-auto-import

// 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 依赖于 vueelement-plus cdn 将无法正常工作

详情请参见 issues/5greasyfork#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 分支

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

稿定AI

稿定设计 是一个多功能的在线设计和创意平台,提供广泛的设计工具和资源,以满足不同用户的需求。从专业的图形设计师到普通用户,无论是进行图片处理、智能抠图、H5页面制作还是视频剪辑,稿定设计都能提供简单、高效的解决方案。该平台以其用户友好的界面和强大的功能集合,帮助用户轻松实现创意设计。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号