Penpot 插件开发实战:基于 penpot-plugins 仓库从脚手架搭建到加载运行的完整指南
导读
本文围绕 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负责初始化插件环境、注入全局penpotAPI,并监听 Penpot 页面/文件/选区变化;plugins-styles提供 Penpot 风格的基础 CSS 库,可用来快速美化插件界面。apps/:存放各类插件应用示例,例如contrast-plugin(色彩对比度检查)、create-palette-plugin(从本地库颜色生成色板 Board)、icons-plugin、table-plugin、rename-layers-plugin等,是学习和拷贝脚手架的最佳参照。
本指南定位是创建位于 apps/ 目录内部的插件(即“在 monorepo 内开发”),因此示例中的插件路径统一写作 apps/example-plugin。仓库内与之最贴近的真实模板是 apps/create-palette-plugin,下文每个步骤都会对照该示例给出可验证的源码依据。
如果要在 Penpot 仓库环境之外创建独立插件,官方另有插件起始模板与开发者文档体系,与本仓库同源的指南还包括 create-angular-plugin.md、api-docs.md 与 test-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 外还有 watch(vite build --watch --mode development,开发期增量构建)、serve(vite preview)、init(用 concurrently 并行跑 watch 与 serve)、lint 与 test(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.json、tsconfig.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-plugin 为 package.json 中的包名;如果你的包名不同,请把 --filter 后面的名字替换成对应值。仓库根 README 中预置了示例插件的快捷命令,例如 pnpm run start:plugin:contrast,对应插件会运行在 http://localhost:4302;create-palette-plugin 这类较新示例则直接以自身 vite.config.ts 的端口(如 4202)提供服务,其 manifest 可直接通过 http://localhost:4202/manifest.json(或 assets/ 子目录路径)访问。构建类命令会先在 plugins 根执行 pnpm -r install 完成依赖安装。
启动后,请先用浏览器确认以下两点再进入下一步:
- manifest 是否可访问:访问
http://localhost:<port>/manifest.json能看到合法的 JSON; - 入口脚本是否可访问:访问 manifest 中
code对应的 URL 能返回编译后的 JS。
Step 7:在 Penpot 中加载插件
插件开发服务器就绪后,进入正在运行的 Penpot 界面。最快捷的方式是直接按快捷键 Ctrl + Alt + P 唤起插件管理器弹窗(Plugin manager)。
在弹窗中输入插件 manifest 的完整 URL(例如 http://localhost:4202/manifest.json),确认后 Penpot 会拉取清单、校验权限字段并安装插件;若一切正常,插件即出现在列表中,此后随时可以再次打开使用。同样可以避免使用快捷键,通过 Penpot 顶部 Menu(菜单) 进入插件管理器:
需要说明的是,加载前请确保插件服务允许跨域访问,且 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:read、content:write 与 library:read,正对应它读取本地库颜色并写入画板内容的全部行为——少声明任何一个,对应的 API 调用都会被运行时权限检查拦截。
后续进阶路线
本文完成了“脚手架 → 清单 → 构建 → 加载”的最小闭环。继续深入的方向按仓库文档索引依次为:
- 使用 Angular 构建插件:参见 create-angular-plugin.md,适合技术栈偏好 Angular 的团队;
- 为插件新增/修改官方 API:参见 create-api.md,在 libs/plugins-runtime/src/lib/api/index.ts 中用
zod校验输入输出并同步更新类型定义; - 插件发布与分发:参见 publish-package.md;
- 端到端测试:参见 test-e2e.md;
- 完整的 API 参考:参见 api-docs.md。
把这些指南与本仓库中 apps/ 下的十余个真实插件配合阅读——先跑通一个示例、再动手改一个字段观察行为变化,是理解 Penpot 插件体系最高效的路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
