import type {
FigmaComment,
FigmaComponent,
FigmaFileMeta,
FigmaFileSummary,
FigmaImageFill,
FigmaImageMap,
FigmaNodeBundle,
FigmaNodeSummary,
FigmaStyle,
FigmaUserRef,
JsonRecord,
} from './types.ts'
function asRecord(value: unknown): JsonRecord {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as JsonRecord)
: {}
}
export function mapUser(value: unknown): FigmaUserRef | null {
const record = asRecord(value)
const id = typeof record.id === 'string' ? record.id : ''
if (!id) return null
return {
id,
handle: typeof record.handle === 'string' ? record.handle : null,
}
}
export function mapNode(value: unknown): FigmaNodeSummary | null {
const record = asRecord(value)
const id = typeof record.id === 'string' ? record.id : ''
if (!id) return null
const children = Array.isArray(record.children)
? record.children
.map(mapNode)
.filter((node): node is FigmaNodeSummary => node !== null)
: []
return {
id,
name: typeof record.name === 'string' ? record.name : '',
type: typeof record.type === 'string' ? record.type : '',
visible: record.visible !== false,
children,
}
}
export function mapFile(fileKey: string, value: unknown): FigmaFileSummary {
const record = asRecord(value)
const components =
record.components && typeof record.components === 'object'
? Object.keys(record.components as JsonRecord).length
: 0
const styles =
record.styles && typeof record.styles === 'object'
? Object.keys(record.styles as JsonRecord).length
: 0
return {
fileKey,
name: typeof record.name === 'string' ? record.name : '',
lastModified: typeof record.lastModified === 'string' ? record.lastModified : null,
version: typeof record.version === 'string' ? record.version : null,
role: typeof record.role === 'string' ? record.role : null,
editorType: typeof record.editorType === 'string' ? record.editorType : null,
thumbnailUrl: typeof record.thumbnailUrl === 'string' ? record.thumbnailUrl : null,
document: mapNode(record.document),
componentCount: components,
styleCount: styles,
}
}
export function mapFileMeta(fileKey: string, value: unknown): FigmaFileMeta {
const record = asRecord(value)
const file = asRecord(record.file)
const source = Object.keys(file).length > 0 ? file : record
return {
fileKey,
name: typeof source.name === 'string' ? source.name : '',
lastModified:
typeof source.last_touched_at === 'string'
? source.last_touched_at
: typeof source.lastModified === 'string'
? source.lastModified
: null,
thumbnailUrl: typeof source.thumbnail_url === 'string' ? source.thumbnail_url : null,
creator: mapUser(source.creator),
lastTouchedBy: mapUser(source.last_touched_by),
folderName: typeof source.folder_name === 'string' ? source.folder_name : null,
editorType: typeof source.editor_type === 'string' ? source.editor_type : null,
role: typeof source.role === 'string' ? source.role : null,
version: typeof source.version === 'string' ? source.version : null,
linkAccess: typeof source.link_access === 'string' ? source.link_access : null,
url: typeof source.url === 'string' ? source.url : null,
}
}
export function mapNodeBundle(id: string, value: unknown): FigmaNodeBundle {
const record = asRecord(value)
const components =
record.components && typeof record.components === 'object'
? Object.keys(record.components as JsonRecord).length
: 0
const styles =
record.styles && typeof record.styles === 'object'
? Object.keys(record.styles as JsonRecord).length
: 0
return {
id,
document: mapNode(record.document),
componentCount: components,
styleCount: styles,
}
}
function mapReactions(value: unknown): Array<{ emoji: string; count: number }> {
if (!Array.isArray(value)) return []
const counts = new Map<string, number>()
for (const item of value) {
const record = asRecord(item)
const emoji = typeof record.emoji === 'string' ? record.emoji : ''
if (!emoji) continue
counts.set(emoji, (counts.get(emoji) ?? 0) + 1)
}
return [...counts.entries()].map(([emoji, count]) => ({ emoji, count }))
}
export function mapComment(fileKey: string, value: unknown): FigmaComment {
const record = asRecord(value)
return {
id: typeof record.id === 'string' ? record.id : '',
message: typeof record.message === 'string' ? record.message : null,
fileKey,
parentId: typeof record.parent_id === 'string' ? record.parent_id : null,
createdAt: typeof record.created_at === 'string' ? record.created_at : null,
resolvedAt: typeof record.resolved_at === 'string' ? record.resolved_at : null,
orderId: typeof record.order_id === 'string' ? record.order_id : null,
user: mapUser(record.user),
clientMeta:
record.client_meta && typeof record.client_meta === 'object'
? (record.client_meta as JsonRecord)
: null,
reactions: mapReactions(record.reactions),
}
}
function mapLibraryItem(value: unknown): {
key: string
fileKey: string | null
nodeId: string | null
name: string
description: string | null
thumbnailUrl: string | null
createdAt: string | null
updatedAt: string | null
containingFrameName: string | null
styleType: string | null
} {
const record = asRecord(value)
const frame = asRecord(record.containing_frame)
return {
key: typeof record.key === 'string' ? record.key : '',
fileKey: typeof record.file_key === 'string' ? record.file_key : null,
nodeId: typeof record.node_id === 'string' ? record.node_id : null,
name: typeof record.name === 'string' ? record.name : '',
description: typeof record.description === 'string' ? record.description : null,
thumbnailUrl: typeof record.thumbnail_url === 'string' ? record.thumbnail_url : null,
createdAt: typeof record.created_at === 'string' ? record.created_at : null,
updatedAt: typeof record.updated_at === 'string' ? record.updated_at : null,
containingFrameName:
typeof frame.name === 'string'
? frame.name
: typeof record.containing_frame === 'string'
? record.containing_frame
: null,
styleType: typeof record.style_type === 'string' ? record.style_type : null,
}
}
export function mapComponent(value: unknown): FigmaComponent {
const item = mapLibraryItem(value)
return {
key: item.key,
fileKey: item.fileKey,
nodeId: item.nodeId,
name: item.name,
description: item.description,
thumbnailUrl: item.thumbnailUrl,
containingFrameName: item.containingFrameName,
createdAt: item.createdAt,
updatedAt: item.updatedAt,
}
}
export function mapStyle(value: unknown): FigmaStyle {
const item = mapLibraryItem(value)
return {
key: item.key,
fileKey: item.fileKey,
nodeId: item.nodeId,
name: item.name,
description: item.description,
styleType: item.styleType,
thumbnailUrl: item.thumbnailUrl,
createdAt: item.createdAt,
updatedAt: item.updatedAt,
}
}
function valuesFromMetaMap(value: unknown, key: 'components' | 'styles'): Array<unknown> {
const root = asRecord(value)
const meta = asRecord(root.meta)
const collection = meta[key] ?? root[key]
if (Array.isArray(collection)) return collection
if (collection && typeof collection === 'object') return Object.values(collection)
return []
}
export function mapComponentList(value: unknown): Array<FigmaComponent> {
return valuesFromMetaMap(value, 'components')
.map(mapComponent)
.filter((item) => item.key.length > 0 || (item.nodeId ?? '').length > 0)
}
export function mapStyleList(value: unknown): Array<FigmaStyle> {
return valuesFromMetaMap(value, 'styles')
.map(mapStyle)
.filter((item) => item.key.length > 0 || (item.name ?? '').length > 0)
}
export function mapImageMap(fileKey: string, value: unknown): FigmaImageMap {
const record = asRecord(value)
const images =
record.images && typeof record.images === 'object' ? (record.images as JsonRecord) : {}
return {
fileKey,
images: Object.entries(images).map(([nodeId, url]) => ({
nodeId,
url: typeof url === 'string' ? url : null,
})),
error: typeof record.err === 'string' ? record.err : null,
}
}
export function mapImageFills(value: unknown): Array<FigmaImageFill> {
const record = asRecord(value)
const meta = asRecord(record.meta)
const images =
meta.images && typeof meta.images === 'object'
? (meta.images as JsonRecord)
: record.images && typeof record.images === 'object'
? (record.images as JsonRecord)
: {}
return Object.entries(images)
.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
.map(([hash, url]) => ({ hash, url }))
}
export function asArray<T>(value: T | Array<T> | null | undefined): Array<T> {
if (value === undefined || value === null) return []
return Array.isArray(value) ? value : [value]
}