refactor(core): avoid record casts for dynamic writes

- Use KiwiNodeChange index signature for raw Figma field assignment
- Type preserved raw NodeChange keys with keyof NodeChange
- Replace scene graph property write casts with a typed setter
This commit is contained in:
Danila Poyarkov 2026-05-24 22:37:03 +03:00
parent 22280cc758
commit 7f2c21bb23
3 changed files with 27 additions and 19 deletions

View file

@ -823,7 +823,7 @@ export const FIGMA_RAW_NODE_FIELD_KEYS = [
'vectorData',
'fillGeometry',
'strokeGeometry'
]
] as const satisfies readonly (keyof NodeChange)[]
function extractFigmaRawGeometry(
nc: NodeChange,
@ -831,7 +831,7 @@ function extractFigmaRawGeometry(
): Pick<SceneNode['source']['fig'], 'rawSize' | 'rawTransform' | 'rawNodeFields'> {
const rawNodeFields: Record<string, unknown> = {}
for (const key of FIGMA_RAW_NODE_FIELD_KEYS) {
const value = (nc as Record<string, unknown>)[key]
const value = nc[key]
if (value !== undefined) rawNodeFields[key] = preserveFigmaPayloadBlobs(value, blobs)
}
return {

View file

@ -309,9 +309,9 @@ function applyRawFigmaNodeFields(
blobIndexByHex: context.blobIndexByHex,
includePaintVariables: true,
includeVariableMaps: true
}) as Record<string, unknown>
for (const key of Object.keys(materialized)) {
if (RAW_FIELDS_OVERRIDE_BLOCKLIST.has(key)) continue
}) as Partial<KiwiNodeChange>
for (const key of Object.keys(materialized) as (keyof KiwiNodeChange)[]) {
if (RAW_FIELDS_OVERRIDE_BLOCKLIST.has(String(key))) continue
// For paint arrays on imported nodes, the raw NC data preserves the
// original opacity/color.a split (e.g. opacity=0 for invisible strokes).
// The scene model may lose this distinction for instance children whose
@ -328,7 +328,7 @@ function applyRawFigmaNodeFields(
if (context.assetRefToVarGuid && context.assetRefToVarGuid.size > 0) {
paints = convertColorVarAssetRefs(paints, context.assetRefToVarGuid)
}
;(nc as Record<string, unknown>)[key] = paints
nc[key] = paints
continue
}
// Also convert colorVar.assetRef in raw effects (e.g. shadow color variables)
@ -339,16 +339,16 @@ function applyRawFigmaNodeFields(
context.assetRefToVarGuid.size > 0
) {
const converted = convertColorVarAssetRefs(materialized[key], context.assetRefToVarGuid)
;(nc as Record<string, unknown>)[key] = converted
nc[key] = converted
continue
}
if (key === 'derivedTextData' && node.source.id) {
;(nc as Record<string, unknown>)[key] = materialized[key]
nc[key] = materialized[key]
continue
}
// Skip any key already set on nc — explicit serialization takes priority
if (key in (nc as Record<string, unknown>)) continue
;(nc as Record<string, unknown>)[key] = materialized[key]
if (key in nc) continue
nc[key] = materialized[key]
}
}
@ -360,7 +360,7 @@ function applyRawFigmaNodeFields(
* reference resolvable regardless of whether key/version is present on the
* VARIABLE NodeChange.
*/
function convertColorVarAssetRefs(paints: unknown, assetRefToVarGuid: Map<string, GUID>): unknown {
function convertColorVarAssetRefs<T>(paints: T, assetRefToVarGuid: Map<string, GUID>): T {
if (!Array.isArray(paints)) return paints
const result = paints.map((paint: Record<string, unknown>) => {
const colorVar = paint.colorVar as Record<string, unknown> | undefined
@ -390,7 +390,7 @@ function convertColorVarAssetRefs(paints: unknown, assetRefToVarGuid: Map<string
})
// Check if any paint was actually changed (skip expensive JSON comparison)
for (let i = 0; i < paints.length; i++) {
if (result[i] !== paints[i]) return result
if (result[i] !== paints[i]) return result as T
}
return paints
}

View file

@ -1,4 +1,4 @@
import type { SceneGraph, SceneNode, Fill, Stroke, Effect, StyleRun } from './'
import type { SceneGraph, SceneNode } from './'
import { copyEffects, copyFills, copyStrokes, copyStyleRuns } from './copy'
const INSTANCE_SYNC_PROPS: (keyof SceneNode)[] = [
@ -40,22 +40,30 @@ const INSTANCE_SYNC_PROPS: (keyof SceneNode)[] = [
'borderLeftWeight'
]
function setSceneProp<K extends keyof SceneNode>(
target: Partial<SceneNode>,
key: K,
value: SceneNode[K]
): void {
target[key] = value
}
function copyProp(
target: Partial<SceneNode> | SceneNode,
source: SceneNode,
key: keyof SceneNode
): void {
const val = source[key]
if (key === 'fills') {
;(target as Record<string, unknown>)[key] = copyFills(val as Fill[])
setSceneProp(target, key, copyFills(source.fills))
} else if (key === 'strokes') {
;(target as Record<string, unknown>)[key] = copyStrokes(val as Stroke[])
setSceneProp(target, key, copyStrokes(source.strokes))
} else if (key === 'effects') {
;(target as Record<string, unknown>)[key] = copyEffects(val as Effect[])
setSceneProp(target, key, copyEffects(source.effects))
} else if (key === 'styleRuns') {
;(target as Record<string, unknown>)[key] = copyStyleRuns(val as StyleRun[])
setSceneProp(target, key, copyStyleRuns(source.styleRuns))
} else {
;(target as Record<string, unknown>)[key] = Array.isArray(val) ? structuredClone(val) : val
const value = source[key]
setSceneProp(target, key, Array.isArray(value) ? structuredClone(value) : value)
}
}