Skip to content

Commit c63d190

Browse files
authored
Merge pull request #36 from dev-five-git/fix-property-issue
Fix property issue
2 parents 09d7088 + 9528c55 commit c63d190

File tree

10 files changed

+156
-61
lines changed

10 files changed

+156
-61
lines changed

bun.lock

Lines changed: 32 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
"license": "",
1717
"devDependencies": {
1818
"@figma/plugin-typings": "^1.123",
19-
"@rspack/cli": "^1.7.8",
20-
"@rspack/core": "^1.7.8",
19+
"@rspack/cli": "^1.7.9",
20+
"@rspack/core": "^1.7.9",
2121

2222
"husky": "^9.1",
2323
"typescript": "^5.9",

src/code-impl.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from './codegen/props/selector'
1212
import { ResponsiveCodegen } from './codegen/responsive/ResponsiveCodegen'
1313
import { isReservedVariantKey } from './codegen/utils/extract-instance-variant-props'
14+
import { getComponentPropertyDefinitions } from './codegen/utils/get-component-property-definitions'
1415
import { nodeProxyTracker } from './codegen/utils/node-proxy'
1516
import { perfEnd, perfReport, perfReset, perfStart } from './codegen/utils/perf'
1617
import { resetVariableCache } from './codegen/utils/variable-cache'
@@ -111,7 +112,7 @@ export function generateComponentUsage(node: SceneNode): string | null {
111112
(node as ComponentNode).parent?.type === 'COMPONENT_SET'
112113
? ((node as ComponentNode).parent as ComponentSetNode)
113114
: null
114-
const defs = parentSet?.componentPropertyDefinitions
115+
const defs = getComponentPropertyDefinitions(parentSet)
115116
let textEntry: { key: string; value: string } | null = null
116117
let textCount = 0
117118
if (defs) {
@@ -162,8 +163,7 @@ export function generateComponentUsage(node: SceneNode): string | null {
162163
}
163164

164165
if (node.type === 'COMPONENT_SET') {
165-
const defs = (node as ComponentSetNode).componentPropertyDefinitions
166-
if (!defs) return `<${componentName} />`
166+
const defs = getComponentPropertyDefinitions(node as ComponentSetNode)
167167

168168
const entries: { key: string; value: string; type: string }[] = []
169169
let textEntry: { key: string; value: string } | null = null

src/codegen/Codegen.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { ComponentTree, NodeTree } from './types'
1010
import { checkAssetNode } from './utils/check-asset-node'
1111
import { checkSameColor } from './utils/check-same-color'
1212
import { extractInstanceVariantProps } from './utils/extract-instance-variant-props'
13+
import { getComponentPropertyDefinitions } from './utils/get-component-property-definitions'
1314
import {
1415
getDevupComponentByNode,
1516
getDevupComponentByProps,
@@ -639,10 +640,9 @@ export class Codegen {
639640

640641
// Collect INSTANCE_SWAP and BOOLEAN property definitions for slot/condition detection.
641642
const parentSet = node.parent?.type === 'COMPONENT_SET' ? node.parent : null
642-
const propDefs =
643-
parentSet?.componentPropertyDefinitions ||
644-
node.componentPropertyDefinitions ||
645-
{}
643+
const propDefs = parentSet
644+
? getComponentPropertyDefinitions(parentSet)
645+
: getComponentPropertyDefinitions(node)
646646
const instanceSwapSlots = new Map<string, string>()
647647
const booleanSlots = new Map<string, string>()
648648
const textSlots = new Map<string, string>()
@@ -758,8 +758,9 @@ export class Codegen {
758758
*/
759759
hasViewportVariant(): boolean {
760760
if (this.node.type !== 'COMPONENT_SET') return false
761-
for (const key in (this.node as ComponentSetNode)
762-
.componentPropertyDefinitions) {
761+
for (const key in getComponentPropertyDefinitions(
762+
this.node as ComponentSetNode,
763+
)) {
763764
if (key.toLowerCase() === 'viewport') return true
764765
}
765766
return false

src/codegen/props/selector.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { fmtPct } from '../utils/fmtPct'
2+
import { getComponentPropertyDefinitions } from '../utils/get-component-property-definitions'
23
import { perfEnd, perfStart } from '../utils/perf'
34
import { getProps } from '.'
45

@@ -141,7 +142,8 @@ async function computeSelectorProps(node: ComponentSetNode): Promise<{
141142
variants: Record<string, string>
142143
variantComments: Record<string, string>
143144
}> {
144-
const hasEffect = !!node.componentPropertyDefinitions.effect
145+
const propDefs = getComponentPropertyDefinitions(node)
146+
const hasEffect = !!propDefs.effect
145147
const tSelector = perfStart()
146148
// Pre-filter: only call expensive getProps() on children with non-default effects.
147149
// The effect/trigger check is a cheap property read — skip children that would be
@@ -169,7 +171,7 @@ async function computeSelectorProps(node: ComponentSetNode): Promise<{
169171
variants: Record<string, string>
170172
variantComments: Record<string, string>
171173
} = { props: {}, variants: {}, variantComments: {} }
172-
const defs = node.componentPropertyDefinitions
174+
const defs = propDefs
173175
for (const name in defs) {
174176
if (name === 'effect' || name === 'viewport') continue
175177
const definition = defs[name]
@@ -233,7 +235,8 @@ export async function getSelectorPropsForGroup(
233235
variantFilter: Record<string, string>,
234236
viewportValue?: string,
235237
): Promise<Record<string, object | string>> {
236-
const hasEffect = !!componentSet.componentPropertyDefinitions.effect
238+
const setDefs = getComponentPropertyDefinitions(componentSet)
239+
const hasEffect = !!setDefs.effect
237240
if (!hasEffect) return {}
238241

239242
// Build cache key from componentSet.id + filter + viewport
@@ -269,9 +272,10 @@ async function computeSelectorPropsForGroup(
269272
viewportValue?: string,
270273
): Promise<Record<string, object | string>> {
271274
// Find viewport key if needed
272-
const viewportKey = Object.keys(
273-
componentSet.componentPropertyDefinitions,
274-
).find((key) => key.toLowerCase() === 'viewport')
275+
const groupDefs = getComponentPropertyDefinitions(componentSet)
276+
const viewportKey = Object.keys(groupDefs).find(
277+
(key) => key.toLowerCase() === 'viewport',
278+
)
275279

276280
// Filter components matching the variant filter (and viewport if specified)
277281
const matchingComponents = componentSet.children.filter((child) => {

src/codegen/responsive/ResponsiveCodegen.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from '../props/selector'
77
import { renderComponent, renderNode } from '../render'
88
import type { NodeTree, Props } from '../types'
9+
import { getComponentPropertyDefinitions } from '../utils/get-component-property-definitions'
910
import { paddingLeftMultiline } from '../utils/padding-left-multiline'
1011
import { perfEnd, perfStart } from '../utils/perf'
1112
import {
@@ -399,9 +400,10 @@ export class ResponsiveCodegen {
399400
componentName: string,
400401
): Promise<ReadonlyArray<readonly [string, string]>> {
401402
// Find viewport and effect variant keys
403+
const viewportDefs = getComponentPropertyDefinitions(componentSet)
402404
let viewportKey: string | undefined
403405
let effectKey: string | undefined
404-
for (const key in componentSet.componentPropertyDefinitions) {
406+
for (const key in viewportDefs) {
405407
const lower = key.toLowerCase()
406408
if (lower === 'viewport') viewportKey = key
407409
else if (lower === 'effect') effectKey = key
@@ -413,8 +415,8 @@ export class ResponsiveCodegen {
413415

414416
// Get variants excluding viewport
415417
const variants: Record<string, string> = {}
416-
for (const name in componentSet.componentPropertyDefinitions) {
417-
const definition = componentSet.componentPropertyDefinitions[name]
418+
for (const name in viewportDefs) {
419+
const definition = viewportDefs[name]
418420
const lowerName = name.toLowerCase()
419421
if (lowerName !== 'viewport' && lowerName !== 'effect') {
420422
const sanitizedName = sanitizePropertyName(name)
@@ -550,9 +552,10 @@ export class ResponsiveCodegen {
550552
const tTotal = perfStart()
551553

552554
// Find viewport and effect variant keys
555+
const variantDefs = getComponentPropertyDefinitions(componentSet)
553556
let viewportKey: string | undefined
554557
let effectKey: string | undefined
555-
for (const key in componentSet.componentPropertyDefinitions) {
558+
for (const key in variantDefs) {
556559
const lower = key.toLowerCase()
557560
if (lower === 'viewport') viewportKey = key
558561
else if (lower === 'effect') effectKey = key
@@ -563,8 +566,8 @@ export class ResponsiveCodegen {
563566
const variants: Record<string, string> = {}
564567
// Map from original name to sanitized name
565568
const variantKeyToSanitized: Record<string, string> = {}
566-
for (const name in componentSet.componentPropertyDefinitions) {
567-
const definition = componentSet.componentPropertyDefinitions[name]
569+
for (const name in variantDefs) {
570+
const definition = variantDefs[name]
568571
if (definition.type === 'VARIANT') {
569572
const lowerName = name.toLowerCase()
570573
// Exclude both viewport and effect from variant keys
@@ -794,8 +797,9 @@ export class ResponsiveCodegen {
794797
// Collect BOOLEAN and INSTANCE_SWAP props for the interface
795798
// (effect is handled via pseudo-selectors, VARIANT keys don't exist in effect-only path)
796799
const variants: Record<string, string> = {}
797-
for (const name in componentSet.componentPropertyDefinitions) {
798-
const definition = componentSet.componentPropertyDefinitions[name]
800+
const effectDefs = getComponentPropertyDefinitions(componentSet)
801+
for (const name in effectDefs) {
802+
const definition = effectDefs[name]
799803
if (definition.type === 'INSTANCE_SWAP') {
800804
variants[sanitizePropertyName(name)] = 'React.ReactNode'
801805
} else if (definition.type === 'BOOLEAN') {
@@ -831,8 +835,9 @@ export class ResponsiveCodegen {
831835
}
832836

833837
// Check if componentSet has effect variant (pseudo-selector)
838+
const groupVariantDefs = getComponentPropertyDefinitions(componentSet)
834839
let hasEffect = false
835-
for (const key in componentSet.componentPropertyDefinitions) {
840+
for (const key in groupVariantDefs) {
836841
if (key.toLowerCase() === 'effect') {
837842
hasEffect = true
838843
break
@@ -905,7 +910,7 @@ export class ResponsiveCodegen {
905910
if (hasEffect) {
906911
const effectValue =
907912
variantProps[
908-
Object.keys(componentSet.componentPropertyDefinitions).find(
913+
Object.keys(groupVariantDefs).find(
909914
(k) => k.toLowerCase() === 'effect',
910915
) || ''
911916
]

src/codegen/utils/__tests__/extract-instance-variant-props.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,20 @@ describe('extractInstanceVariantProps', () => {
151151
expect(result.Viewport).toBeUndefined()
152152
})
153153

154+
test('returns empty object when componentProperties getter throws', () => {
155+
const node = {
156+
get componentProperties(): never {
157+
throw new Error(
158+
'in get_componentProperties: Component set for node has existing errors',
159+
)
160+
},
161+
} as unknown as InstanceNode
162+
163+
const result = extractInstanceVariantProps(node)
164+
165+
expect(result).toEqual({})
166+
})
167+
154168
test('filters out both effect and viewport but keeps other variants', () => {
155169
const node = {
156170
componentProperties: {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, expect, test } from 'bun:test'
2+
import { getComponentPropertyDefinitions } from '../get-component-property-definitions'
3+
4+
describe('getComponentPropertyDefinitions', () => {
5+
test('returns definitions from a valid node', () => {
6+
const defs = {
7+
status: {
8+
type: 'VARIANT',
9+
defaultValue: 'active',
10+
variantOptions: ['active', 'inactive'],
11+
},
12+
}
13+
const node = {
14+
componentPropertyDefinitions: defs,
15+
} as unknown as ComponentSetNode
16+
17+
expect(getComponentPropertyDefinitions(node)).toBe(
18+
defs as ComponentPropertyDefinitions,
19+
)
20+
})
21+
22+
test('returns empty object when node is null', () => {
23+
expect(getComponentPropertyDefinitions(null)).toEqual({})
24+
})
25+
26+
test('returns empty object when node is undefined', () => {
27+
expect(getComponentPropertyDefinitions(undefined)).toEqual({})
28+
})
29+
30+
test('returns empty object when getter throws', () => {
31+
const node = {
32+
get componentPropertyDefinitions(): never {
33+
throw new Error(
34+
'in get_componentPropertyDefinitions: Component set has existing errors',
35+
)
36+
},
37+
} as unknown as ComponentSetNode
38+
39+
expect(getComponentPropertyDefinitions(node)).toEqual({})
40+
})
41+
})

src/codegen/utils/extract-instance-variant-props.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,20 @@ export function extractInstanceVariantProps(
3030
): Record<string, unknown> {
3131
const variantProps: Record<string, unknown> = {}
3232

33-
if (!node.componentProperties) {
33+
let componentProperties: InstanceNode['componentProperties']
34+
try {
35+
componentProperties = node.componentProperties
36+
} catch {
37+
// Figma throws when the component set has validation errors
38+
// (e.g. duplicate variant names, missing properties).
3439
return variantProps
3540
}
3641

37-
for (const [key, prop] of Object.entries(node.componentProperties)) {
42+
if (!componentProperties) {
43+
return variantProps
44+
}
45+
46+
for (const [key, prop] of Object.entries(componentProperties)) {
3847
if (isReservedVariantKey(key)) continue
3948
const sanitizedKey = sanitizePropertyName(key)
4049
if (prop.type === 'VARIANT') {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Safely access componentPropertyDefinitions on a node.
3+
* Figma's getter throws when the component set has validation errors
4+
* (e.g. duplicate variant names, missing properties).
5+
* Returns an empty object on error so callers can iterate safely.
6+
*/
7+
export function getComponentPropertyDefinitions(
8+
node: ComponentSetNode | ComponentNode | null | undefined,
9+
): ComponentSetNode['componentPropertyDefinitions'] {
10+
if (!node) {
11+
return {} as ComponentSetNode['componentPropertyDefinitions']
12+
}
13+
try {
14+
return (
15+
node.componentPropertyDefinitions ||
16+
({} as ComponentSetNode['componentPropertyDefinitions'])
17+
)
18+
} catch {
19+
return {} as ComponentSetNode['componentPropertyDefinitions']
20+
}
21+
}

0 commit comments

Comments
 (0)