From a10911c772c5989f67af5ab8ac55992131e8d9cd Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sat, 18 Jul 2026 01:32:13 +0300 Subject: [PATCH] refactor(fig): extract NodeChange policy helpers - Move style references, plugin metadata, text values, and font feature conversion into @open-pencil/fig\n- Add a public node-change subpath with package-local and packed smoke coverage\n- Keep core conversion callers on compatibility exports during the staged migration --- bun.lock | 1 + packages/core/src/kiwi/fig/import.ts | 2 +- .../fig/instance-overrides/symbol/patches.ts | 2 +- .../core/src/kiwi/fig/node-change/convert.ts | 23 ++++++--- .../src/kiwi/fig/node-change/export-node.ts | 15 +++--- .../src/kiwi/fig/node-change/font/index.ts | 9 +++- .../src/kiwi/fig/node-change/serialize.ts | 14 +++--- .../src/kiwi/fig/node-change/style-runs.ts | 10 ++-- .../kiwi/fig/node-change/text-data-export.ts | 4 +- packages/fig/README.md | 1 + packages/fig/package.json | 9 +++- packages/fig/scripts/smoke-dist.ts | 4 +- .../src}/node-change/font/features.ts | 0 .../src}/node-change/font/variations.ts | 0 packages/fig/src/node-change/index.ts | 5 ++ .../src}/node-change/plugin-data.ts | 0 .../fig => fig/src}/node-change/style-refs.ts | 0 .../src}/node-change/text-values.ts | 0 packages/fig/tests/node-change.test.ts | 49 +++++++++++++++++++ packages/fig/tsconfig.json | 25 +++++++--- packages/fig/tsdown.config.ts | 3 +- tests/engine/io/fig/import/style-refs.test.ts | 3 +- tools/package-quality/src/smoke.ts | 4 ++ 23 files changed, 137 insertions(+), 46 deletions(-) rename packages/{core/src/kiwi/fig => fig/src}/node-change/font/features.ts (100%) rename packages/{core/src/kiwi/fig => fig/src}/node-change/font/variations.ts (100%) create mode 100644 packages/fig/src/node-change/index.ts rename packages/{core/src/kiwi/fig => fig/src}/node-change/plugin-data.ts (100%) rename packages/{core/src/kiwi/fig => fig/src}/node-change/style-refs.ts (100%) rename packages/{core/src/kiwi/fig => fig/src}/node-change/text-values.ts (100%) create mode 100644 packages/fig/tests/node-change.test.ts diff --git a/bun.lock b/bun.lock index 9f14f859b..42cab165a 100644 --- a/bun.lock +++ b/bun.lock @@ -216,6 +216,7 @@ }, "peerDependencies": { "@open-pencil/kiwi": "workspace:*", + "@open-pencil/scene-graph": "workspace:*", }, }, "packages/kiwi": { diff --git a/packages/core/src/kiwi/fig/import.ts b/packages/core/src/kiwi/fig/import.ts index aa8a6c8c1..6ce5f2af6 100644 --- a/packages/core/src/kiwi/fig/import.ts +++ b/packages/core/src/kiwi/fig/import.ts @@ -1,5 +1,6 @@ import { isNotNil } from 'es-toolkit/predicate' +import { applyStyleRefsToFields } from '@open-pencil/fig/node-change' import type { NodeChange, VariableDataValuesEntry, Color, GUID } from '@open-pencil/kiwi/fig/codec' import { SceneGraph } from '@open-pencil/scene-graph' import type { VariableType, VariableValue } from '@open-pencil/scene-graph' @@ -16,7 +17,6 @@ import { setVariableColorResolver, VARIABLE_BINDING_FIELDS_INVERSE } from '#core/kiwi/fig/node-change/convert' -import { applyStyleRefsToFields } from '#core/kiwi/fig/node-change/style-refs' type AssetRef = { key: string; version?: string } type AliasRef = { guid?: GUID; assetRef?: AssetRef } diff --git a/packages/core/src/kiwi/fig/instance-overrides/symbol/patches.ts b/packages/core/src/kiwi/fig/instance-overrides/symbol/patches.ts index 922a7e902..aa4cd4cce 100644 --- a/packages/core/src/kiwi/fig/instance-overrides/symbol/patches.ts +++ b/packages/core/src/kiwi/fig/instance-overrides/symbol/patches.ts @@ -1,3 +1,4 @@ +import { applyStyleRefsToFields } from '@open-pencil/fig/node-change' import type { GUID } from '@open-pencil/kiwi/fig/codec' import type { OverridePatch } from '#core/kiwi/fig/instance-overrides/patches' @@ -7,7 +8,6 @@ import type { SymbolOverrideFields } from '#core/kiwi/fig/instance-overrides/types' import { guidToString, VARIABLE_BINDING_FIELDS_INVERSE } from '#core/kiwi/fig/node-change/convert' -import { applyStyleRefsToFields } from '#core/kiwi/fig/node-change/style-refs' import { convertOverrideToProps } from './props' diff --git a/packages/core/src/kiwi/fig/node-change/convert.ts b/packages/core/src/kiwi/fig/node-change/convert.ts index a3c893432..cbbc17fcb 100644 --- a/packages/core/src/kiwi/fig/node-change/convert.ts +++ b/packages/core/src/kiwi/fig/node-change/convert.ts @@ -8,13 +8,11 @@ import { styleToWeight } from '#core/text/fonts' import { convertEffects, convertFills, convertStrokes } from './paint' import { importStyleRuns } from './style-runs' export { importStyleRuns } from './style-runs' -import { convertFigmaDerivedTextGlyphs } from './derived-text-glyphs' -import { convertFontFeatures } from './font/features' -import { convertFontVariations } from './font/variations' -import { convertLetterSpacing, convertLineHeight, mapTextDecoration } from './text-values' -export { convertEffects, convertFills, convertStrokes, setVariableColorResolver } from './paint' -export { convertLetterSpacing, convertLineHeight, mapTextDecoration } from './text-values' import { + convertFontFeatures, + convertFontVariations, + convertLetterSpacing, + convertLineHeight, extractBoundVariables, extractExportSettings, extractPluginData, @@ -22,8 +20,17 @@ import { getOpenPencilPluginValue, LAYOUT_DIRECTION_PLUGIN_KEY, NODE_TYPE_PLUGIN_KEY, - TEXT_DIRECTION_PLUGIN_KEY -} from './plugin-data' + TEXT_DIRECTION_PLUGIN_KEY, + mapTextDecoration +} from '@open-pencil/fig/node-change' + +import { convertFigmaDerivedTextGlyphs } from './derived-text-glyphs' +export { convertEffects, convertFills, convertStrokes, setVariableColorResolver } from './paint' +export { + convertLetterSpacing, + convertLineHeight, + mapTextDecoration +} from '@open-pencil/fig/node-change' import { resolveGeometryPaths, resolveVectorNetwork } from './vector-geometry' export { resolveGeometryPaths } from './vector-geometry' diff --git a/packages/core/src/kiwi/fig/node-change/export-node.ts b/packages/core/src/kiwi/fig/node-change/export-node.ts index 6d4d145b6..ab568f4b0 100644 --- a/packages/core/src/kiwi/fig/node-change/export-node.ts +++ b/packages/core/src/kiwi/fig/node-change/export-node.ts @@ -1,3 +1,10 @@ +import { + applyExportSettingsPluginData, + mergePluginData, + NODE_TYPE_PLUGIN_KEY, + serializePluginRelaunchData, + upsertPluginData +} from '@open-pencil/fig/node-change' import type { NodeChange, Paint } from '@open-pencil/kiwi/fig/codec' import { stringToGuid } from '@open-pencil/kiwi/fig/guid' import type { @@ -12,14 +19,6 @@ import type { Color, GUID, Matrix, Vector } from '@open-pencil/scene-graph/primi import { bytesToHex } from '#core/bytes/hex' import { DEFAULT_STROKE_MITER_LIMIT } from '#core/constants' -import { - applyExportSettingsPluginData, - mergePluginData, - NODE_TYPE_PLUGIN_KEY, - serializePluginRelaunchData, - upsertPluginData -} from './plugin-data' - export type KiwiNodeChange = NodeChange & Record type KiwiBooleanOperation = NonNullable diff --git a/packages/core/src/kiwi/fig/node-change/font/index.ts b/packages/core/src/kiwi/fig/node-change/font/index.ts index d51fc7a70..b83ab250f 100644 --- a/packages/core/src/kiwi/fig/node-change/font/index.ts +++ b/packages/core/src/kiwi/fig/node-change/font/index.ts @@ -1,3 +1,8 @@ -export { convertFontFeatures, applyFontFeaturesToKiwi } from './features' -export { figmaAxisTagToString, stringToFigmaAxisTag, convertFontVariations } from './variations' +export { + applyFontFeaturesToKiwi, + convertFontFeatures, + convertFontVariations, + figmaAxisTagToString, + stringToFigmaAxisTag +} from '@open-pencil/fig/node-change' export { buildFontDigestMap } from './digests' diff --git a/packages/core/src/kiwi/fig/node-change/serialize.ts b/packages/core/src/kiwi/fig/node-change/serialize.ts index 2e5d03ee4..cd64fbffe 100644 --- a/packages/core/src/kiwi/fig/node-change/serialize.ts +++ b/packages/core/src/kiwi/fig/node-change/serialize.ts @@ -13,6 +13,13 @@ export { } from '@open-pencil/kiwi/fig/container' export { buildFontDigestMap } from './font/digests' +import { + applyFontFeaturesToKiwi, + BOUND_VARIABLES_PLUGIN_KEY, + LAYOUT_DIRECTION_PLUGIN_KEY, + TEXT_DIRECTION_PLUGIN_KEY, + upsertPluginData +} from '@open-pencil/fig/node-change' import type { NodeChange, Paint, VariableConsumptionEntry } from '@open-pencil/kiwi/fig/codec' import type { SceneGraph, SceneNode } from '@open-pencil/scene-graph' import type { Color, GUID, JsonObject, Matrix } from '@open-pencil/scene-graph/primitives' @@ -23,13 +30,6 @@ import { sceneNodeToKiwiWithContext, type KiwiNodeChange } from './export-node' -import { applyFontFeaturesToKiwi } from './font/features' -import { - BOUND_VARIABLES_PLUGIN_KEY, - LAYOUT_DIRECTION_PLUGIN_KEY, - TEXT_DIRECTION_PLUGIN_KEY, - upsertPluginData -} from './plugin-data' import { exportTextData, fontVariationToKiwi } from './text-data-export' export function mapToFigmaType(type: SceneNode['type']): string { diff --git a/packages/core/src/kiwi/fig/node-change/style-runs.ts b/packages/core/src/kiwi/fig/node-change/style-runs.ts index 8aa27c649..56bdb4451 100644 --- a/packages/core/src/kiwi/fig/node-change/style-runs.ts +++ b/packages/core/src/kiwi/fig/node-change/style-runs.ts @@ -1,12 +1,16 @@ +import { + convertFontFeatures, + convertFontVariations, + convertLetterSpacing, + convertLineHeight, + mapTextDecoration +} from '@open-pencil/fig/node-change' import type { NodeChange } from '@open-pencil/kiwi/fig/codec' import type { CharacterStyleOverride, StyleRun } from '@open-pencil/scene-graph' import { styleToWeight } from '#core/text/fonts' -import { convertFontFeatures } from './font/features' -import { convertFontVariations } from './font/variations' import { convertFills } from './paint' -import { convertLetterSpacing, convertLineHeight, mapTextDecoration } from './text-values' function applyTextDecorationOverride(style: CharacterStyleOverride, override: NodeChange): void { const deco = override.textDecoration diff --git a/packages/core/src/kiwi/fig/node-change/text-data-export.ts b/packages/core/src/kiwi/fig/node-change/text-data-export.ts index 820921cca..1393ad3ea 100644 --- a/packages/core/src/kiwi/fig/node-change/text-data-export.ts +++ b/packages/core/src/kiwi/fig/node-change/text-data-export.ts @@ -1,11 +1,9 @@ +import { applyFontFeaturesToKiwi, stringToFigmaAxisTag } from '@open-pencil/fig/node-change' import type { NodeChange, Paint } from '@open-pencil/kiwi/fig/codec' import type { CharacterStyleOverride, SceneNode } from '@open-pencil/scene-graph' import { normalizeFontFamily, weightToFigmaStyle } from '#core/text/fonts' -import { applyFontFeaturesToKiwi } from './font/features' -import { stringToFigmaAxisTag } from './font/variations' - export function fontVariationToKiwi(variation: SceneNode['fontVariations'][number]) { const axisTag = stringToFigmaAxisTag(variation.axis) return axisTag === undefined diff --git a/packages/fig/README.md b/packages/fig/README.md index 3d440cbaa..a9165c82f 100644 --- a/packages/fig/README.md +++ b/packages/fig/README.md @@ -13,6 +13,7 @@ Current ownership: - Canvas payload and image resource handling - `readFigContainer()` / `writeFigContainer()` helpers for raw `fig-kiwi` payloads - `.fig` source and archive result types +- Dependency-free NodeChange policy helpers for styles, plugin metadata, text values, and font axes/features through `@open-pencil/fig/node-change` Planned ownership: diff --git a/packages/fig/package.json b/packages/fig/package.json index 32e2fafc1..864b8ca4e 100644 --- a/packages/fig/package.json +++ b/packages/fig/package.json @@ -9,6 +9,12 @@ "bun": "./src/index.ts", "import": "./dist/index.js", "default": "./dist/index.js" + }, + "./node-change": { + "types": "./dist/node-change.d.ts", + "bun": "./src/node-change/index.ts", + "import": "./dist/node-change.js", + "default": "./dist/node-change.js" } }, "main": "./dist/index.js", @@ -36,7 +42,8 @@ "provenance": true }, "peerDependencies": { - "@open-pencil/kiwi": "workspace:*" + "@open-pencil/kiwi": "workspace:*", + "@open-pencil/scene-graph": "workspace:*" }, "dependencies": { "fflate": "^0.8.2" diff --git a/packages/fig/scripts/smoke-dist.ts b/packages/fig/scripts/smoke-dist.ts index 2825fc98a..eb42004f6 100644 --- a/packages/fig/scripts/smoke-dist.ts +++ b/packages/fig/scripts/smoke-dist.ts @@ -1,11 +1,13 @@ export {} const mod = await import('../dist/index.js') +const nodeChange = await import('../dist/node-change.js') if ( mod.FIG_PACKAGE_STATUS !== 'archive-api' || typeof mod.parseFigBuffer !== 'function' || - typeof mod.writeFigArchive !== 'function' + typeof mod.writeFigArchive !== 'function' || + typeof nodeChange.convertLineHeight !== 'function' ) { throw new Error('Expected @open-pencil/fig archive API exports') } diff --git a/packages/core/src/kiwi/fig/node-change/font/features.ts b/packages/fig/src/node-change/font/features.ts similarity index 100% rename from packages/core/src/kiwi/fig/node-change/font/features.ts rename to packages/fig/src/node-change/font/features.ts diff --git a/packages/core/src/kiwi/fig/node-change/font/variations.ts b/packages/fig/src/node-change/font/variations.ts similarity index 100% rename from packages/core/src/kiwi/fig/node-change/font/variations.ts rename to packages/fig/src/node-change/font/variations.ts diff --git a/packages/fig/src/node-change/index.ts b/packages/fig/src/node-change/index.ts new file mode 100644 index 000000000..090f460b2 --- /dev/null +++ b/packages/fig/src/node-change/index.ts @@ -0,0 +1,5 @@ +export * from './font/features' +export * from './font/variations' +export * from './plugin-data' +export * from './style-refs' +export * from './text-values' diff --git a/packages/core/src/kiwi/fig/node-change/plugin-data.ts b/packages/fig/src/node-change/plugin-data.ts similarity index 100% rename from packages/core/src/kiwi/fig/node-change/plugin-data.ts rename to packages/fig/src/node-change/plugin-data.ts diff --git a/packages/core/src/kiwi/fig/node-change/style-refs.ts b/packages/fig/src/node-change/style-refs.ts similarity index 100% rename from packages/core/src/kiwi/fig/node-change/style-refs.ts rename to packages/fig/src/node-change/style-refs.ts diff --git a/packages/core/src/kiwi/fig/node-change/text-values.ts b/packages/fig/src/node-change/text-values.ts similarity index 100% rename from packages/core/src/kiwi/fig/node-change/text-values.ts rename to packages/fig/src/node-change/text-values.ts diff --git a/packages/fig/tests/node-change.test.ts b/packages/fig/tests/node-change.test.ts new file mode 100644 index 000000000..2bd9778ac --- /dev/null +++ b/packages/fig/tests/node-change.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, test } from 'bun:test' + +import { + applyStyleRefsToFields, + convertFontFeatures, + convertLetterSpacing, + convertLineHeight, + mapTextDecoration +} from '../src/node-change' + +describe('@open-pencil/fig NodeChange policy', () => { + test('converts normalized text values', () => { + expect(convertLineHeight({ value: 120, units: 'PERCENT' }, 20)).toBe(24) + expect(convertLetterSpacing({ value: 10, units: 'PERCENT' }, 20)).toBe(2) + expect(mapTextDecoration('UNDERLINE')).toBe('UNDERLINE') + }) + + test('converts Figma OpenType feature toggles', () => { + expect( + convertFontFeatures({ + toggledOnOTFeatures: ['DLIG'], + toggledOffOTFeatures: ['LIGA'] + }) + ).toEqual([ + { tag: 'DLIG', enabled: true }, + { tag: 'LIGA', enabled: false } + ]) + }) + + test('resolves imported style references before SceneGraph conversion', () => { + const fields: Record = { + styleIdForFill: { guid: { sessionID: 2, localID: 3 } } + } + applyStyleRefsToFields( + new Map([ + [ + '2:3', + { + type: 'RECTANGLE', + styleType: 'FILL', + fillPaints: [{ type: 'SOLID', visible: true }] + } + ] + ]), + fields + ) + expect(fields.fillPaints).toEqual([{ type: 'SOLID', visible: true }]) + }) +}) diff --git a/packages/fig/tsconfig.json b/packages/fig/tsconfig.json index f78bbb1ae..7143ab4be 100644 --- a/packages/fig/tsconfig.json +++ b/packages/fig/tsconfig.json @@ -1,14 +1,23 @@ { - "extends": "../../tsconfig.json", "compilerOptions": { - "rootDir": ".", - "outDir": "dist", + "target": "ES2022", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, "declaration": true, "declarationMap": true, - "emitDeclarationOnly": true, - "noEmit": false, - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo", - "types": ["bun"] + "outDir": "./dist", + "rootDir": ".", + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "types": ["bun"], + "paths": { + "@open-pencil/fig": ["./src/index.ts"], + "@open-pencil/fig/node-change": ["./src/node-change/index.ts"] + } }, - "include": ["src"] + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] } diff --git a/packages/fig/tsdown.config.ts b/packages/fig/tsdown.config.ts index b7cae2edb..5e935e1a5 100644 --- a/packages/fig/tsdown.config.ts +++ b/packages/fig/tsdown.config.ts @@ -2,7 +2,8 @@ import { defineConfig } from 'tsdown' export default defineConfig({ entry: { - index: './src/index.ts' + index: './src/index.ts', + 'node-change': './src/node-change/index.ts' }, platform: 'neutral', format: ['esm'], diff --git a/tests/engine/io/fig/import/style-refs.test.ts b/tests/engine/io/fig/import/style-refs.test.ts index e05da69cf..0155746f6 100644 --- a/tests/engine/io/fig/import/style-refs.test.ts +++ b/tests/engine/io/fig/import/style-refs.test.ts @@ -1,9 +1,8 @@ import { describe, expect, test } from 'bun:test' +import { applyStyleRefsToFields } from '@open-pencil/fig/node-change' import type { NodeChange } from '@open-pencil/kiwi/fig/codec' -import { applyStyleRefsToFields } from '#core/kiwi/fig/node-change/style-refs' - describe('fig import style refs', () => { test('effect and grid styles replace stale direct payloads', () => { const effectGuid = { sessionID: 4, localID: 5000 } diff --git a/tools/package-quality/src/smoke.ts b/tools/package-quality/src/smoke.ts index b66a60844..c453e73f8 100644 --- a/tools/package-quality/src/smoke.ts +++ b/tools/package-quality/src/smoke.ts @@ -93,6 +93,10 @@ try { "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( + "const { convertLineHeight } = await import('@open-pencil/fig/node-change'); if (convertLineHeight({ value: 120, units: 'PERCENT' }, 20) !== 24) throw new Error('Fig NodeChange subpath failed')", + tempDir + ) nodeEval( "const { SceneGraph } = await import('@open-pencil/scene-graph'); const graph = new SceneGraph(); if (graph.getPages().length !== 1) throw new Error('SceneGraph package smoke failed')", tempDir