Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/__tests__/__snapshots__/code.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,57 @@ exports[`registerCodegen should register codegen 2`] = `
`;

exports[`registerCodegen should register codegen 3`] = `[]`;

exports[`registerCodegen should generate responsive code when root node is SECTION 1`] = `
[
{
"code":
"<Box boxSize="100%">
<Box boxSize="100%" />
<Box boxSize="100%" />
</Box>"
,
"language": "TYPESCRIPT",
"title": "ResponsiveSection",
},
{
"code":
"import { Box } from '@devup-ui/react'

export function ResponsiveSection() {
return <Box boxSize="100%" />
}"
,
"language": "TYPESCRIPT",
"title": "ResponsiveSection - Responsive",
},
{
"code":
"mkdir -p src/components

echo 'import { Box } from \\'@devup-ui/react\\'

export function ResponsiveSection() {
return <Box boxSize="100%" />
}' > src/components/ResponsiveSection.tsx"
,
"language": "BASH",
"title": "ResponsiveSection - Responsive CLI (Bash)",
},
{
"code":
"New-Item -ItemType Directory -Force -Path src\\components | Out-Null

@'
import { Box } from '@devup-ui/react'

export function ResponsiveSection() {
return <Box boxSize="100%" />
}
'@ | Out-File -FilePath src\\components\\ResponsiveSection.tsx -Encoding UTF8"
,
"language": "BASH",
"title": "ResponsiveSection - Responsive CLI (PowerShell)",
},
]
`;
174 changes: 174 additions & 0 deletions src/__tests__/code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,53 @@ describe('registerCodegen', () => {
),
).toMatchSnapshot()
})

it('should generate responsive code when root node is SECTION', async () => {
const figmaMock = {
editorType: 'dev',
mode: 'codegen',
command: 'noop',
codegen: { on: mock(() => {}) },
closePlugin: mock(() => {}),
} as unknown as typeof figma

codeModule.registerCodegen(figmaMock)

const sectionNode = {
type: 'SECTION',
name: 'ResponsiveSection',
visible: true,
children: [
{
type: 'FRAME',
name: 'MobileFrame',
visible: true,
width: 375,
height: 200,
children: [],
layoutMode: 'VERTICAL',
},
{
type: 'FRAME',
name: 'DesktopFrame',
visible: true,
width: 1440,
height: 200,
children: [],
layoutMode: 'HORIZONTAL',
},
],
}

const result = await (
figmaMock.codegen.on as ReturnType<typeof mock>
).mock.calls[0][1]({
node: sectionNode,
language: 'devup-ui',
})

expect(result).toMatchSnapshot()
})
})

it('should not register codegen if figma is not defined', async () => {
Expand Down Expand Up @@ -633,4 +680,131 @@ describe('registerCodegen with viewport variant', () => {
)
}
})

it('should generate componentsResponsiveCodes when FRAME contains INSTANCE of COMPONENT_SET with viewport', async () => {
let capturedHandler: CodegenHandler | null = null

const figmaMock = {
editorType: 'dev',
mode: 'codegen',
command: 'noop',
codegen: {
on: (_event: string, handler: CodegenHandler) => {
capturedHandler = handler
},
},
closePlugin: mock(() => {}),
} as unknown as typeof figma

codeModule.registerCodegen(figmaMock)

expect(capturedHandler).not.toBeNull()
if (capturedHandler === null) throw new Error('Handler not captured')

// Create a COMPONENT_SET with viewport variants
const componentSetNode = {
type: 'COMPONENT_SET',
name: 'ResponsiveButton',
visible: true,
componentPropertyDefinitions: {
viewport: {
type: 'VARIANT',
defaultValue: 'desktop',
variantOptions: ['mobile', 'desktop'],
},
},
children: [] as unknown[],
defaultVariant: null as unknown,
}

// Create COMPONENT children for the COMPONENT_SET
const mobileComponent = {
type: 'COMPONENT',
name: 'viewport=mobile',
visible: true,
variantProperties: { viewport: 'mobile' },
children: [],
layoutMode: 'VERTICAL',
width: 320,
height: 100,
parent: componentSetNode,
componentPropertyDefinitions: {},
reactions: [],
}

const desktopComponent = {
type: 'COMPONENT',
name: 'viewport=desktop',
visible: true,
variantProperties: { viewport: 'desktop' },
children: [],
layoutMode: 'HORIZONTAL',
width: 1200,
height: 100,
parent: componentSetNode,
componentPropertyDefinitions: {},
reactions: [],
}

componentSetNode.children = [mobileComponent, desktopComponent]
componentSetNode.defaultVariant = desktopComponent

// Create an INSTANCE that references the desktop component
const instanceNode = {
type: 'INSTANCE',
name: 'ResponsiveButton',
visible: true,
width: 1200,
height: 100,
getMainComponentAsync: async () => desktopComponent,
}

// Create a FRAME that contains the INSTANCE
const frameNode = {
type: 'FRAME',
name: 'MyFrame',
visible: true,
children: [instanceNode],
width: 1400,
height: 200,
layoutMode: 'VERTICAL',
} as unknown as SceneNode

const handler = capturedHandler as CodegenHandler
const result = await handler({
node: frameNode,
language: 'devup-ui',
})

// Should include Components Responsive results
const responsiveResult = result.find(
(r: unknown) =>
typeof r === 'object' &&
r !== null &&
'title' in r &&
(r as { title: string }).title === 'MyFrame - Components Responsive',
)
expect(responsiveResult).toBeDefined()

// Should also include CLI results for Components Responsive
const bashCLI = result.find(
(r: unknown) =>
typeof r === 'object' &&
r !== null &&
'title' in r &&
(r as { title: string }).title ===
'MyFrame - Components Responsive CLI (Bash)',
)
expect(bashCLI).toBeDefined()

const powershellCLI = result.find(
(r: unknown) =>
typeof r === 'object' &&
r !== null &&
'title' in r &&
(r as { title: string }).title ===
'MyFrame - Components Responsive CLI (PowerShell)',
)
expect(powershellCLI).toBeDefined()
})
})
96 changes: 91 additions & 5 deletions src/code-impl.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Codegen } from './codegen/Codegen'
import { ResponsiveCodegen } from './codegen/responsive/ResponsiveCodegen'
import { nodeProxyTracker } from './codegen/utils/node-proxy'
import { wrapComponent } from './codegen/utils/wrap-component'
import { exportDevup, importDevup } from './commands/devup'
import { exportAssets } from './commands/exportAssets'
import { exportComponents } from './commands/exportComponents'
import { getComponentName } from './utils'
import { toPascal } from './utils/to-pascal'

const DEVUP_COMPONENTS = [
'Center',
Expand Down Expand Up @@ -140,25 +142,88 @@ export function registerCodegen(ctx: typeof figma) {
)
}

// Generate responsive codes for components extracted from the page
let componentsResponsiveCodes: ReadonlyArray<
readonly [string, string]
> = []
if (componentsCodes.length > 0) {
const componentNodes = codegen.getComponentNodes()
const processedComponentSets = new Set<string>()
const responsiveResults: Array<readonly [string, string]> = []

for (const componentNode of componentNodes) {
// Check if the component belongs to a COMPONENT_SET
const parentSet =
componentNode.type === 'COMPONENT' &&
componentNode.parent?.type === 'COMPONENT_SET'
? (componentNode.parent as ComponentSetNode)
: null

if (parentSet && !processedComponentSets.has(parentSet.id)) {
processedComponentSets.add(parentSet.id)
const componentName = getComponentName(parentSet)
const responsiveCodes =
await ResponsiveCodegen.generateVariantResponsiveComponents(
parentSet,
componentName,
)
responsiveResults.push(...responsiveCodes)
}
}
componentsResponsiveCodes = responsiveResults
}

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

// Check if node itself is SECTION or has a parent SECTION
const isNodeSection = ResponsiveCodegen.canGenerateResponsive(node)
const parentSection = ResponsiveCodegen.hasParentSection(node)
const sectionNode = isNodeSection
? (node as SectionNode)
: parentSection
// When parent is Section (not node itself), use Page postfix and export default
const isParentSection = !isNodeSection && parentSection !== null
let responsiveResult: {
title: string
language: 'TYPESCRIPT'
language: 'TYPESCRIPT' | 'BASH'
code: string
}[] = []

if (parentSection) {
if (sectionNode) {
try {
const responsiveCodegen = new ResponsiveCodegen(parentSection)
const responsiveCodegen = new ResponsiveCodegen(sectionNode)
const responsiveCode =
await responsiveCodegen.generateResponsiveCode()
const baseName = toPascal(sectionNode.name)
const sectionComponentName = isParentSection
? `${baseName}Page`
: baseName
const wrappedCode = wrapComponent(
sectionComponentName,
responsiveCode,
{ exportDefault: isParentSection },
)
const sectionCodes: ReadonlyArray<readonly [string, string]> = [
[sectionComponentName, wrappedCode],
]
const importStatement = generateImportStatements(sectionCodes)
const fullCode = importStatement + wrappedCode

responsiveResult = [
{
title: `${parentSection.name} - Responsive`,
title: `${sectionNode.name} - Responsive`,
language: 'TYPESCRIPT' as const,
code: responsiveCode,
code: fullCode,
},
{
title: `${sectionNode.name} - Responsive CLI (Bash)`,
language: 'BASH' as const,
code: generateBashCLI(sectionCodes),
},
{
title: `${sectionNode.name} - Responsive CLI (PowerShell)`,
language: 'BASH' as const,
code: generatePowerShellCLI(sectionCodes),
},
]
} catch (e) {
Expand Down Expand Up @@ -202,6 +267,27 @@ export function registerCodegen(ctx: typeof figma) {
},
] as const)
: []),
...(componentsResponsiveCodes.length > 0
? [
{
title: `${node.name} - Components Responsive`,
language: 'TYPESCRIPT' as const,
code: componentsResponsiveCodes
.map((code) => code[1])
.join('\n\n'),
},
{
title: `${node.name} - Components Responsive CLI (Bash)`,
language: 'BASH' as const,
code: generateBashCLI(componentsResponsiveCodes),
},
{
title: `${node.name} - Components Responsive CLI (PowerShell)`,
language: 'BASH' as const,
code: generatePowerShellCLI(componentsResponsiveCodes),
},
]
: []),
...(responsiveComponentsCodes.length > 0
? [
{
Expand Down
Loading