/** * Figma 桌面版「已导入的开发插件」清单的纯逻辑:算新 id、查重、造条目。 * * 清单落在 `~/Library/Application Support/Figma/settings.json` 的 * `localFileExtensions` 数组里,一条 manifest 条目长这样: * * ```json * { "id": 9, "manifestPath": "/abs/.../manifest.json", * "lastKnownName": "Xxx Design Lab", "lastKnownPluginId": "", * "fileMetadata": { "type": "manifest", "codeFileId": 10, "uiFileIds": [] } } * ``` * * 坑:id 是**整个数组共享**的递增计数器,manifest 条目占一个 id,Figma 之后 * 给它的 code.js 建条目时占 `fileMetadata.codeFileId` 那个 id。所以算下一个 * id 时不能只看已有条目的 id,还得把被 codeFileId / manifestFileId 预定掉的 * 号算进去,否则新插件会和 Figma 迟早要建的 code 条目撞号。 * * 这里不碰文件系统,好单测;读写与关 Figma 在 importPlugin.ts。 */ export interface FigmaFileMetadata { type: 'manifest' | 'code' | string /** manifest 条目专有:预留给同插件 code.js 条目的 id。 */ codeFileId?: number /** code 条目专有:反指所属 manifest 条目的 id。 */ manifestFileId?: number uiFileIds?: number[] } export interface FigmaLocalFileExtension { id: number manifestPath: string lastKnownName?: string lastKnownPluginId?: string fileMetadata?: FigmaFileMetadata [key: string]: unknown } /** 数组里混着 Figma 写的杂项,只把「有数字 id + 字符串 manifestPath」的当条目。 */ export function asExtensionEntries(value: unknown): FigmaLocalFileExtension[] { if (!Array.isArray(value)) return [] return value.filter((e): e is FigmaLocalFileExtension => !!e && typeof e === 'object' && typeof (e as FigmaLocalFileExtension).id === 'number' && typeof (e as FigmaLocalFileExtension).manifestPath === 'string', ) } /** * 下一个可用 id:已有条目 id 与被 codeFileId / manifestFileId 预定的 id 取最大再 +1。 * 空清单从 1 开始(Figma 自己也是从 1 起)。 */ export function nextExtensionId(entries: FigmaLocalFileExtension[]): number { let max = 0 for (const e of entries) { for (const n of [e.id, e.fileMetadata?.codeFileId, e.fileMetadata?.manifestFileId]) { if (typeof n === 'number' && Number.isFinite(n) && n > max) max = n } } return max + 1 } /** 按 manifestPath 精确查重;已导入过就返回那条,用来跳过写入。 */ export function findManifestEntry( entries: FigmaLocalFileExtension[], manifestPath: string, ): FigmaLocalFileExtension | undefined { return entries.find(e => e.manifestPath === manifestPath) } /** 造一条 manifest 条目;code 条目留给 Figma 自己建,这里只把 id 号占住。 */ export function buildManifestEntry(opts: { id: number manifestPath: string name: string }): FigmaLocalFileExtension { return { id: opts.id, manifestPath: opts.manifestPath, lastKnownName: opts.name, lastKnownPluginId: '', fileMetadata: { type: 'manifest', codeFileId: opts.id + 1, uiFileIds: [], }, } } export interface AppendResult { /** 追加后的完整条目数组;已存在时原样返回。 */ entries: FigmaLocalFileExtension[] /** false = 已导入过,调用方跳过写文件直接拉起 Figma。 */ added: boolean /** 这条插件在 Figma 里的显示名(新建用传入名,已存在用清单里记的名)。 */ name: string } /** 查重 → 算 id → 追加,一次算完,写文件的那端只管落盘。 */ export function appendManifestEntry( entries: FigmaLocalFileExtension[], opts: { manifestPath: string, name: string }, ): AppendResult { const existing = findManifestEntry(entries, opts.manifestPath) if (existing) return { entries, added: false, name: existing.lastKnownName || opts.name } const entry = buildManifestEntry({ id: nextExtensionId(entries), manifestPath: opts.manifestPath, name: opts.name, }) return { entries: [...entries, entry], added: true, name: opts.name } }