Skip to content

Commit

Permalink
Code Connect v0.2.1
Browse files Browse the repository at this point in the history
  • Loading branch information
figma-bot committed Jun 17, 2024
1 parent 024dc61 commit 3388b6c
Show file tree
Hide file tree
Showing 37 changed files with 176 additions and 121 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# Code Connect v0.2.1 (17th June 2024)

## Fixed

### React
- Fixed a bug in v0.2.0 where source paths for components could be incorrect
- Fixed a bug in v0.2.0 where Code Connect files using the new prop types failed to validate

### SwiftUI
- Fixed parsing of Code Connect files using `@FigmaChildren` annotations

# Code Connect v0.2.0 (14th June 2024)

## Breaking changes
Expand Down
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@figma/code-connect",
"version": "0.2.0",
"version": "0.2.1",
"description": "A tool for connecting your design system components in code with your design system in Figma",
"keywords": [],
"author": "Figma",
Expand Down
4 changes: 2 additions & 2 deletions cli/src/common/figma_connect.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { FigmaConnectLink } from './api'
import { Intrinsic, IntrinsicBase } from './intrinsics'
import { Intrinsic } from './intrinsics'

export type BaseCodeConnectObject = {
figmaNode: string
component?: string
variant?: Record<string, any>
template: string
templateData: {
props?: Record<string, IntrinsicBase>
props?: Record<string, Intrinsic>
imports?: string[]
nestable?: boolean
}
Expand Down
3 changes: 1 addition & 2 deletions cli/src/connect/parser_executable_types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { z } from 'zod'
import { BaseCodeConnectObject } from '../common/figma_connect'
import { IntrinsicBase } from '../common/intrinsics'

export type ParseRequestPayload = {
mode: 'PARSE'
Expand Down Expand Up @@ -84,7 +83,7 @@ export const ParseResponsePayload = z.object({
// TODO We could look to extract this from the template somehow instead,
// (e.g. run it with figma.properties.* stubbed to record accesses) to
// avoid needing this duplication.
props: z.record(z.object({ kind: z.string(), args: z.any() }) as z.ZodType<IntrinsicBase>),
props: z.record(z.object({ kind: z.string(), args: z.any() }) as any),
// Optional array of imports for this component. These are prepended
// to the example code, but it's useful to keep them separate e.g. if
// we ever want to auto-insert imports in VS Code. If more control
Expand Down
12 changes: 7 additions & 5 deletions cli/src/connect/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,8 @@ export function getRemoteFileUrl(filePath: string, repoURL?: string) {
return ''
}

filePath = filePath.replaceAll(path.sep, '/')

let url = repoURL.trim()
if (url.startsWith('git@')) {
url = url.replace(':', '/')
Expand All @@ -370,16 +372,16 @@ export function getRemoteFileUrl(filePath: string, repoURL?: string) {
// so we need to find the relative path of the file to the root of the repo
// and append that to the remote URL
const repoAbsPath = getGitRepoAbsolutePath(filePath)
// Windows uses \ as the path separator, so replace with /
.replaceAll(path.sep, '/')

const defaultBranch = getGitRepoDefaultBranchName(repoAbsPath)
const index = filePath.indexOf(repoAbsPath)
if (index === -1) {
return ''
}

const relativeFilePath = filePath
.substring(index + repoAbsPath.length)
// Windows uses \ as the path separator, so replace with /
.replaceAll('\\', '/')
const relativeFilePath = filePath.substring(index + repoAbsPath.length)

return `${url}/blob/${defaultBranch}${relativeFilePath}`
}
Expand All @@ -388,7 +390,7 @@ export function getStorybookUrl(filePath: string, storybookUrl: string) {
// the folder of the git repo on disk could be named differently,
// so we need to find the relative path of the file to the root of the repo
// and append that to the remote URL
const repoAbsPath = getGitRepoAbsolutePath(filePath)
const repoAbsPath = getGitRepoAbsolutePath(filePath).replaceAll(path.sep, '/')
const index = filePath.indexOf(repoAbsPath)
if (index === -1) {
return ''
Expand Down
31 changes: 19 additions & 12 deletions cli/src/connect/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function validateProps(doc: CodeConnectJSON, document: any): boolean {
const codeConnectProps = Object.keys(doc.templateData.props ?? {})

for (let i = 0; i < codeConnectProps.length; i++) {
const codeConnectProp: any = doc.templateData?.props[codeConnectProps[i]]
const codeConnectProp = doc.templateData?.props[codeConnectProps[i]]
if (codeConnectProp.kind === 'children') {
const codeConnectLayerNames = codeConnectProp.args.layers
// Get all layer names in the figma doc
Expand All @@ -89,7 +89,8 @@ function validateProps(doc: CodeConnectJSON, document: any): boolean {
getLayerNames(document)
// And make sure that the layer names in the code connect file are present in the figma doc
for (const codeConnectLayerName of codeConnectLayerNames) {
if (!figmaLayerNames.includes(codeConnectLayerName)) {
const regex = new RegExp('^' + codeConnectLayerName.replace('*', '.*'))
if (figmaLayerNames.every((name) => !regex.test(name))) {
logger.error(
`Validation failed for ${doc.component} (${doc.figmaNode}): The layer "${codeConnectLayerName}" does not exist on the Figma component`,
)
Expand All @@ -98,18 +99,24 @@ function validateProps(doc: CodeConnectJSON, document: any): boolean {
}
continue
}
const codeConnectFigmaPropName = codeConnectProp?.args?.figmaPropName

if (
!document.componentPropertyDefinitions ||
!Object.keys(document.componentPropertyDefinitions).find((figmaProp) =>
propMatches(figmaProp, codeConnectFigmaPropName, document.componentPropertyDefinitions),
)
codeConnectProp.kind === 'boolean' ||
codeConnectProp.kind === 'enum' ||
codeConnectProp.kind === 'string'
) {
logger.error(
`Validation failed for ${doc.component} (${doc.figmaNode}): The property "${codeConnectFigmaPropName}" does not exist on the Figma component`,
)
propsValid = false
const codeConnectFigmaPropName = codeConnectProp?.args?.figmaPropName

if (
!document.componentPropertyDefinitions ||
!Object.keys(document.componentPropertyDefinitions).find((figmaProp) =>
propMatches(figmaProp, codeConnectFigmaPropName, document.componentPropertyDefinitions),
)
) {
logger.error(
`Validation failed for ${doc.component} (${doc.figmaNode}): The property "${codeConnectFigmaPropName}" does not exist on the Figma component`,
)
propsValid = false
}
}
}

Expand Down
44 changes: 23 additions & 21 deletions cli/src/connect/wizard/run_wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ async function askQuestionWithExitConfirmation<T extends string = string>(

function formatComponentTitle(componentName: string, path: string, pad: number) {
const nameLabel = `${chalk.dim('Figma component:')} ${componentName.padEnd(pad, ' ')}`
const linkedLabel = `${chalk.dim('Linked to:')} ${path ?? '-'}`
const linkedLabel = `↔️ ${path ?? '-'}`
return `${nameLabel} ${linkedLabel}`
}

Expand Down Expand Up @@ -167,7 +167,7 @@ async function runManualLinking({
const { nodeId } = await prompts({
type: 'select',
name: 'nodeId',
message: `Pick a link to edit (or press ${chalk.green('esc')} to continue)`,
message: `Select a link to edit (Press ${chalk.green('esc')} when you're ready to continue on)`,
choices: getComponentChoicesForPrompt(
unconnectedComponents,
linkedNodeIdsToPaths,
Expand All @@ -183,7 +183,7 @@ async function runManualLinking({
const { pathToComponent } = await prompts({
type: 'autocomplete',
name: 'pathToComponent',
message: 'Choose a path to your component (type to filter results)',
message: 'Choose a path to your code component (type to filter results)',
choices: pathChoices,
// default suggest uses .startsWith(input) which isn't very useful for full paths
suggest: (input, choices) =>
Expand Down Expand Up @@ -215,7 +215,7 @@ async function runManualLinkingWithConfirmation(manualLinkingArgs: ManualLinking
const { outputDirectory } = await askQuestionWithExitConfirmation({
type: 'text',
name: 'outputDirectory',
message: `By default, Code Connect files are created alongside the component files they link to. Press ${chalk.green('enter')} to proceed or enter an output directory.`,
message: `What directory should Code Connect files be created in? (Press ${chalk.green('enter')} to co-locate your files alongside your component files)`,
})
hasAskedOutDirQuestion = true
outDir = outputDirectory
Expand Down Expand Up @@ -245,14 +245,14 @@ async function runManualLinkingWithConfirmation(manualLinkingArgs: ManualLinking
const { confirmation } = await askQuestionWithExitConfirmation({
type: 'select',
name: 'confirmation',
message: `You're about to create ${chalk.green(linkedNodes.length)} Code Connect file${linkedNodes.length == 1 ? '' : 's'}. Continue?`,
message: `You're ready to create ${chalk.green(linkedNodes.length)} Code Connect file${linkedNodes.length == 1 ? '' : 's'}. Continue?`,
choices: [
{
title: 'Create',
title: 'Create files',
value: 'create',
},
{
title: 'Back to edit',
title: 'Back to editing',
value: 'backToEdit',
},
],
Expand Down Expand Up @@ -444,7 +444,7 @@ async function askForTopLevelDirectoryOrDetermineFromConfig({
if (!hasConfigFile) {
const { componentDirectory } = await askQuestionWithExitConfirmation({
type: 'text',
message: `Which top-level directory contains the code to be linked to your Figma design system? (Press ${chalk.green('enter')} to use current directory)`,
message: `Which top-level directory contains the code to be connected to your Figma design system? (Press ${chalk.green('enter')} to use current directory)`,
name: 'componentDirectory',
format: (val) => val || process.cwd(),
validate: (value) => {
Expand Down Expand Up @@ -495,12 +495,13 @@ export async function runWizard(cmd: BaseCommand) {
)
logger.info(
boxen(
`${chalk.bold(`Welcome to ${chalk.green('Code Connect')}.`)}\n\n` +
`Code Connect helps you link your Figma design system to your\n` +
`codebase, so you can see code for your components in Figma Dev Mode.\n` +
`Find out more at ${chalk.cyan('figma.com/developers/code-connect')}.\n\n` +
`${chalk.red.bold('Important: ')}This CLI will create and modify Code Connect files.\n` +
`Please ensure you've committed any changes.`,
`${chalk.bold(`Welcome to ${chalk.green('Code Connect')}`)}\n\n` +
`Follow a few simple steps to connect your Figma design system to your codebase.\n` +
`When you're done, you'll be able to see your component code while inspecting in\n` +
`Figma's Dev Mode.\n\n` +
`Learn more at ${chalk.cyan('https://www.figma.com/developers/code-connect')}.\n\n` +
`${chalk.red.bold('Note: ')}This process will create and modify Code Connect files. Make sure you've\n` +
`committed necessary changes in your codebase first.`,
{
padding: 1,
margin: 1,
Expand Down Expand Up @@ -551,7 +552,7 @@ export async function runWizard(cmd: BaseCommand) {

const { figmaFileUrl } = await askQuestionWithExitConfirmation({
type: 'text',
message: 'What is the URL of the Figma file which contains your design system?',
message: 'What is the URL of the Figma file containing your design system library?',
name: 'figmaFileUrl',
})

Expand All @@ -568,7 +569,8 @@ export async function runWizard(cmd: BaseCommand) {
const { createConfigFile } = await askQuestionWithExitConfirmation({
type: 'select',
name: 'createConfigFile',
message: `Would you like to generate a Code Connect config file from your provided answers?`,
message:
"It looks like you don't have a Code Connect config file. Would you like to generate one now from your provided answers?",
choices: [
{
title: 'Yes',
Expand Down Expand Up @@ -604,13 +606,13 @@ export async function runWizard(cmd: BaseCommand) {

logger.info(
boxen(
`${chalk.bold(`Connect your components`)}\n\n` +
`Choose how your Figma components should connect to your codebase. Once confirmed,\n` +
`Code Connect files will be created for all new links.\n\n` +
`${chalk.bold(`Connecting your components`)}\n\n` +
`${chalk.green(
`${chalk.bold(Object.keys(linkedNodeIdsToPaths).length)} ${Object.keys(linkedNodeIdsToPaths).length === 1 ? 'component was automatically connected based on its name' : 'components were automatically connected based on their names'}`,
`${chalk.bold(Object.keys(linkedNodeIdsToPaths).length)} ${Object.keys(linkedNodeIdsToPaths).length === 1 ? 'component was automatically matched based on its name' : 'components were automatically matched based on their names'}`,
)}\n` +
`${chalk.yellow(`${chalk.bold(unconnectedComponents.length)} ${unconnectedComponents.length === 1 ? 'component has not been connected' : 'components have not been connected'}`)}`,
`${chalk.yellow(`${chalk.bold(unconnectedComponents.length)} ${unconnectedComponents.length === 1 ? 'component has not been matched' : 'components have not been matched'}`)}\n\n` +
`Match up Figma components with their code definitions. When you're finished, you\n` +
`can specify the directory you want to create Code Connect files in.`,
{
padding: 1,
margin: 1,
Expand Down
2 changes: 1 addition & 1 deletion cli/src/react/__test__/ButtonArrowFunction.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import figma from '../..'

import { ButtonArrowFunction } from './TestComponents'
import { ButtonArrowFunction } from './components/TestComponents'

figma.connect(ButtonArrowFunction, 'ui/button')
2 changes: 1 addition & 1 deletion cli/src/react/__test__/ButtonTest.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import figma from '../..'
import { Button } from './TestComponents'
import { Button } from './components/TestComponents'

figma.connect(Button, 'ui/button', {
example: () => <Button>Click me</Button>,
Expand Down
2 changes: 1 addition & 1 deletion cli/src/react/__test__/ChildInstances.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react'
import { figma } from '../..'
import { Button } from './TestComponents'
import { Button } from './components/TestComponents'

figma.connect(Button, 'instanceSwap', {
props: {
Expand Down
2 changes: 1 addition & 1 deletion cli/src/react/__test__/ComponentWithLogic.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import figma from '../..'
import { Button } from './TestComponents'
import { Button } from './components/TestComponents'

figma.connect(Button, 'ui/button', {
example: () => {
Expand Down
2 changes: 1 addition & 1 deletion cli/src/react/__test__/CustomImports.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import figma from '../..'
import { Button } from './TestComponents'
import { Button } from './components/TestComponents'

figma.connect(Button, 'ui/button', {
example: () => <Button>Click me</Button>,
Expand Down
2 changes: 1 addition & 1 deletion cli/src/react/__test__/DefaultImport.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import figma from '../..'
import RenamedButton from './TestComponents'
import RenamedButton from './components/TestComponents'

figma.connect(RenamedButton, 'ui/button')
2 changes: 1 addition & 1 deletion cli/src/react/__test__/EnumLikeBooleanFalseProp.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import figma from '../..'
import { Button } from './TestComponents'
import { Button } from './components/TestComponents'

figma.connect(Button, 'test', {
props: {
Expand Down
2 changes: 1 addition & 1 deletion cli/src/react/__test__/ForwardRefComponent.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import figma from '../..'
import { ForwardRefButton } from './TestComponents'
import { ForwardRefButton } from './components/TestComponents'

figma.connect(ForwardRefButton, 'ui/button')
2 changes: 1 addition & 1 deletion cli/src/react/__test__/MemoizedComponent.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import figma from '../..'
import { MemoButton } from './TestComponents'
import { MemoButton } from './components/TestComponents'

figma.connect(MemoButton, 'ui/button', {
example: () => <MemoButton>Click me</MemoButton>,
Expand Down
2 changes: 1 addition & 1 deletion cli/src/react/__test__/NamespacedComponent.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react'
import figma from '../..'
import { NamespacedComponents } from './TestComponents'
import { NamespacedComponents } from './components/TestComponents'

figma.connect(NamespacedComponents.Button, 'ui/button', {
example: () => <NamespacedComponents.Button />,
Expand Down
2 changes: 1 addition & 1 deletion cli/src/react/__test__/NoProps.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import figma from '../..'
import { ComponentWithoutProps } from './TestComponents'
import { ComponentWithoutProps } from './components/TestComponents'

figma.connect(ComponentWithoutProps, 'ui/button')
2 changes: 1 addition & 1 deletion cli/src/react/__test__/PropMappings.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import figma from '../..'
import { Button } from './TestComponents'
import { Button } from './components/TestComponents'

figma.connect(Button, 'propsInline', {
props: {
Expand Down
15 changes: 15 additions & 0 deletions cli/src/react/__test__/TestTopLevelComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react'

interface ButtonProps {
width?: string
variant?: string
disabled?: boolean
iconLead?: string
children?: React.ReactNode
icon?: string
onClick?: any
}

export function Button({ width, variant, disabled, children }: ButtonProps) {
return <button>{children}</button>
}
7 changes: 7 additions & 0 deletions cli/src/react/__test__/TopLevelComponent.figma.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react'
import figma from '../..'
import { Button } from './TestTopLevelComponent'

figma.connect(Button, 'ui/button', {
example: () => <Button>Click me</Button>,
})
2 changes: 1 addition & 1 deletion cli/src/react/__test__/VariableRefFigmaNode.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import figma from '../..'
import Button from './TestComponents'
import Button from './components/TestComponents'

const buttonURL = 'identifierAsUrl'

Expand Down
2 changes: 1 addition & 1 deletion cli/src/react/__test__/VariantRestriction.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import figma from '../..'
import { Button } from './TestComponents'
import { Button } from './components/TestComponents'

figma.connect(Button, 'ui/button', {
example: () => <Button>Click me</Button>,
Expand Down
File renamed without changes.
Loading

0 comments on commit 3388b6c

Please sign in to comment.