Files
superwork/vscode/src/figma/importPlugin.ts
T
cruldra 2b02da27e7 🐛 fix(vscode): 导入原型插件时总是重启 Figma
插件已经在 Figma 清单里时原先只是把 Figma 拉到前台,而 Figma 把插件代码
缓存在进程里,改完原型点按钮看到的还是上一版 code.js。现在不管清单里有没有,
只要 Figma 在跑就先关掉再拉起,让它重新读盘。

Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
2026-09-15 01:03:29 +08:00

207 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 「在 Figma 中导入并打开」:把工单挂着的 Figma 插件 manifest.json 写进
* Figma 桌面版的已导入插件清单,然后拉起 Figma。
*
* 为什么要自己改配置文件:Figma 没有命令行导入开发插件的入口,UI 上得
* 「Plugins → Development → Import plugin from manifest」一路点。清单本身
* 就是 `~/Library/Application Support/Figma/settings.json` 里的一个数组,
* 直接往里加一条即可。
*
* 最大的坑:**Figma 退出时会用内存里的状态整个覆盖 settings.json**。开着
* Figma 写进去,用户一关 Figma 就全没了。所以写之前必须先把 Figma 关掉,
* 而且要等进程真的退出——不弹窗问用户,问了也只能答「关」。
*
* 只支持 macOS:路径和 `osascript` / `open -a` 都是 mac 专有。
*/
import { execFile } from 'node:child_process'
import * as fs from 'node:fs'
import { promises as fsp } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import process from 'node:process'
import { env, window } from 'vscode'
import { logger } from '../logging/logger'
import { appendManifestEntry, asExtensionEntries } from './localExtensions'
/** Figma 桌面版的设置文件(含已导入开发插件清单)。 */
function figmaSettingsPath(): string {
return path.join(os.homedir(), 'Library', 'Application Support', 'Figma', 'settings.json')
}
/** 等 Figma 退出的上限;到点还没退就让用户自己关,绝不 kill -9。 */
const QUIT_TIMEOUT_MS = 10_000
const QUIT_POLL_MS = 400
function run(cmd: string, args: string[]): Promise<{ code: number, stdout: string, stderr: string }> {
return new Promise((resolve) => {
execFile(cmd, args, { timeout: 15_000 }, (err, stdout, stderr) => {
// pgrep 没匹配到进程也是非零退出,所以这里不把非零当失败,交给调用方判。
const code = err ? Number((err as NodeJS.ErrnoException).code ?? 1) : 0
resolve({
code: Number.isFinite(code) ? code : 1,
stdout: (stdout ?? '').toString(),
stderr: (stderr ?? '').toString(),
})
})
})
}
async function isFigmaRunning(): Promise<boolean> {
const { stdout } = await run('pgrep', ['-x', 'Figma'])
return stdout.trim().length > 0
}
/**
* 优雅退出 Figma 并轮询等它真的没了。超时返回 false(调用方报错让用户手动关),
* 不强杀——Figma 被 SIGKILL 会丢用户没保存的画布。
*/
async function quitFigmaAndWait(): Promise<boolean> {
await run('osascript', ['-e', 'quit app "Figma"'])
const deadline = Date.now() + QUIT_TIMEOUT_MS
while (Date.now() < deadline) {
if (!await isFigmaRunning())
return true
await new Promise(resolve => setTimeout(resolve, QUIT_POLL_MS))
}
return !await isFigmaRunning()
}
/** 读 manifest.json 的 `name` 当 Figma 里显示的插件名;没有就用目录名兜底。 */
async function readPluginName(manifestAbs: string): Promise<string> {
const raw = await fsp.readFile(manifestAbs, 'utf8')
const parsed: unknown = JSON.parse(raw)
const name = parsed && typeof parsed === 'object'
? (parsed as { name?: unknown }).name
: undefined
if (typeof name === 'string' && name.trim().length > 0)
return name.trim()
return path.basename(path.dirname(manifestAbs))
}
function fail(message: string, err?: unknown): void {
const details = err === undefined
? undefined
: err instanceof Error
? (err.stack ?? err.message)
: String(err)
logger.add({ level: 'error', source: 'figma', message, details })
void window.showErrorMessage(message)
}
/**
* 主流程。`manifestPath` 可以是仓库相对路径也可以是绝对路径。
* 任何一步失败都出错误提示 + 记日志,不静默。
*/
export async function importPluginIntoFigma(opts: {
issueNumber: number
manifestPath: string
workspaceRoot: string
}): Promise<void> {
const { issueNumber, manifestPath, workspaceRoot } = opts
const manifestAbs = path.isAbsolute(manifestPath)
? manifestPath
: path.join(workspaceRoot, manifestPath)
if (!fs.existsSync(manifestAbs)) {
fail(`#${issueNumber} 的原型插件不存在:${manifestAbs}`)
return
}
// 非 mac 没有这条路径可走,把绝对路径给用户,让他自己在 Figma 里导入。
if (process.platform !== 'darwin') {
await env.clipboard.writeText(manifestAbs)
void window.showWarningMessage(
`当前系统不支持自动导入 Figma 插件,manifest 路径已复制到剪贴板,请在 Figma 里手动导入:${manifestAbs}`,
)
return
}
const settingsPath = figmaSettingsPath()
if (!fs.existsSync(settingsPath)) {
fail(`没找到 Figma 配置文件:${settingsPath}。先装 Figma 桌面版并至少启动一次。`)
return
}
let pluginName: string
try {
pluginName = await readPluginName(manifestAbs)
}
catch (err) {
fail(`读取 manifest.json 失败:${manifestAbs}`, err)
return
}
let settings: Record<string, unknown>
try {
const parsed: unknown = JSON.parse(await fsp.readFile(settingsPath, 'utf8'))
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
throw new TypeError('settings.json 顶层不是对象')
settings = parsed as Record<string, unknown>
}
catch (err) {
fail(`读取 Figma 配置失败:${settingsPath}`, err)
return
}
const entries = asExtensionEntries(settings.localFileExtensions)
const result = appendManifestEntry(entries, { manifestPath: manifestAbs, name: pluginName })
// Figma 在跑就一律先关掉,两个原因都成立:
// 写清单时——退出时的内存快照会把这次写入盖掉;
// 清单里已经有了——Figma 把插件代码缓存在进程里,不重启就还跑上一版
// code.js,改完原型点这个按钮会看到旧界面。
if (await isFigmaRunning()) {
logger.add({
level: 'info',
source: 'figma',
message: result.added
? '写配置前先退出 Figma(退出会覆盖 settings.json)'
: '插件已在清单中,重启 Figma 让它重新读盘上最新的插件代码',
})
if (!await quitFigmaAndWait()) {
fail(`Figma 在 ${QUIT_TIMEOUT_MS / 1000} 秒内没有退出,请手动关掉 Figma 后重试。`)
return
}
}
if (result.added) {
try {
const backup = `${settingsPath}.bak-${Date.now()}`
await fsp.copyFile(settingsPath, backup)
settings.localFileExtensions = result.entries
await fsp.writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8')
logger.add({
level: 'info',
source: 'figma',
message: `#${issueNumber} 原型插件已写入 Figma 配置:${pluginName}`,
details: `manifest=${manifestAbs} backup=${backup}`,
})
}
catch (err) {
fail(`写入 Figma 配置失败:${settingsPath}`, err)
return
}
}
else {
logger.add({
level: 'info',
source: 'figma',
message: `#${issueNumber} 原型插件已在 Figma 清单中,跳过写入:${result.name}`,
details: `manifest=${manifestAbs}`,
})
}
const open = await run('open', ['-a', 'Figma'])
if (open.code !== 0) {
fail(`拉起 Figma 失败:${open.stderr.trim() || `exit ${open.code}`}`)
return
}
void window.showInformationMessage(
result.added
? `原型插件「${result.name}」已导入 Figma,在 Plugins > Development 里跑它`
: `Figma 已重启,原型插件「${result.name}」是最新代码,在 Plugins > Development 里跑它`,
)
}