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
This commit is contained in:
Danila Poyarkov 2026-07-18 01:32:13 +03:00
parent f0d10edf69
commit a10911c772
23 changed files with 137 additions and 46 deletions

View file

@ -216,6 +216,7 @@
},
"peerDependencies": {
"@open-pencil/kiwi": "workspace:*",
"@open-pencil/scene-graph": "workspace:*",
},
},
"packages/kiwi": {

View file

@ -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 }

View file

@ -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'

View file

@ -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'

View file

@ -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<string, unknown>
type KiwiBooleanOperation = NonNullable<NodeChange['booleanOperation']>

View file

@ -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'

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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"

View file

@ -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')
}

View file

@ -0,0 +1,5 @@
export * from './font/features'
export * from './font/variations'
export * from './plugin-data'
export * from './style-refs'
export * from './text-values'

View file

@ -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<string, unknown> = {
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 }])
})
})

View file

@ -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"]
}

View file

@ -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'],

View file

@ -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 }

View file

@ -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