import { readFile, writeFile, readdir } from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { nanoid } from 'nanoid' import { LibraryItem_v1, LibraryItem, LibraryItems } from '@excalidraw/excalidraw/types' // 合并素材库 // 由于官方不支持多素材库文件,只能将其合并成一个文件 type LibraryItems_v1 = readonly LibraryItem_v1[]; interface ImportedLibraryData extends Partial { /** @compatible v1 */ library?: LibraryItems; } interface ExportedLibraryData { type: string; version: number; source: string; libraryItems: LibraryItems; } /** * checks if library item does not exist already in current library */ const isUniqueItem = ( existingLibraryItems: LibraryItems, targetLibraryItem: LibraryItem, ) => { return !existingLibraryItems.find((libraryItem) => { if (libraryItem.elements.length !== targetLibraryItem.elements.length) { return false; } // detect z-index difference by checking the excalidraw elements // are in order return libraryItem.elements.every((libItemExcalidrawItem, idx) => { return ( libItemExcalidrawItem.id === targetLibraryItem.elements[idx].id && libItemExcalidrawItem.versionNonce === targetLibraryItem.elements[idx].versionNonce ); }); }); }; const isUniqueItemV1 = ( existingLibraryItems: LibraryItems, targetLibraryItem: LibraryItem_v1, ) => { return !existingLibraryItems.find((libraryItem) => { if (libraryItem.elements.length !== targetLibraryItem.length) { return false; } // detect z-index difference by checking the excalidraw elements // are in order return libraryItem.elements.every((libItemExcalidrawItem, idx) => { return ( libItemExcalidrawItem.id === targetLibraryItem[idx].id && libItemExcalidrawItem.versionNonce === targetLibraryItem[idx].versionNonce ); }); }); }; /** Merges otherItems into localItems. Unique items in otherItems array are sorted first. */ const mergeLibraryItems = ( localItems: LibraryItems, otherItems: LibraryItems | LibraryItems_v1, version: number ): LibraryItems => { const newItems: LibraryItem[] = []; otherItems.map(item => { if (version === 1 && isUniqueItemV1(localItems, item)) { newItems.push({ status: "published", elements: [...item], id: nanoid(), created: Date.now() }); } else if (isUniqueItem(localItems, item)) { newItems.push({...item}); } }) return [...newItems, ...localItems]; }; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const inputDir = path.resolve(__dirname, '../lib'); // 输入目录 const outputFile = path.resolve(__dirname, '../library.excalidrawlib'); // 输出文件路径 function isExcalidrawlibFile(fileName: string): boolean { return fileName.endsWith('.excalidrawlib'); } function positionError(obj: any, msg?: string): false { console.error({ error_msg: msg, error_obj: obj }) return false } function isLibraryItem(obj: any): obj is LibraryItem { if (!obj) return positionError(obj, "LibraryItem obj does not exist") if (!obj.id || !obj.status || !obj.elements || !obj.created) return positionError(obj, "obj missing fields") if (typeof obj.id !== 'string') return positionError(obj, "obj.id type is not string") if (obj.status !== "published" && obj.status !== "unpublished") return positionError(obj, "obj.status value is wrong") if (!Array.isArray(obj.elements)) return positionError(obj, "obj.elements is not an array") if (typeof obj.created !== 'number') return positionError(obj, "obj.created type is not number") return true } function isLibrary(obj: any): obj is LibraryItem { if (!obj) return positionError(obj, "Library obj does not exist") if (!Array.isArray(obj)) return positionError(obj, "obj.elements is not an array") return true } function isLibraryItems(obj: any): obj is LibraryItems { if (!obj) return positionError(obj, "LibraryItems obj does not exist") if (!Array.isArray(obj)) return positionError(obj, "obj is not an array") for (const item of obj) { if (!isLibraryItem(item)) return positionError(obj, "LibraryItems is not an array") } return true } function isImportedLibraryData(obj: any): obj is ImportedLibraryData { if (!obj) return positionError(obj, "ImportedLibraryData obj does not exist") if (obj.type !== "excalidrawlib") return positionError(obj, "obj.type value is not excalidrawlib") if (typeof obj.version !== "number") return positionError(obj, "obj.version type is not number") if ((!obj.libraryItems || !isLibraryItems(obj.libraryItems)) && (!obj.library || !isLibrary(obj.library)) ) return positionError(obj, "The library array does not exist or is wrong") return true } async function readExcalidrawlibFile(filePath: string): Promise { const content = await readFile(filePath, 'utf-8'); const parsed = JSON.parse(content); if (!isImportedLibraryData(parsed)) { console.error({filePath, parsed}) throw new Error(`Invalid excalidrawlib file: ${filePath}`); } return parsed; } async function mergeExcalidrawlibFiles(dir: string): Promise { const files = (await readdir(dir)).filter(isExcalidrawlibFile); let libraryItems: LibraryItems = [] let mianLibData: ExportedLibraryData | undefined = undefined for (const file of files) { const fullPath = path.join(dir, file); const data = await readExcalidrawlibFile(fullPath); if (file === "library.excalidrawlib") { mianLibData = { type: data.type ?? "excalidrawlib", version: data.version ?? 2, source: data.source ?? "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", libraryItems: data.libraryItems ?? data.library ?? [] } } else { libraryItems = mergeLibraryItems(libraryItems, data.libraryItems ?? data.library ?? [], data.version ?? 1) } } if (mianLibData === undefined) return undefined mianLibData.libraryItems = mergeLibraryItems(mianLibData.libraryItems, libraryItems) return mianLibData; } async function saveMergedExcalidrawlib(data: ImportedLibraryData, filePath: string) { const jsonString = JSON.stringify(data, null, 2); // 缩进美化 await writeFile(filePath, jsonString, 'utf-8'); console.log(`✅ 合并完成,已保存到 ${filePath}`); } // 执行流程 const merged = await mergeExcalidrawlibFiles(inputDir); if (merged !== undefined) { saveMergedExcalidrawlib(merged, outputFile); }