Skip to content

Commit 258f9e8

Browse files
committed
Add case
1 parent 77b091b commit 258f9e8

File tree

2 files changed

+334
-12
lines changed

2 files changed

+334
-12
lines changed

src/__tests__/code.test.ts

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,332 @@ describe('extractImports', () => {
214214
expect(result).toContain('Box')
215215
})
216216
})
217+
218+
describe('extractCustomComponentImports', () => {
219+
it('should extract custom component imports', () => {
220+
const result = codeModule.extractCustomComponentImports([
221+
['MyComponent', '<Box><CustomButton /><CustomInput /></Box>'],
222+
])
223+
expect(result).toContain('CustomButton')
224+
expect(result).toContain('CustomInput')
225+
expect(result).not.toContain('Box')
226+
expect(result).not.toContain('MyComponent')
227+
})
228+
229+
it('should not include devup-ui components', () => {
230+
const result = codeModule.extractCustomComponentImports([
231+
[
232+
'MyComponent',
233+
'<Box><Flex><VStack><CustomCard /></VStack></Flex></Box>',
234+
],
235+
])
236+
expect(result).toContain('CustomCard')
237+
expect(result).not.toContain('Box')
238+
expect(result).not.toContain('Flex')
239+
expect(result).not.toContain('VStack')
240+
})
241+
242+
it('should return empty array when no custom components', () => {
243+
const result = codeModule.extractCustomComponentImports([
244+
['MyComponent', '<Box><Flex><Text>Hello</Text></Flex></Box>'],
245+
])
246+
expect(result).toEqual([])
247+
})
248+
249+
it('should sort custom components alphabetically', () => {
250+
const result = codeModule.extractCustomComponentImports([
251+
['MyComponent', '<Box><Zebra /><Apple /><Mango /></Box>'],
252+
])
253+
expect(result).toEqual(['Apple', 'Mango', 'Zebra'])
254+
})
255+
256+
it('should handle multiple components with same custom component', () => {
257+
const result = codeModule.extractCustomComponentImports([
258+
['ComponentA', '<Box><SharedButton /></Box>'],
259+
['ComponentB', '<Flex><SharedButton /></Flex>'],
260+
])
261+
expect(result).toEqual(['SharedButton'])
262+
})
263+
264+
it('should handle nested custom components', () => {
265+
const result = codeModule.extractCustomComponentImports([
266+
['Parent', '<Box><ChildA><ChildB><ChildC /></ChildB></ChildA></Box>'],
267+
])
268+
expect(result).toContain('ChildA')
269+
expect(result).toContain('ChildB')
270+
expect(result).toContain('ChildC')
271+
})
272+
})
273+
274+
describe('registerCodegen with viewport variant', () => {
275+
type CodegenHandler = (event: {
276+
node: SceneNode
277+
language: string
278+
}) => Promise<unknown[]>
279+
280+
it('should generate responsive component codes for COMPONENT_SET with viewport variant', async () => {
281+
let capturedHandler: CodegenHandler | null = null
282+
283+
const figmaMock = {
284+
editorType: 'dev',
285+
mode: 'codegen',
286+
command: 'noop',
287+
codegen: {
288+
on: (_event: string, handler: CodegenHandler) => {
289+
capturedHandler = handler
290+
},
291+
},
292+
closePlugin: mock(() => {}),
293+
} as unknown as typeof figma
294+
295+
codeModule.registerCodegen(figmaMock)
296+
297+
expect(capturedHandler).not.toBeNull()
298+
if (capturedHandler === null) throw new Error('Handler not captured')
299+
300+
const componentSetNode = {
301+
type: 'COMPONENT_SET',
302+
name: 'ResponsiveButton',
303+
visible: true,
304+
componentPropertyDefinitions: {
305+
viewport: {
306+
type: 'VARIANT',
307+
defaultValue: 'desktop',
308+
variantOptions: ['mobile', 'desktop'],
309+
},
310+
},
311+
children: [
312+
{
313+
type: 'COMPONENT',
314+
name: 'viewport=mobile',
315+
visible: true,
316+
variantProperties: { viewport: 'mobile' },
317+
children: [],
318+
layoutMode: 'VERTICAL',
319+
width: 320,
320+
height: 100,
321+
},
322+
{
323+
type: 'COMPONENT',
324+
name: 'viewport=desktop',
325+
visible: true,
326+
variantProperties: { viewport: 'desktop' },
327+
children: [],
328+
layoutMode: 'HORIZONTAL',
329+
width: 1200,
330+
height: 100,
331+
},
332+
],
333+
defaultVariant: {
334+
type: 'COMPONENT',
335+
name: 'viewport=desktop',
336+
visible: true,
337+
variantProperties: { viewport: 'desktop' },
338+
children: [],
339+
},
340+
} as unknown as SceneNode
341+
342+
const handler = capturedHandler as CodegenHandler
343+
const result = await handler({
344+
node: componentSetNode,
345+
language: 'devup-ui',
346+
})
347+
348+
// Should include responsive components result
349+
const responsiveResult = result.find(
350+
(r: unknown) =>
351+
typeof r === 'object' &&
352+
r !== null &&
353+
'title' in r &&
354+
(r as { title: string }).title.includes('Responsive'),
355+
)
356+
expect(responsiveResult).toBeDefined()
357+
})
358+
359+
it('should generate responsive code for node with parent SECTION', async () => {
360+
let capturedHandler: CodegenHandler | null = null
361+
362+
const figmaMock = {
363+
editorType: 'dev',
364+
mode: 'codegen',
365+
command: 'noop',
366+
codegen: {
367+
on: (_event: string, handler: CodegenHandler) => {
368+
capturedHandler = handler
369+
},
370+
},
371+
closePlugin: mock(() => {}),
372+
} as unknown as typeof figma
373+
374+
codeModule.registerCodegen(figmaMock)
375+
376+
expect(capturedHandler).not.toBeNull()
377+
if (capturedHandler === null) throw new Error('Handler not captured')
378+
379+
// Create a SECTION node with children of different widths
380+
const sectionNode = {
381+
type: 'SECTION',
382+
name: 'ResponsiveSection',
383+
visible: true,
384+
children: [
385+
{
386+
type: 'FRAME',
387+
name: 'MobileFrame',
388+
visible: true,
389+
width: 375,
390+
height: 200,
391+
children: [],
392+
layoutMode: 'VERTICAL',
393+
},
394+
{
395+
type: 'FRAME',
396+
name: 'DesktopFrame',
397+
visible: true,
398+
width: 1200,
399+
height: 200,
400+
children: [],
401+
layoutMode: 'HORIZONTAL',
402+
},
403+
],
404+
}
405+
406+
// Create a child node that has the SECTION as parent
407+
const childNode = {
408+
type: 'FRAME',
409+
name: 'ChildFrame',
410+
visible: true,
411+
width: 375,
412+
height: 100,
413+
children: [],
414+
layoutMode: 'VERTICAL',
415+
parent: sectionNode,
416+
} as unknown as SceneNode
417+
418+
const handler = capturedHandler as CodegenHandler
419+
const result = await handler({
420+
node: childNode,
421+
language: 'devup-ui',
422+
})
423+
424+
// Should include responsive result from parent section
425+
const responsiveResult = result.find(
426+
(r: unknown) =>
427+
typeof r === 'object' &&
428+
r !== null &&
429+
'title' in r &&
430+
(r as { title: string }).title.includes('Responsive'),
431+
)
432+
expect(responsiveResult).toBeDefined()
433+
})
434+
435+
it('should generate CLI with custom component imports', async () => {
436+
let capturedHandler: CodegenHandler | null = null
437+
438+
const figmaMock = {
439+
editorType: 'dev',
440+
mode: 'codegen',
441+
command: 'noop',
442+
codegen: {
443+
on: (_event: string, handler: CodegenHandler) => {
444+
capturedHandler = handler
445+
},
446+
},
447+
closePlugin: mock(() => {}),
448+
} as unknown as typeof figma
449+
450+
codeModule.registerCodegen(figmaMock)
451+
452+
expect(capturedHandler).not.toBeNull()
453+
if (capturedHandler === null) throw new Error('Handler not captured')
454+
455+
// Create a custom component that will be referenced
456+
const customComponent = {
457+
type: 'COMPONENT',
458+
name: 'CustomButton',
459+
visible: true,
460+
children: [],
461+
width: 100,
462+
height: 40,
463+
layoutMode: 'NONE',
464+
componentPropertyDefinitions: {},
465+
parent: null,
466+
}
467+
468+
// Create an INSTANCE referencing the custom component
469+
const instanceNode = {
470+
type: 'INSTANCE',
471+
name: 'CustomButton',
472+
visible: true,
473+
width: 100,
474+
height: 40,
475+
getMainComponentAsync: async () => customComponent,
476+
}
477+
478+
// Create a COMPONENT that contains the INSTANCE
479+
const componentNode = {
480+
type: 'COMPONENT',
481+
name: 'MyComponent',
482+
visible: true,
483+
children: [instanceNode],
484+
width: 200,
485+
height: 100,
486+
layoutMode: 'VERTICAL',
487+
componentPropertyDefinitions: {},
488+
reactions: [],
489+
parent: null,
490+
} as unknown as SceneNode
491+
492+
// Create COMPONENT_SET parent with proper children array
493+
const componentSetNode = {
494+
type: 'COMPONENT_SET',
495+
name: 'MyComponentSet',
496+
componentPropertyDefinitions: {},
497+
children: [componentNode],
498+
defaultVariant: componentNode,
499+
reactions: [],
500+
}
501+
502+
// Set parent reference
503+
;(componentNode as { parent: unknown }).parent = componentSetNode
504+
505+
const handler = capturedHandler as CodegenHandler
506+
const result = await handler({
507+
node: componentNode,
508+
language: 'devup-ui',
509+
})
510+
511+
// Should include CLI outputs
512+
const bashCLI = result.find(
513+
(r: unknown) =>
514+
typeof r === 'object' &&
515+
r !== null &&
516+
'title' in r &&
517+
(r as { title: string }).title.includes('CLI (Bash)'),
518+
)
519+
const powershellCLI = result.find(
520+
(r: unknown) =>
521+
typeof r === 'object' &&
522+
r !== null &&
523+
'title' in r &&
524+
(r as { title: string }).title.includes('CLI (PowerShell)'),
525+
)
526+
527+
expect(bashCLI).toBeDefined()
528+
expect(powershellCLI).toBeDefined()
529+
530+
// Check that custom component import is included (bash escapes quotes)
531+
const bashCode = (bashCLI as { code: string } | undefined)?.code
532+
const powershellCode = (powershellCLI as { code: string } | undefined)?.code
533+
534+
if (bashCode) {
535+
expect(bashCode).toContain(
536+
"import { CustomButton } from \\'@/components/CustomButton\\'",
537+
)
538+
}
539+
if (powershellCode) {
540+
expect(powershellCode).toContain(
541+
"import { CustomButton } from '@/components/CustomButton'",
542+
)
543+
}
544+
})
545+
})

src/code-impl.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,19 +129,12 @@ export function registerCodegen(ctx: typeof figma) {
129129
readonly [string, string]
130130
> = []
131131
if (codegen.hasViewportVariant() && node.type === 'COMPONENT_SET') {
132-
try {
133-
const componentName = getComponentName(node)
134-
responsiveComponentsCodes =
135-
await ResponsiveCodegen.generateViewportResponsiveComponents(
136-
node,
137-
componentName,
138-
)
139-
} catch (e) {
140-
console.error(
141-
'[responsive] Error generating responsive component code:',
142-
e,
132+
const componentName = getComponentName(node)
133+
responsiveComponentsCodes =
134+
await ResponsiveCodegen.generateViewportResponsiveComponents(
135+
node,
136+
componentName,
143137
)
144-
}
145138
}
146139

147140
console.info(`[benchmark] devup-ui end ${Date.now() - time}ms`)

0 commit comments

Comments
 (0)