首页
/ Penpot 插件开发实战:基于 penpot-plugins 仓库从脚手架搭建到加载运行的完整指南

Penpot 插件开发实战:基于 penpot-plugins 仓库从脚手架搭建到加载运行的完整指南

2026-09-07 10:02:47作者:宣利权Counsellor

导读

本文围绕 Penpot 官方插件仓库 penpot-plugins 中的《Creating a Plugin》文档,完整讲解如何在该 monorepo 内从零创建一个 Penpot 设计插件:涵盖目录与脚手架初始化、package.json / ESLint / manifest.json 清单配置、Vite 与 TypeScript 配置、本地预览服务器启动,以及通过 Penpot 插件管理器加载插件的完整闭环。读完本文你将能够参照仓库内真实示例 create-palette-plugin,独立搭建、编写并运行一个属于自己的 Penpot 插件,并理解插件清单(manifest)各字段与权限声明在运行时是如何被校验和执行的。

前置知识:penpot-plugins 仓库结构

创建插件之前,先了解当前仓库的两个关键目录(见 plugins/README.md):

  • libs/:插件运行库,其中 plugins-runtime 负责初始化插件环境、注入全局 penpot API,并监听 Penpot 页面/文件/选区变化;plugins-styles 提供 Penpot 风格的基础 CSS 库,可用来快速美化插件界面。
  • apps/:存放各类插件应用示例,例如 contrast-plugin(色彩对比度检查)、create-palette-plugin(从本地库颜色生成色板 Board)、icons-plugintable-pluginrename-layers-plugin 等,是学习和拷贝脚手架的最佳参照。

本指南定位是创建位于 apps/ 目录内部的插件(即“在 monorepo 内开发”),因此示例中的插件路径统一写作 apps/example-plugin。仓库内与之最贴近的真实模板是 apps/create-palette-plugin,下文每个步骤都会对照该示例给出可验证的源码依据。

如果要在 Penpot 仓库环境之外创建独立插件,官方另有插件起始模板与开发者文档体系,与本仓库同源的指南还包括 create-angular-plugin.mdapi-docs.mdtest-e2e.md,可互为参考。

Step 1:初始化插件脚手架

先在 monorepo 的 apps/ 下为插件创建两个基础目录:src(存放 TypeScript 源码)与 public(存放清单与静态资源):

mkdir -p apps/example-plugin/src apps/example-plugin/public

接着在 apps/example-plugin 下创建最基础的 package.json。它声明了插件自身的开发/构建/预览/校验命令:

{
  "name": "example-plugin",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint src --ext .ts"
  }
}

对照仓库中真实的 apps/create-palette-plugin/package.json 可以看到更完整的脚本集:除 dev/build/preview 外还有 watchvite build --watch --mode development,开发期增量构建)、servevite preview)、init(用 concurrently 并行跑 watch 与 serve)、linttest(vitest)。真实示例还在构建时加了 --emptyOutDir,避免残留旧产物。由于这是 pnpm workspace monorepo,所有子包都通过 private: true 防止被误发布。

创建好目录与 package.json 后,参考 create-palette-plugin 的 vite.config.ts 创建你自己的 vite.config.ts,随后再补充 TypeScript 相关配置文件。若仓库尚未安装过依赖,先在 plugins 根目录执行一次 pnpm -r install(见 plugins/README.md)。

Step 2:配置 ESLint

Penpot 插件代码规范沿用仓库统一的 TypeScript ESLint 配置。在插件根目录创建 eslint.config.js

import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';

export default tseslint.config(
  ...tseslint.configs.recommended,
  eslintConfigPrettier,
  {
    languageOptions: {
      parserOptions: {
        project: './tsconfig.*?.json',
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
);

要点说明:

  • tseslint.configs.recommended 提供推荐的 TypeScript 规则集;
  • eslintConfigPrettier 用于关闭与 Prettier 冲突的格式类规则,保证 lint 与格式化互不打架;
  • parserOptions.project 使用 ./tsconfig.*?.json 通配,使类型感知规则(type-aware rules)能同时覆盖 tsconfig.app.jsontsconfig.spec.json 等多个配置文件。

仓库根目录还维护了统一的 plugins/eslint.config.js,各 app 的 lint 脚本最终都会被 pnpm workspace 串联执行,所以你的 eslint.config.js 只需覆盖本插件自身的文件范围即可。

Step 3:配置 Manifest 清单(最关键一步)

public/ 目录中创建 manifest.json。它是插件能被 Penpot 识别的“身份证”,决定了插件名称、宿主地址、入口脚本以及权限范围。文档给出的基础示例为:

{
  "name": "Example Plugin",
  "host": "http://localhost:4201",
  "code": "/plugin.js",
  "icon": "/icon.png",
  "permissions": [
    "content:write",
    "library:write",
    "user:read",
    "comment:read",
    "allow:downloads"
  ]
}

各字段含义与取值建议:

字段 作用 说明
name 插件在 Penpot 中展示的名称 建议与 package.json 语义一致但可更具可读性
host 插件开发服务器的地址 指向 manifest 所服务的域名/端口,便于本地联调
code 入口脚本的相对路径 相对 manifest 所在位置解析;路径可以是 /plugin.js 这类根相对写法,也可以是 plugin.js 这类相对写法
icon 插件图标路径 用于插件管理器与入口列表中展示
permissions 权限声明数组 声明插件运行时允许调用的 API 域,见下方展开

参考真实示例 apps/create-palette-plugin/public/manifest.json,仓库实际使用的 manifest 还包含 version(此处为 2)与 description 字段,且 code 直接写为相对形式 "plugin.js"、图标放在 assets/icon.png

{
  "name": "Create Palette from library",
  "description": "Create a board with all the colors in the local library",
  "code": "plugin.js",
  "version": 2,
  "icon": "assets/icon.png",
  "permissions": ["content:read", "content:write", "library:read"]
}

权限(permissions)是如何被执行的

权限声明不是摆设。查看插件运行时源码 libs/plugins-runtime/src/lib/api/index.ts,可以看到 API 层通过 checkPermission 逐一校验当前插件 manifest 中是否包含对应权限字符串,未声明权限的调用会被拦截。也就是说:manifest 中少声明一个权限,插件代码里对应域的 API 调用就会直接失效,这是排查“API 不生效”类问题时最优先检查的点。

仓库中实际出现的权限值主要分为两类域:

  • 数据操作域:content:read / content:write(读写画布内容)、library:read / library:write(读写本地资源库)、user:read(读取用户信息)、comment:read(读取评论);
  • 能力放行域:allow:downloads 等,用于放开浏览器层面对剪贴板/下载等能力的限制(运行时在插件弹窗上按权限决定是否设置对应 iframe 能力属性,见 plugin-modal 相关测试)。

所以最稳妥的做法是“按需最小授权”:只声明插件实际调用的 API 所对应的权限,既能保证功能正常,也避免过度索权。运行时 manifest 的数据结构与权限类型的字面量定义可进一步查看 models/manifest.model.ts

Step 4:配置 Vite 多入口构建

vite.config.ts 中加入 build.rollupOptions.input,同时把插件入口脚本与 UI 页面声明为两个构建入口:

build: {
  rollupOptions: {
    input: {
      plugin: 'src/plugin.ts',
      index: './index.html',
    },
    output: {
      entryFileNames: '[name].js',
    },
  },
}

对照真实模板 create-palette-plugin/vite.config.ts 可以看到完整形态:插件 root 设为自身目录,开发/预览服务器统一监听 4202 端口并开放到 0.0.0.0;产物输出到 ../../dist/apps/create-palette-plugin(monorepo 集中式 dist);output.entryFileNames: '[name].js' 保证入口文件名与 manifest 中 code 声明的 plugin.js 保持一致。这里的关键约束是:构建产出的入口文件名必须与 manifest 里的 code 严格对应,否则 Penpot 加载时无法找到插件脚本。

Step 5:调整 TypeScript 配置

插件的 tsconfig.app.json 必须把插件类型声明文件包含进来,才能让 penpot 全局对象获得完整的类型提示与编译期校验:

{
  "include": ["src/**/*.ts", "../../libs/plugin-types/index.d.ts"]
}

其中 ../../libs/plugin-types/index.d.ts 正是 monorepo 中共享的插件 API 类型定义。真实示例 create-palette-plugin/tsconfig.app.json 还额外继承了同目录 tsconfig.json、把产物输出到 ../../dist/out-tsc、并在 types 中加入 node,同时用 exclude 排除 *.spec.ts / *.test.ts 测试文件——这些细节能保证类型检查、构建与 vitest 各司其职。建议照抄该结构,只替换 name 与路径。

Step 6:启动静态服务器预览插件

在 monorepo 中,所有命令都通过 pnpm workspace 的 --filter 语法按包名执行。开发模式启动热更新服务器:

pnpm --filter example-plugin dev

产物预览(先构建、再启动静态服务)则使用:

pnpm --filter example-plugin build && pnpm --filter example-plugin preview

example-pluginpackage.json 中的包名;如果你的包名不同,请把 --filter 后面的名字替换成对应值。仓库根 README 中预置了示例插件的快捷命令,例如 pnpm run start:plugin:contrast,对应插件会运行在 http://localhost:4302create-palette-plugin 这类较新示例则直接以自身 vite.config.ts 的端口(如 4202)提供服务,其 manifest 可直接通过 http://localhost:4202/manifest.json(或 assets/ 子目录路径)访问。构建类命令会先在 plugins 根执行 pnpm -r install 完成依赖安装。

启动后,请先用浏览器确认以下两点再进入下一步:

  1. manifest 是否可访问:访问 http://localhost:<port>/manifest.json 能看到合法的 JSON;
  2. 入口脚本是否可访问:访问 manifest 中 code 对应的 URL 能返回编译后的 JS。

Step 7:在 Penpot 中加载插件

插件开发服务器就绪后,进入正在运行的 Penpot 界面。最快捷的方式是直接按快捷键 Ctrl + Alt + P 唤起插件管理器弹窗(Plugin manager)。

在弹窗中输入插件 manifest 的完整 URL(例如 http://localhost:4202/manifest.json),确认后 Penpot 会拉取清单、校验权限字段并安装插件;若一切正常,插件即出现在列表中,此后随时可以再次打开使用。同样可以避免使用快捷键,通过 Penpot 顶部 Menu(菜单) 进入插件管理器:

Penpot 菜单中打开插件管理器的入口

需要说明的是,加载前请确保插件服务允许跨域访问,且 Penpot 本体处于本地开发/测试环境(仓库 README 中运行示例插件前要求先按官方 devenv 文档启动本地 Penpot)。

深入:manifest 之后的插件代码长什么样

清单和构建配置只是“外壳”,插件的真正逻辑在 src/plugin.ts 中,运行时会向插件注入全局 penpot API。以 create-palette-plugin/src/plugin.ts 为例,其主体逻辑清晰展示了几类高频 API 的典型用法:

  • penpot.library.local.colors:读取当前文件本地资源库中的所有颜色;
  • penpot.createBoard() / board.addGridLayout() / board.appendChild(...):在画布上创建画板、栅格布局并挂载子对象;
  • board.fills / board.strokes / text.fontWeight 等属性:直接对图层进行样式与排版操作;
  • penpot.viewport.center:获取视口中心,用于将新画板定位到用户视野中央;
  • penpot.closePlugin():逻辑执行完毕后关闭插件弹窗。

这也解释了 manifest 权限与代码的对应关系:该插件声明了 content:readcontent:writelibrary:read,正对应它读取本地库颜色并写入画板内容的全部行为——少声明任何一个,对应的 API 调用都会被运行时权限检查拦截。

后续进阶路线

本文完成了“脚手架 → 清单 → 构建 → 加载”的最小闭环。继续深入的方向按仓库文档索引依次为:

把这些指南与本仓库中 apps/ 下的十余个真实插件配合阅读——先跑通一个示例、再动手改一个字段观察行为变化,是理解 Penpot 插件体系最高效的路径。

登录后查看全文
热门项目推荐
相关项目推荐