refactor(fig): own instance interpretation

- Move component properties, symbol overrides, derived symbol data, and clone synchronization into @open-pencil/fig\n- Add the public instance-overrides subpath with package tests and packed smoke coverage\n- Remove cross-package forwarding modules and enforce direct owner imports in architecture checks
This commit is contained in:
Danila Poyarkov 2026-07-18 02:06:34 +03:00
parent 22a160bfc3
commit 2bd2304bcb
77 changed files with 243 additions and 167 deletions

View file

@ -23,7 +23,7 @@ The root app (`src/`) is the Tauri/Vite desktop editor. App-specific editor, doc
### Public package exports
Use public package exports across package/app boundaries. Do not import workspace package internals from app code.
Use public package exports across package/app boundaries. Do not import workspace package internals from app code. Do not create cross-package re-export shim files whose only purpose is forwarding another package's API. Import the owning package directly at call sites; public compatibility barrels may re-export the owner directly when preserving an established package API.
- `@open-pencil/scene-graph` — SceneGraph, node types, primitives, copy/snap/undo, instance helpers, variable helpers, vector-network types.
- `@open-pencil/core` — broad compatibility barrel for editor/rendering/tooling APIs.

View file

@ -4,7 +4,8 @@
### Changed
- Move complete `.fig` archive parsing and NodeChange-to-SceneGraph conversion policy into `@open-pencil/fig`, keeping `@open-pencil/kiwi` focused on Kiwi schema, message, and raw container mechanics.
- Move complete `.fig` archive parsing, NodeChange-to-SceneGraph conversion, and component/instance interpretation into `@open-pencil/fig`, keeping `@open-pencil/kiwi` focused on Kiwi schema, message, and raw container mechanics.
- Remove internal cross-package forwarding modules; import `@open-pencil/fig`, `@open-pencil/pen`, and `@open-pencil/scene-graph` from their owning public exports.
- Add Figma-style page management in the Pages panel, including rename/delete actions and drag-and-drop page reordering.
- Add DOM/CSS import and authoring support so HTML, CSS, Tailwind, and JSX can be converted into editable OpenPencil documents from the app, CLI, and SDK.
- Add Tailwind class serialization for DOM/CSS HTML export in the SDK and CLI.

View file

@ -100,11 +100,6 @@
"import": "./dist/io/formats/fig/index.js",
"default": "./dist/io/formats/fig/index.js"
},
"./io/formats/pen": {
"types": "./dist/io/formats/pen/index.d.ts",
"import": "./dist/io/formats/pen/index.js",
"default": "./dist/io/formats/pen/index.js"
},
"./io/formats/jsx": {
"types": "./dist/io/formats/jsx/index.d.ts",
"import": "./dist/io/formats/jsx/index.js",

View file

@ -1,7 +1,6 @@
import type { SceneGraph, SceneNode } from '@open-pencil/scene-graph'
import { copyFills } from '@open-pencil/scene-graph/copy'
import { parseSVGPath } from '#core/io/formats/svg/parse-path'
import { parseSVGPath } from '@open-pencil/scene-graph/parse-path'
import { makeBooleanSourcePath, makeStrokeOutlinePath, nodePathTransform } from './boolean'
import type { SkiaRenderer } from './renderer'

View file

@ -1,18 +1,18 @@
import { inflateSync, deflateSync } from 'fflate'
import { populateAndApplyOverrides } from '@open-pencil/fig/instance-overrides'
import type { InstanceNodeChange } from '@open-pencil/fig/instance-overrides'
import {
nodeChangeToProps,
shouldImportTextAsAutoSize,
sortChildren
} from '@open-pencil/fig/node-change'
import { initCodec, getCompiledSchema, getSchemaBytes } from '@open-pencil/kiwi/fig/codec'
import type { NodeChange as KiwiNodeChange } from '@open-pencil/kiwi/fig/codec'
import { decodeBinarySchema, compileSchema, ByteBuffer } from '@open-pencil/kiwi/schema-runtime'
import type { SceneGraph, SceneNode } from '@open-pencil/scene-graph'
import { shapeTextForClipboard } from './canvas/text/clipboard'
import { populateAndApplyOverrides } from './kiwi/fig/instance-overrides'
import type { InstanceNodeChange } from './kiwi/fig/instance-overrides'
import {
nodeChangeToProps,
shouldImportTextAsAutoSize,
sortChildren
} from './kiwi/fig/node-change/convert'
import {
sceneNodeToKiwi,
buildFigKiwi,

View file

@ -1,7 +1,7 @@
import { iconToSVG } from '@iconify/utils'
import svgpath from 'svgpath'
import { parseSVGPath } from '#core/io/formats/svg/parse-path'
import { parseSVGPath } from '@open-pencil/scene-graph/parse-path'
import type { IconData, IconifyIconEntry, IconPathInfo } from './types'

View file

@ -272,7 +272,7 @@ export {
type SVGExportOptions
} from './io/formats/svg/export'
export { svg, renderSVGNode, type SVGNode } from './io/formats/svg/node'
export { parseSVGPath } from './io/formats/svg/parse-path'
export { parseSVGPath } from '@open-pencil/scene-graph/parse-path'
export {
fetchIcon,
fetchIcons,
@ -371,7 +371,7 @@ export {
} from './clipboard'
export { probeGlyphOutlineCommands, type GlyphOutlineProbe } from './text/opentype'
export { readPenFile, parsePenFile } from './io/formats/pen'
export { readPenFile, parsePenFile } from '@open-pencil/pen'
export {
readFigFile,

View file

@ -1,7 +1,8 @@
import { parsePenFile } from '@open-pencil/pen'
import { sceneNodeToJSX, selectionToJSX } from '#core/design-jsx'
import { exportFigFile, parseFigFile } from './formats/fig'
import { parsePenFile } from './formats/pen'
import { headlessRenderNodes, renderNodesToImage, type RasterExportFormat } from './formats/raster'
import { renderNodesToSVG } from './formats/svg'
import { extractExportGraph, findPageId } from './subgraph'

View file

@ -1 +0,0 @@
export { compressFigDataSync } from '@open-pencil/fig'

View file

@ -1,4 +1,4 @@
import { compressFigDataSync } from './compress'
import { compressFigDataSync } from '@open-pencil/fig'
interface CompressMessage {
schemaDeflated: Uint8Array

View file

@ -1,6 +1,8 @@
import type { CanvasKit } from 'canvaskit-wasm'
import { deflateSync, inflateSync } from 'fflate'
import { compressFigDataSync } from '@open-pencil/fig'
import { stringToGuid } from '@open-pencil/fig/node-change'
import { initCodec, getCompiledSchema, getSchemaBytes } from '@open-pencil/kiwi/fig/codec'
import type { NodeChange } from '@open-pencil/kiwi/fig/codec'
import { decodeBinarySchema, compileSchema, ByteBuffer } from '@open-pencil/kiwi/schema-runtime'
@ -11,7 +13,6 @@ import type { SkiaRenderer } from '#core/canvas'
import { CANVAS_BG_COLOR, IS_BROWSER, IS_TAURI } from '#core/constants'
import { renderThumbnail } from '#core/io/formats/raster'
import { populateAllLazyFigImportRoots } from '#core/kiwi/fig/lazy-import'
import { stringToGuid } from '#core/kiwi/fig/node-change/convert'
import {
sceneNodeToKiwi,
fractionalPosition,
@ -21,8 +22,6 @@ import {
makeCanvasNodeChange
} from '#core/kiwi/fig/node-change/serialize'
import { compressFigDataSync } from './compress'
const THUMBNAIL_1X1 = Uint8Array.from(
atob(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='
@ -521,7 +520,7 @@ export async function exportFigFile(
return compressFigData(schemaDeflated, kiwiData, thumbnailPng, metaJson, imageEntries, version)
}
export { compressFigDataSync } from './compress'
export { compressFigDataSync } from '@open-pencil/fig'
function canUseWorker(): boolean {
return typeof Worker !== 'undefined' && IS_BROWSER

View file

@ -1 +0,0 @@
export * from '@open-pencil/pen'

View file

@ -1 +0,0 @@
export * from '@open-pencil/pen'

View file

@ -1 +0,0 @@
export { parsePenFile, readPenFile } from '@open-pencil/pen'

View file

@ -1 +0,0 @@
export { parseSVGPath } from '@open-pencil/scene-graph/parse-path'

View file

@ -11,7 +11,7 @@ export {
jsxFormat
} from './formats'
export { exportFigFile, parseFigFile, readFigFile } from './formats/fig'
export { parsePenFile, readPenFile } from './formats/pen'
export { parsePenFile, readPenFile } from '@open-pencil/pen'
export { sceneNodeToJSX, selectionToJSX, type JSXFormat } from './formats/jsx'
export {
computeContentBounds,

View file

@ -1,22 +1,22 @@
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'
import { BLACK } from '#core/constants'
import { populateAndApplyOverrides } from '#core/kiwi/fig/instance-overrides'
import type { InstanceNodeChange } from '#core/kiwi/fig/instance-overrides'
import { setLazyFigImportContext } from '#core/kiwi/fig/lazy-import'
import { populateAndApplyOverrides } from '@open-pencil/fig/instance-overrides'
import type { InstanceNodeChange } from '@open-pencil/fig/instance-overrides'
import {
applyStyleRefsToFields,
guidToString,
nodeChangeToProps,
shouldImportTextAsAutoSize,
sortChildren,
setVariableColorResolver,
VARIABLE_BINDING_FIELDS_INVERSE
} from '#core/kiwi/fig/node-change/convert'
} 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'
import { BLACK } from '#core/constants'
import { setLazyFigImportContext } from '#core/kiwi/fig/lazy-import'
type AssetRef = { key: string; version?: string }
type AliasRef = { guid?: GUID; assetRef?: AssetRef }

View file

@ -1,8 +1,7 @@
import { populateAndApplyOverrides } from '@open-pencil/fig/instance-overrides'
import type { InstanceNodeChange } from '@open-pencil/fig/instance-overrides'
import type { SceneGraph } from '@open-pencil/scene-graph'
import { populateAndApplyOverrides } from '#core/kiwi/fig/instance-overrides'
import type { InstanceNodeChange } from '#core/kiwi/fig/instance-overrides'
export interface LazyFigImportContext {
changeMap: Map<string, InstanceNodeChange>
guidToNodeId: Map<string, string>

View file

@ -1 +0,0 @@
export * from '@open-pencil/fig/node-change'

View file

@ -16,15 +16,17 @@ export { buildFontDigestMap } from './font/digests'
import {
applyFontFeaturesToKiwi,
BOUND_VARIABLES_PLUGIN_KEY,
guidToString,
LAYOUT_DIRECTION_PLUGIN_KEY,
stringToGuid,
TEXT_DIRECTION_PLUGIN_KEY,
upsertPluginData
upsertPluginData,
VARIABLE_BINDING_FIELDS
} 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'
import { guidToString, stringToGuid, VARIABLE_BINDING_FIELDS } from './convert'
import {
buildAssetRefToVarGuidMap,
sceneNodeToKiwiWithContext,

View file

@ -1 +0,0 @@
export { importStyleRuns } from '@open-pencil/fig/node-change'

View file

@ -1,3 +1,4 @@
import type { InstanceNodeChange } from '@open-pencil/fig/instance-overrides'
import { SceneGraph } from '@open-pencil/scene-graph'
import type {
SceneNode,
@ -6,7 +7,6 @@ import type {
DocumentColorSpace
} from '@open-pencil/scene-graph'
import type { InstanceNodeChange } from '#core/kiwi/fig/instance-overrides'
import { getLazyFigImportContext, setLazyFigImportContext } from '#core/kiwi/fig/lazy-import'
export interface SerializedLazyFigImportContext {

View file

@ -1,10 +1,10 @@
import { parseSVGPath } from '@open-pencil/scene-graph/parse-path'
import type { Rect } from '@open-pencil/scene-graph/primitives'
import { parseColor } from '#core/color'
import { createPathStroke } from '#core/icons/path-style'
import { extractPaths } from '#core/icons/svg'
import type { IconPathInfo } from '#core/icons/types'
import { parseSVGPath } from '#core/io/formats/svg/parse-path'
import { defineTool } from '#core/tools/schema'
function parseSvgViewBox(svg: string): Rect | null {

View file

@ -14,13 +14,13 @@ Current ownership:
- `readFigContainer()` / `writeFigContainer()` helpers for raw `fig-kiwi` payloads
- `.fig` source and archive result types
- NodeChange-to-SceneGraph property conversion, including styles, plugin metadata, text, paint, vector, and font policy, through `@open-pencil/fig/node-change`
- Package-local archive, conversion, and dist smoke tests
- Component-property, symbol-override, derived-symbol-data, and instance synchronization policy through `@open-pencil/fig/instance-overrides`
- Package-local archive, conversion, instance, and dist smoke tests
Planned ownership:
- Remaining SceneGraph-to-`NodeChange` export conversion and document orchestration
- Raw Figma metadata precedence and invalidation policy
- Component and instance interpretation
- Oracle-backed `.fig` fixtures
Non-goals:

View file

@ -15,6 +15,12 @@
"bun": "./src/node-change/index.ts",
"import": "./dist/node-change.js",
"default": "./dist/node-change.js"
},
"./instance-overrides": {
"types": "./dist/instance-overrides.d.ts",
"bun": "./src/instance-overrides/index.ts",
"import": "./dist/instance-overrides.js",
"default": "./dist/instance-overrides.js"
}
},
"main": "./dist/index.js",

View file

@ -1,12 +1,14 @@
export {}
const mod = await import('../dist/index.js')
const instanceOverrides = await import('../dist/instance-overrides.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 instanceOverrides.populateAndApplyOverrides !== 'function' ||
typeof nodeChange.convertLineHeight !== 'function'
) {
throw new Error('Expected @open-pencil/fig archive API exports')

View file

@ -1,14 +1,9 @@
import { guidToString } from '@open-pencil/fig/node-change'
import { copyFills, copyStyleRuns } from '@open-pencil/scene-graph/copy'
import { applyOverridePatch, type OverridePatch } from '#core/kiwi/fig/instance-overrides/patches'
import { getComponentRoot } from '#core/kiwi/fig/instance-overrides/resolve'
import type {
ComponentPropRef,
ComponentPropValue,
OverrideContext
} from '#core/kiwi/fig/instance-overrides/types'
import { guidToString } from '#core/kiwi/fig/node-change/convert'
import { applyOverridePatch, type OverridePatch } from '../patches'
import { getComponentRoot } from '../resolve'
import type { ComponentPropRef, ComponentPropValue, OverrideContext } from '../types'
import { propTextCharacters } from './values'
function applyPatchAndMark(

View file

@ -1,20 +1,16 @@
import type { SceneNode } from '@open-pencil/scene-graph'
import { buildCloneIndex, instanceAndClones } from '#core/kiwi/fig/instance-overrides/clone-index'
import { applyComponentPropRef } from '#core/kiwi/fig/instance-overrides/component-props/apply'
import {
fallbackRefsForChild,
findPropRefs,
valueForRef
} from '#core/kiwi/fig/instance-overrides/component-props/refs'
import { assignmentsToValueMap } from '#core/kiwi/fig/instance-overrides/component-props/values'
import { resolveOverrideTarget } from '#core/kiwi/fig/instance-overrides/resolve'
import { buildCloneIndex, instanceAndClones } from '../clone-index'
import { resolveOverrideTarget } from '../resolve'
import type {
ComponentPropAssignment,
ComponentPropRef,
ComponentPropValue,
OverrideContext
} from '#core/kiwi/fig/instance-overrides/types'
} from '../types'
import { applyComponentPropRef } from './apply'
import { fallbackRefsForChild, findPropRefs, valueForRef } from './refs'
import { assignmentsToValueMap } from './values'
function applyChildPropRefs(
ctx: OverrideContext,

View file

@ -1,5 +1,4 @@
import type { OverrideContext } from '#core/kiwi/fig/instance-overrides/types'
import type { OverrideContext } from '../types'
import { applyInstanceDirectAssignments, applyOverrideAssignments } from './assignments'
import { collectAssignmentsMap, collectPropRefsMap } from './maps'

View file

@ -1,8 +1,4 @@
import type {
ComponentPropAssignment,
ComponentPropRef,
OverrideContext
} from '#core/kiwi/fig/instance-overrides/types'
import type { ComponentPropAssignment, ComponentPropRef, OverrideContext } from '../types'
export function collectPropRefsMap(ctx: OverrideContext): Map<string, ComponentPropRef[]> {
const result = new Map<string, ComponentPropRef[]>()

View file

@ -1,10 +1,6 @@
import type {
ComponentPropRef,
ComponentPropValue,
OverrideContext
} from '#core/kiwi/fig/instance-overrides/types'
import { guidToString } from '#core/kiwi/fig/node-change/convert'
import { guidToString } from '@open-pencil/fig/node-change'
import type { ComponentPropRef, ComponentPropValue, OverrideContext } from '../types'
import { normalizePropName, stringToGuidParts } from './values'
export function findPropRefs(

View file

@ -1,11 +1,7 @@
import { guidToString } from '@open-pencil/fig/node-change'
import type { GUID } from '@open-pencil/kiwi/fig/codec'
import type {
ComponentPropAssignment,
ComponentPropValue,
OverrideContext
} from '#core/kiwi/fig/instance-overrides/types'
import { guidToString } from '#core/kiwi/fig/node-change/convert'
import type { ComponentPropAssignment, ComponentPropValue, OverrideContext } from '../types'
export function normalizePropName(value: string): string {
return value.toLowerCase().replace(/[^a-z0-9]/g, '')

View file

@ -1,7 +1,7 @@
import { resolveGeometryPaths } from '@open-pencil/fig/node-change'
import type { GeometryPath, SceneNode } from '@open-pencil/scene-graph'
import type { DerivedSymbolOverride } from '#core/kiwi/fig/instance-overrides/types'
import { resolveGeometryPaths } from '#core/kiwi/fig/node-change/convert'
import type { DerivedSymbolOverride } from '../types'
function scaleGeometryBlobs(geom: GeometryPath[], sx: number, sy: number): GeometryPath[] {
if (sx === 1 && sy === 1) return geom

View file

@ -1,10 +1,6 @@
import { applyOverridePatch } from '#core/kiwi/fig/instance-overrides/patches'
import { resolveOverrideTarget } from '#core/kiwi/fig/instance-overrides/resolve'
import type {
DerivedSymbolOverride,
OverrideContext
} from '#core/kiwi/fig/instance-overrides/types'
import { applyOverridePatch } from '../patches'
import { resolveOverrideTarget } from '../resolve'
import type { DerivedSymbolOverride, OverrideContext } from '../types'
import { buildDsdLayoutUpdates } from './layout'
import { propagateDsdChanges } from './propagate'

View file

@ -1,12 +1,11 @@
import { convertFigmaDerivedTextGlyphs } from '@open-pencil/fig/node-change'
import {
convertFigmaDerivedTextGlyphs,
convertLetterSpacing,
convertLineHeight
} from '@open-pencil/fig/node-change'
import type { SceneNode } from '@open-pencil/scene-graph'
import type {
DerivedSymbolOverride,
OverrideContext
} from '#core/kiwi/fig/instance-overrides/types'
import { convertLetterSpacing, convertLineHeight } from '#core/kiwi/fig/node-change/convert'
import type { DerivedSymbolOverride, OverrideContext } from '../types'
import { resolveDsdGeometry } from './geometry'
function getVisibleSiblingCount(

View file

@ -1,8 +1,8 @@
import type { SceneNode } from '@open-pencil/scene-graph'
import { copyGeometryPaths } from '@open-pencil/scene-graph/copy'
import { buildClonesMap } from '#core/kiwi/fig/instance-overrides/sync'
import type { OverrideContext } from '#core/kiwi/fig/instance-overrides/types'
import { buildClonesMap } from '../sync'
import type { OverrideContext } from '../types'
function buildCloneUpdates(
ctx: OverrideContext,

View file

@ -1,3 +1,7 @@
export { buildDsdLayoutUpdates } from './derived-symbol-data/layout'
export { propagateDsdChanges } from './derived-symbol-data/propagate'
export { protectField, type ProtectionMap } from './patches'
export { syncChildrenDeep, syncNodeProps } from './sync'
export type {
InstanceNodeChange,
OverrideContext,
@ -10,12 +14,11 @@ export type {
SymbolOverride
} from './types'
import { guidToString } from '@open-pencil/fig/node-change'
import type { SceneGraph, SceneNode } from '@open-pencil/scene-graph'
import { copyFills, copyStyleRuns } from '@open-pencil/scene-graph/copy'
import type { JsonObject } from '@open-pencil/scene-graph/primitives'
import { guidToString } from '#core/kiwi/fig/node-change/convert'
import { applyComponentProperties } from './component-props'
import { applyConstraintScaling } from './constraints'
import { applyDerivedSymbolData } from './derived-symbol-data'

View file

@ -1,8 +1,7 @@
import type { SceneNode } from '@open-pencil/scene-graph'
import { repopulateInstance } from '#core/kiwi/fig/instance-overrides/resolve'
import type { OverrideContext } from '#core/kiwi/fig/instance-overrides/types'
import { repopulateInstance } from '../resolve'
import type { OverrideContext } from '../types'
import { protectField, protectPatchProps } from './protection'
import type { OverridePatch } from './types'

View file

@ -1,9 +1,8 @@
import { guidToString } from '@open-pencil/fig/node-change'
import type { GUID } from '@open-pencil/kiwi/fig/codec'
import type { SceneNode } from '@open-pencil/scene-graph'
import { copyStrokes } from '@open-pencil/scene-graph/copy'
import { guidToString } from '#core/kiwi/fig/node-change/convert'
import type { InstanceNodeChange, OverrideContext } from './types'
const MAX_CHAIN_DEPTH = 20

View file

@ -1,7 +1,6 @@
import { applyOverridePatch } from '#core/kiwi/fig/instance-overrides/patches'
import { resolveOverrideTarget } from '#core/kiwi/fig/instance-overrides/resolve'
import type { OverrideContext } from '#core/kiwi/fig/instance-overrides/types'
import { applyOverridePatch } from '../patches'
import { resolveOverrideTarget } from '../resolve'
import type { OverrideContext } from '../types'
import { patchFromSymbolOverride } from './patches'
function isActiveInstance(ctx: OverrideContext, nodeId: string | undefined): nodeId is string {

View file

@ -1,14 +1,12 @@
import { applyStyleRefsToFields } from '@open-pencil/fig/node-change'
import {
applyStyleRefsToFields,
guidToString,
VARIABLE_BINDING_FIELDS_INVERSE
} 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'
import type {
OverrideContext,
SymbolOverride,
SymbolOverrideFields
} from '#core/kiwi/fig/instance-overrides/types'
import { guidToString, VARIABLE_BINDING_FIELDS_INVERSE } from '#core/kiwi/fig/node-change/convert'
import type { OverridePatch } from '../patches'
import type { OverrideContext, SymbolOverride, SymbolOverrideFields } from '../types'
import { convertOverrideToProps } from './props'
interface AliasRef {

View file

@ -1,7 +1,3 @@
import type { NodeChange, Paint, Effect as KiwiEffect } from '@open-pencil/kiwi/fig/codec'
import type { SceneNode, ArcData, TextAutoResize } from '@open-pencil/scene-graph'
import type { Vector } from '@open-pencil/scene-graph/primitives'
import {
convertFills,
mapStackSizing,
@ -15,8 +11,11 @@ import {
importStyleRuns,
convertStrokes,
convertEffects
} from '#core/kiwi/fig/node-change/convert'
import { styleToWeight } from '#core/text/fonts'
} from '@open-pencil/fig/node-change'
import type { NodeChange, Paint, Effect as KiwiEffect } from '@open-pencil/kiwi/fig/codec'
import type { SceneNode, ArcData, TextAutoResize } from '@open-pencil/scene-graph'
import { styleToWeight } from '@open-pencil/scene-graph'
import type { Vector } from '@open-pencil/scene-graph/primitives'
function applyOverridePaints(ov: Record<string, unknown>, updates: Partial<SceneNode>): void {
if (ov.textData != null) {

View file

@ -1,7 +1,6 @@
import type { SceneGraph, SceneNode } from '@open-pencil/scene-graph'
import type { ProtectionMap } from '#core/kiwi/fig/instance-overrides/patches'
import type { ProtectionMap } from '../patches'
import { syncNodeProps } from './fields'
export function recloneChildren(

View file

@ -1,8 +1,8 @@
import type { SceneGraph, SceneNode } from '@open-pencil/scene-graph'
import { copyFills, copyStrokes, copyEffects, copyStyleRuns } from '@open-pencil/scene-graph/copy'
import type { ProtectionMap, ProtectedField } from '#core/kiwi/fig/instance-overrides/patches'
import { isFieldProtected } from '#core/kiwi/fig/instance-overrides/patches'
import { isFieldProtected } from '../patches'
import type { ProtectedField, ProtectionMap } from '../patches'
function canSync(
protections: ProtectionMap | undefined,

View file

@ -1,7 +1,6 @@
import type { SceneGraph } from '@open-pencil/scene-graph'
import type { ProtectionMap } from '#core/kiwi/fig/instance-overrides/patches'
import type { ProtectionMap } from '../patches'
import { buildClonesMap, syncChildrenDeep } from './clones'
import { syncNodeProps } from './fields'

View file

@ -0,0 +1,42 @@
import { describe, expect, test } from 'bun:test'
import { SceneGraph } from '@open-pencil/scene-graph'
import {
populateAndApplyOverrides,
protectField,
syncNodeProps,
type ProtectionMap
} from '../src/instance-overrides'
describe('@open-pencil/fig instance interpretation', () => {
test('populates an empty instance from its component tree', () => {
const graph = new SceneGraph()
const pageId = graph.getPages()[0].id
const component = graph.createNode('COMPONENT', pageId, { name: 'Button' })
graph.createNode('TEXT', component.id, { text: 'Label' })
const instance = graph.createNode('INSTANCE', pageId, {
componentId: component.id,
childIds: []
})
populateAndApplyOverrides(graph, new Map(), new Map())
const populated = graph.getNode(instance.id)
expect(populated?.childIds).toHaveLength(1)
expect(graph.getNode(populated?.childIds[0] ?? '')?.text).toBe('Label')
})
test('preserves protected text while synchronizing other fields', () => {
const graph = new SceneGraph()
const pageId = graph.getPages()[0].id
const source = graph.createNode('TEXT', pageId, { text: 'Source', visible: false })
const target = graph.createNode('TEXT', pageId, { text: 'Override', visible: true })
const protections: ProtectionMap = new Map()
protectField(protections, target.id, 'text')
syncNodeProps(graph, source, target, protections)
expect(graph.getNode(target.id)).toMatchObject({ text: 'Override', visible: false })
})
})

View file

@ -15,6 +15,7 @@
"types": ["bun"],
"paths": {
"@open-pencil/fig": ["./src/index.ts"],
"@open-pencil/fig/instance-overrides": ["./src/instance-overrides/index.ts"],
"@open-pencil/fig/node-change": ["./src/node-change/index.ts"]
}
},

View file

@ -3,6 +3,7 @@ import { defineConfig } from 'tsdown'
export default defineConfig({
entry: {
index: './src/index.ts',
'instance-overrides': './src/instance-overrides/index.ts',
'node-change': './src/node-change/index.ts'
},
platform: 'neutral',

View file

@ -1 +0,0 @@
export { parseSVGPath } from '@open-pencil/scene-graph/parse-path'

View file

@ -2,6 +2,7 @@ import { SceneGraph } from '@open-pencil/scene-graph'
import type { LayoutMode, LayoutSizing, SceneNode, VectorNetwork } from '@open-pencil/scene-graph'
import { copyEffects, copyFills, copyStrokes } from '@open-pencil/scene-graph/copy'
import { populateInstanceChildren } from '@open-pencil/scene-graph/instances'
import { parseSVGPath } from '@open-pencil/scene-graph/parse-path'
import {
applyCornerRadius,
@ -24,7 +25,6 @@ import {
type PenNode,
type VarContext
} from './convert'
import { parseSVGPath } from './parse-path'
function scaleVectorNetwork(vn: VectorNetwork, targetW: number, targetH: number): void {
if (vn.vertices.length === 0) return

View file

@ -1,9 +1,9 @@
import { describe, expect, test } from 'bun:test'
import { nodeChangeToProps } from '@open-pencil/fig/node-change'
import type { NodeChange } from '@open-pencil/kiwi/fig/codec'
import { importNodeChanges } from '#core/kiwi/fig/import'
import { nodeChangeToProps } from '#core/kiwi/fig/node-change/convert'
describe('Figma boolean operation import', () => {
test('preserves boolean operation nodes', () => {

View file

@ -1,11 +1,12 @@
import { describe, expect, test } from 'bun:test'
import {
buildDsdLayoutUpdates,
propagateDsdChanges,
type OverrideContext
} from '@open-pencil/fig/instance-overrides'
import { SceneGraph } from '@open-pencil/scene-graph'
import type { OverrideContext } from '#core/kiwi/fig/instance-overrides'
import { buildDsdLayoutUpdates } from '#core/kiwi/fig/instance-overrides/derived-symbol-data/layout'
import { propagateDsdChanges } from '#core/kiwi/fig/instance-overrides/derived-symbol-data/propagate'
function pageId(graph: SceneGraph): string {
return graph.getPages()[0].id
}

View file

@ -1,11 +1,13 @@
import { describe, expect, test } from 'bun:test'
import {
protectField,
syncNodeProps,
type ProtectionMap
} from '@open-pencil/fig/instance-overrides'
import { SceneGraph } from '@open-pencil/scene-graph'
import type { Fill, Stroke } from '@open-pencil/scene-graph'
import { protectField, type ProtectionMap } from '#core/kiwi/fig/instance-overrides/patches'
import { syncNodeProps } from '#core/kiwi/fig/instance-overrides/sync'
function pageId(graph: SceneGraph): string {
return graph.getPages()[0].id
}

View file

@ -1,9 +1,8 @@
import { describe, expect, test } from 'bun:test'
import { nodeChangeToProps, importStyleRuns } from '@open-pencil/fig/node-change'
import type { NodeChange } from '@open-pencil/kiwi/fig/codec'
import { nodeChangeToProps, importStyleRuns } from '#core/kiwi/fig/node-change/convert'
describe('Figma font variation import', () => {
test('imports base text variable font axes', () => {
const props = nodeChangeToProps(

View file

@ -1,7 +1,8 @@
import { describe, expect, test } from 'bun:test'
import { nodeChangeToProps } from '@open-pencil/fig/node-change'
import type { NodeChange } from '#core/kiwi/fig/codec'
import { nodeChangeToProps } from '#core/kiwi/fig/node-change/convert'
import { parseFixture } from '#tests/helpers/fig-fixtures'
import { collectAllNodes } from '#tests/helpers/fig-traversal'

View file

@ -1,10 +1,10 @@
import { describe, expect, test } from 'bun:test'
import { readFileSync } from 'node:fs'
import { nodeChangeToProps } from '@open-pencil/fig/node-change'
import type { NodeChange } from '@open-pencil/kiwi/fig/codec'
import { SceneGraph, type MaskType } from '@open-pencil/scene-graph'
import { nodeChangeToProps } from '#core/kiwi/fig/node-change/convert'
import { sceneNodeToKiwi } from '#core/kiwi/fig/node-change/serialize'
interface MaskOracleEntry {

View file

@ -1,9 +1,8 @@
import { describe, expect, test } from 'bun:test'
import { nodeChangeToProps } from '@open-pencil/fig/node-change'
import type { NodeChange } from '@open-pencil/kiwi/fig/codec'
import { nodeChangeToProps } from '#core/kiwi/fig/node-change/convert'
describe('Figma mask import', () => {
test('imports schema mask fields', () => {
const props = nodeChangeToProps(

View file

@ -1,9 +1,8 @@
import { describe, expect, test } from 'bun:test'
import { nodeChangeToProps } from '@open-pencil/fig/node-change'
import type { NodeChange } from '@open-pencil/kiwi/fig/codec'
import { nodeChangeToProps } from '#core/kiwi/fig/node-change/convert'
const sourceNodeId = { sessionID: 12, localID: 34 }
const customEffectId = { guid: { sessionID: 56, localID: 78 } }

View file

@ -1,6 +1,6 @@
import { describe, expect, test } from 'bun:test'
import { FIGMA_RAW_NODE_FIELD_KEYS } from '#core/kiwi/fig/node-change/convert'
import { FIGMA_RAW_NODE_FIELD_KEYS } from '@open-pencil/fig/node-change'
const RAW_FIELD_COVERAGE = {
rendered: [

View file

@ -1,10 +1,10 @@
import { describe, expect, test } from 'bun:test'
import { readFileSync } from 'node:fs'
import { nodeChangeToProps } from '@open-pencil/fig/node-change'
import type { NodeChange, Paint } from '@open-pencil/kiwi/fig/codec'
import { SceneGraph } from '@open-pencil/scene-graph'
import { nodeChangeToProps } from '#core/kiwi/fig/node-change/convert'
import { sceneNodeToKiwi } from '#core/kiwi/fig/node-change/serialize'
interface OracleColor {

View file

@ -3,10 +3,9 @@ import { readFileSync } from 'node:fs'
import ts from 'typescript'
import { FIGMA_RAW_NODE_FIELD_KEYS } from '@open-pencil/fig/node-change'
import { parseSchema } from '@open-pencil/kiwi/schema-runtime'
import { FIGMA_RAW_NODE_FIELD_KEYS } from '#core/kiwi/fig/node-change/convert'
interface SchemaField {
name: string
type: string | null

View file

@ -1,8 +1,8 @@
import { beforeAll, describe, expect, test } from 'bun:test'
import { exportFigFile, initCodec, parseFigFile, SceneGraph } from '@open-pencil/core'
import { guidToString } from '@open-pencil/core/kiwi/fig/node-change/convert'
import { parseFigBuffer } from '@open-pencil/fig'
import { guidToString } from '@open-pencil/fig/node-change'
function decodeExport(bytes: Uint8Array) {
return parseFigBuffer(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength))

View file

@ -1,8 +1,7 @@
import { describe, expect, test } from 'bun:test'
import { SceneGraph } from '@open-pencil/core'
import { syncChildrenDeep } from '#core/kiwi/fig/instance-overrides/sync/clones'
import { syncChildrenDeep } from '@open-pencil/fig/instance-overrides'
describe('instance override clone sync', () => {
test('reclones nested instance children when the source component changes', () => {

View file

@ -1,7 +1,7 @@
import { describe, expect, test } from 'bun:test'
import type { SceneNode } from '@open-pencil/core'
import { applyPadding, isVarRef, type VarContext } from '@open-pencil/core/io/formats/pen'
import { applyPadding, isVarRef, type VarContext } from '@open-pencil/pen'
/**
* Regression test for open-pencil/open-pencil#201

View file

@ -1,10 +1,10 @@
import { writeFile } from 'node:fs/promises'
import type { SceneNode } from '@open-pencil/scene-graph'
import { parseSVGPath } from '@open-pencil/scene-graph/parse-path'
import { initCanvasKit } from '#cli/headless'
import { SkiaRenderer } from '#core/canvas'
import { parseSVGPath } from '#core/io/formats/svg/parse-path'
import { createAPI } from '#tests/engine/figma/api/helpers'
import { expectDefined } from '#tests/helpers/assert'

View file

@ -0,0 +1,28 @@
import ts from 'typescript'
import { createTextRule, type Rule } from './support.ts'
export function isCrossPackageReexportShim(sourceRel: string, content: string): boolean {
if (!/^packages\/[^/]+\/src\/.*\.[cm]?tsx?$/u.test(sourceRel)) return false
const source = ts.createSourceFile(sourceRel, content, ts.ScriptTarget.Latest, false)
if (source.statements.length === 0) return false
return source.statements.every(
(statement) =>
ts.isExportDeclaration(statement) &&
ts.isStringLiteral(statement.moduleSpecifier) &&
statement.moduleSpecifier.text.startsWith('@open-pencil/')
)
}
export const noCrossPackageReexportShims: Rule = createTextRule(
'open-pencil/no-cross-package-reexport-shims',
(sourceRel, content) =>
isCrossPackageReexportShim(sourceRel, content)
? [
{
message:
'Remove this cross-package re-export shim and import the owning package directly.'
}
]
: []
)

View file

@ -2,6 +2,7 @@ import path from 'node:path'
import { parse as parseVueSfc } from 'vue/compiler-sfc'
import { noCrossPackageReexportShims } from './cross-package-reexport-shims.ts'
import {
noDynamicTailwindStateClasses,
noVueTemplateUiHooksOrSvg
@ -496,6 +497,7 @@ export const openPencilArchitecturePlugin = {
meta: { name: 'open-pencil-architecture', version: '0.0.0' },
ruleDefinitions: [
preferDomainFoldersOverFilenamePrefixes,
noCrossPackageReexportShims,
scriptsAreEntrypointShims,
strictToolsLayout,
strictTestFilePlacement,

View file

@ -0,0 +1,35 @@
import { describe, expect, test } from 'bun:test'
import { isCrossPackageReexportShim } from '../src/steiger-rules/cross-package-reexport-shims'
describe('cross-package re-export shims', () => {
test('rejects star and named forwarding-only modules', () => {
expect(
isCrossPackageReexportShim(
'packages/core/src/kiwi/fig/instance-overrides/index.ts',
"export * from '@open-pencil/fig/instance-overrides'\n"
)
).toBe(true)
expect(
isCrossPackageReexportShim(
'packages/core/src/io/formats/fig/compress.ts',
"export { compressFigDataSync } from '@open-pencil/fig'\n"
)
).toBe(true)
})
test('allows package barrels with owned exports or implementation', () => {
expect(
isCrossPackageReexportShim(
'packages/core/src/index.ts',
"export { parsePenFile } from '@open-pencil/pen'\nexport { createEditor } from './editor'\n"
)
).toBe(false)
expect(
isCrossPackageReexportShim(
'packages/core/src/io/formats.ts',
"import { parsePenFile } from '@open-pencil/pen'\nexport const pen = parsePenFile\n"
)
).toBe(false)
})
})

View file

@ -97,6 +97,10 @@ try {
"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 { populateAndApplyOverrides } = await import('@open-pencil/fig/instance-overrides'); if (typeof populateAndApplyOverrides !== 'function') throw new Error('Fig instance override 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