From f0d10edf696ce23e86ac3f34a268ef9f205cf2f5 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sat, 18 Jul 2026 01:23:06 +0300 Subject: [PATCH] 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 --- packages/core/src/io/formats/fig/compress.ts | 24 +--------- packages/fig/README.md | 4 +- packages/fig/scripts/smoke-dist.ts | 6 ++- packages/fig/src/archive.ts | 48 +++++++++++++++++++- packages/fig/src/index.ts | 9 +++- packages/fig/tests/index.test.ts | 15 +++--- tools/package-quality/src/smoke.ts | 2 +- 7 files changed, 71 insertions(+), 37 deletions(-) diff --git a/packages/core/src/io/formats/fig/compress.ts b/packages/core/src/io/formats/fig/compress.ts index 99d365fb0..b0824a8b2 100644 --- a/packages/core/src/io/formats/fig/compress.ts +++ b/packages/core/src/io/formats/fig/compress.ts @@ -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' diff --git a/packages/fig/README.md b/packages/fig/README.md index 6b9134b96..3d440cbaa 100644 --- a/packages/fig/README.md +++ b/packages/fig/README.md @@ -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 diff --git a/packages/fig/scripts/smoke-dist.ts b/packages/fig/scripts/smoke-dist.ts index 1643dec64..2825fc98a 100644 --- a/packages/fig/scripts/smoke-dist.ts +++ b/packages/fig/scripts/smoke-dist.ts @@ -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') } diff --git a/packages/fig/src/archive.ts b/packages/fig/src/archive.ts index b297d4d9e..7b850c6e9 100644 --- a/packages/fig/src/archive.ts +++ b/packages/fig/src/archive.ts @@ -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 + }) +} diff --git a/packages/fig/src/index.ts b/packages/fig/src/index.ts index 67deb4cd3..d12f8ba48 100644 --- a/packages/fig/src/index.ts +++ b/packages/fig/src/index.ts @@ -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, diff --git a/packages/fig/tests/index.test.ts b/packages/fig/tests/index.test.ts index 2f2f13b1a..ad5b8e201 100644 --- a/packages/fig/tests/index.test.ts +++ b/packages/fig/tests/index.test.ts @@ -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) diff --git a/tools/package-quality/src/smoke.ts b/tools/package-quality/src/smoke.ts index 974894d1f..b66a60844 100644 --- a/tools/package-quality/src/smoke.ts +++ b/tools/package-quality/src/smoke.ts @@ -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(