refactor(fig): own archive assembly

- Add public writeFigArchive API for canvas, thumbnail, metadata, and image entries\n- Reduce core compression to a compatibility re-export\n- Exercise archive writing through package-local and packed smoke tests
This commit is contained in:
Danila Poyarkov 2026-07-18 01:23:06 +03:00
parent f9e2588b84
commit f0d10edf69
7 changed files with 71 additions and 37 deletions

View file

@ -1,23 +1 @@
import { zipSync, type Zippable } from 'fflate'
import { buildFigKiwi } from '#core/kiwi/fig/node-change/serialize'
export function compressFigDataSync(
schemaDeflated: Uint8Array,
kiwiData: Uint8Array,
thumbnailPng: Uint8Array,
metaJson: string,
imageEntries: Array<{ name: string; data: Uint8Array }>,
figKiwiVersion?: number
): Uint8Array {
const canvasData = buildFigKiwi(schemaDeflated, kiwiData, figKiwiVersion)
const zipEntries: Zippable = {
'canvas.fig': [canvasData, { level: 0 }],
'thumbnail.png': [thumbnailPng, { level: 0 }],
'meta.json': new TextEncoder().encode(metaJson)
}
for (const entry of imageEntries) {
zipEntries[entry.name] = [entry.data, { level: 0 }]
}
return zipSync(zipEntries)
}
export { compressFigDataSync } from '@open-pencil/fig'

View file

@ -9,13 +9,13 @@ SceneGraph conversion policy. Production SceneGraph read/write remains available
Current ownership:
- Complete `.fig` archive parsing through `parseFigBuffer()`
- Canvas payload and image resource discovery
- `.fig` archive assembly through `writeFigArchive()`
- Canvas payload and image resource handling
- `readFigContainer()` / `writeFigContainer()` helpers for raw `fig-kiwi` payloads
- `.fig` source and archive result types
Planned ownership:
- `.fig` archive assembly
- SceneGraph ⇄ Figma `NodeChange` conversion
- Raw Figma metadata precedence and invalidation policy
- Component and instance interpretation

View file

@ -2,7 +2,11 @@ export {}
const mod = await import('../dist/index.js')
if (mod.FIG_PACKAGE_STATUS !== 'archive-api' || typeof mod.parseFigBuffer !== 'function') {
if (
mod.FIG_PACKAGE_STATUS !== 'archive-api' ||
typeof mod.parseFigBuffer !== 'function' ||
typeof mod.writeFigArchive !== 'function'
) {
throw new Error('Expected @open-pencil/fig archive API exports')
}

View file

@ -1,8 +1,23 @@
import { unzipSync } from 'fflate'
import { unzipSync, zipSync, type Zippable } from 'fflate'
import type { NodeChange } from '@open-pencil/kiwi/fig/codec'
import { buildFigKiwi } from '@open-pencil/kiwi/fig/container'
import { decodeFigKiwiCanvas } from '@open-pencil/kiwi/fig/parse'
export interface FigImageEntry {
name: string
data: Uint8Array
}
export interface WriteFigArchiveInput {
schemaDeflated: Uint8Array
kiwiData: Uint8Array
thumbnailPng: Uint8Array
metaJson: string
images?: FigImageEntry[]
figKiwiVersion?: number
}
export interface FigParseResult {
nodeChanges: NodeChange[]
blobs: Uint8Array[]
@ -51,3 +66,34 @@ export function parseFigBuffer(buffer: ArrayBuffer): FigParseResult {
return { ...decoded, images }
}
/** Assemble a complete zipped `.fig` archive from an encoded Kiwi message and resources. */
export function writeFigArchive(input: WriteFigArchiveInput): Uint8Array {
const canvasData = buildFigKiwi(input.schemaDeflated, input.kiwiData, input.figKiwiVersion)
const entries: Zippable = {
'canvas.fig': [canvasData, { level: 0 }],
'thumbnail.png': [input.thumbnailPng, { level: 0 }],
'meta.json': new TextEncoder().encode(input.metaJson)
}
for (const image of input.images ?? []) entries[image.name] = [image.data, { level: 0 }]
return zipSync(entries)
}
/** Compatibility signature used by core while archive assembly migrates to this package. */
export function compressFigDataSync(
schemaDeflated: Uint8Array,
kiwiData: Uint8Array,
thumbnailPng: Uint8Array,
metaJson: string,
imageEntries: FigImageEntry[],
figKiwiVersion?: number
): Uint8Array {
return writeFigArchive({
schemaDeflated,
kiwiData,
thumbnailPng,
metaJson,
images: imageEntries,
figKiwiVersion
})
}

View file

@ -1,4 +1,11 @@
export { parseFigBuffer, type FigParseResult } from './archive'
export {
compressFigDataSync,
parseFigBuffer,
writeFigArchive,
type FigImageEntry,
type FigParseResult,
type WriteFigArchiveInput
} from './archive'
import {
FIG_KIWI_DEFAULT_VERSION,

View file

@ -1,6 +1,6 @@
import { beforeAll, describe, expect, it } from 'bun:test'
import { deflateSync, zipSync } from 'fflate'
import { deflateSync } from 'fflate'
import {
createNodeChangesMessage,
@ -14,6 +14,7 @@ import {
assertFigPackageReady,
parseFigBuffer,
readFigContainer,
writeFigArchive,
writeFigContainer
} from '../src/index'
@ -40,9 +41,9 @@ describe('@open-pencil/fig package API', () => {
})
it('parses complete .fig archives and image resources', () => {
const canvas = writeFigContainer({
const bytes = writeFigArchive({
schemaDeflated: deflateSync(getSchemaBytes()),
dataRaw: encodeMessage(
kiwiData: encodeMessage(
createNodeChangesMessage(0, 0, [
{
guid: { sessionID: 0, localID: 0 },
@ -52,11 +53,9 @@ describe('@open-pencil/fig package API', () => {
}
])
),
source: { fileName: 'fixture.fig' }
})
const bytes = zipSync({
'canvas.fig': canvas,
'images/hash': new Uint8Array([9, 8, 7])
thumbnailPng: new Uint8Array([1]),
metaJson: '{}',
images: [{ name: 'images/hash', data: new Uint8Array([9, 8, 7]) }]
})
const parsed = parseFigBuffer(bytes.buffer as ArrayBuffer)

View file

@ -90,7 +90,7 @@ try {
tempDir
)
nodeEval(
"const { FIG_PACKAGE_STATUS, parseFigBuffer, readFigContainer, writeFigContainer } = await import('@open-pencil/fig'); if (FIG_PACKAGE_STATUS !== 'archive-api' || typeof parseFigBuffer !== 'function') throw new Error('Fig package status smoke failed'); const document = readFigContainer(writeFigContainer({ schemaDeflated: new Uint8Array([1]), dataRaw: new Uint8Array([2]) })); if (document.dataRaw[0] !== 2) throw new Error('Fig container smoke failed')",
"const { FIG_PACKAGE_STATUS, parseFigBuffer, writeFigArchive, readFigContainer, writeFigContainer } = await import('@open-pencil/fig'); if (FIG_PACKAGE_STATUS !== 'archive-api' || typeof parseFigBuffer !== 'function' || typeof writeFigArchive !== 'function') throw new Error('Fig package status smoke failed'); const document = readFigContainer(writeFigContainer({ schemaDeflated: new Uint8Array([1]), dataRaw: new Uint8Array([2]) })); if (document.dataRaw[0] !== 2) throw new Error('Fig container smoke failed')",
tempDir
)
nodeEval(