Skip to content

Commit

Permalink
Updated to 0.1.2
Browse files Browse the repository at this point in the history
GitOrigin-RevId: 1995705d32b831b69ee94975df3413db6767227e
  • Loading branch information
ptomas-figma committed May 15, 2024
1 parent 34703b2 commit e0e48e2
Show file tree
Hide file tree
Showing 124 changed files with 1,187 additions and 380 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ Code Connect is easy to set up, easy to maintain, type-safe, and extensible. Out

To learn how to implement Code Connect for your platform, please navigate to the platform-specific API usage and documentation.

- [React](react/README.md)
- [React](cli/README.md)
- [SwiftUI](swiftui/README.md)
4 changes: 2 additions & 2 deletions react/README.md → cli/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Code Connect (React)

For more information about Code Connect as well as guides for other platforms and frameworks, please [go here](../README.md).
For more information about Code Connect as well as guides for other platforms and frameworks, please [go here](../../README.md).

This documentation will help you connect your React components with Figma components using Code Connect. We'll cover basic setup to display your first connected code snippet, followed by making snippets dynamic by using property mappings. Code Connect for React works as both a standalone implementation and as an integration with existing Storybook files to enable easily maintaining both systems in parallel.

Expand All @@ -22,7 +22,7 @@ npx figma connect create "https://..." --token <auth token>

This will create a Code Connect file with some basic scaffolding for the component you want to connect. By default this file will be called `<component-name>.figma.tsx` based on the name of the component in Figma. However, you may rename this file as you see fit. The scaffolding that is generated is based on the interface of the component in Figma. Depending on how closely this matches your code component you'll need to make some edits to this file before you publish it.

Some CLI commands, like `create`, require a valid [authentication token](https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens) with write permission for the Code Connect scope. You can either pass this via the `--token` flag, or set the `FIGMA_ACCESS_TOKEN` environment variable. The Figma CLI reads this from a `.env` file in the same folder, if it exists.
Some CLI commands, like `create`, require a valid [authentication token](https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens) with write permission for the Code Connect scope as well as the read permission for the File content scope. You can either pass this via the `--token` flag, or set the `FIGMA_ACCESS_TOKEN` environment variable. The Figma CLI reads this from a `.env` file in the same folder, if it exists.

To keep things simple, we're going to start by replacing the contents of the generated file with the most basic Code Connect configuration possible to make sure everything is set up and working as expected. Replace the contents of the file with the following, replacing the `Button` reference with a reference to whatever component you are trying to connect. The object called by `figma.connect` is your Code Connect doc.

Expand Down
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions react/package-lock.json → cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion react/package.json → cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@figma/code-connect",
"version": "0.1.1",
"version": "0.1.2",
"description": "A tool for connecting your design system components in code with your design system in Figma",
"keywords": [],
"author": "Figma",
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ xdescribe('e2e test for `connect` command', () => {
language: 'typescript',
component: 'ReactApiComponent',
source:
'https://github.com/figma/code-connect/tree/master/react/src/__test__/e2e_connect_command/ReactApiComponent.tsx',
'https://github.com/figma/code-connect/tree/master/cli/src/__test__/e2e_connect_command/ReactApiComponent.tsx',
sourceLocation: { line: 13 },
template: '<ReactApiComponent />',
templateData: {
Expand All @@ -31,7 +31,7 @@ xdescribe('e2e test for `connect` command', () => {
{
figmaNode: 'https://figma.com/test',
source:
'https://github.com/figma/code-connect/tree/master/react/src/__test__/e2e_connect_command/StorybookComponent.tsx',
'https://github.com/figma/code-connect/tree/master/cli/src/__test__/e2e_connect_command/StorybookComponent.tsx',
sourceLocation: { line: 7 },
template: '<StorybookComponent disabled={false}>Hello</StorybookComponent>',
templateData: { imports: [] },
Expand Down
File renamed without changes.
14 changes: 7 additions & 7 deletions react/src/commands/connect.ts → cli/src/commands/connect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as commander from 'commander'
import { InternalError, ParserError, isFigmaConnectFile, parse } from '../common/parser'
import { InternalError, ParserError, isFigmaConnectFile, parse } from '../react/parser'
import fs from 'fs'
import { upload } from '../connect/upload'
import { validateDocs } from '../connect/validation'
Expand All @@ -12,7 +12,7 @@ import { delete_docs } from '../connect/delete_docs'

type BaseCommand = commander.Command & {
token: string
debug: boolean
verbose: boolean
outfile: string
config: string
dryRun: boolean
Expand All @@ -26,7 +26,7 @@ function addBaseCommand(command: commander.Command, name: string, description: s
.usage('[options]')
.option('-r --dir <dir>', 'directory to parse')
.option('-t --token <token>', 'figma access token')
.option('-d --debug', 'output debug logs')
.option('-v --verbose', 'enable verbose logging for debugging')
.option('-o --outfile <file>', 'output to JSON file')
.option('-c --config <path>', 'path to a figma config file')
.option('--dry-run', 'tests publishing without actually publishing')
Expand Down Expand Up @@ -84,7 +84,7 @@ function getAccessToken(cmd: BaseCommand) {
}

function setupHandler(cmd: BaseCommand) {
if (cmd.debug) {
if (cmd.verbose) {
logger.setLogLevel(LogLevel.Debug)
}
}
Expand All @@ -96,7 +96,7 @@ async function getCodeConnectObjects(dir: string, cmd: BaseCommand, projectInfo:
const figmaNodeToFile = new Map()
for (const file of files.filter((f: string) => isFigmaConnectFile(tsProgram, f))) {
try {
const docs = await parse(tsProgram, file, remoteUrl, config, cmd.debug)
const docs = await parse(tsProgram, file, remoteUrl, config, cmd.verbose)
for (const doc of docs) {
figmaNodeToFile.set(doc.figmaNode, file)
}
Expand All @@ -105,13 +105,13 @@ async function getCodeConnectObjects(dir: string, cmd: BaseCommand, projectInfo:
} catch (e) {
logger.error(`❌ ${file}`)
if (e instanceof ParserError) {
if (cmd.debug) {
if (cmd.verbose) {
console.trace(e)
} else {
logger.error(e.toString())
}
} else {
if (cmd.debug) {
if (cmd.verbose) {
console.trace(e)
} else {
logger.error(new InternalError(String(e)).toString())
Expand Down
33 changes: 26 additions & 7 deletions react/src/common/api.ts → cli/src/common/api.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
type EnumValue = string | boolean | number | symbol | undefined | JSX.Element

export interface FigmaConnectAPI {
/**
* Creates Figma documentation for a React component and a Figma component. This function is used to
* define a code example that displays in Figma when that component is selected. The function must be
* called with a link to the figmaNode and a reference to the React component.
* Defines a code snippet that displays in Figma when a component is selected. This function has two signatures:
* - When called with a component reference as the first argument, it will infer metadata such as the import statement
* and location in source file from the component reference
* - When called only with a Figma node URL, this metadata will not be included. This is useful when you want to display a code snippet
* for something that isn't a React component, such as a `<button>` element
*
* @param component A reference to the React component
* @param figmaNodeUrl A link to the node in Figma, for example:`https://www.figma.com/file/123abc/My-Component?node-id=123:456`
* @param meta {@link FigmaConnectMeta}
*/
connect<P = {}>(component: any, figmaNodeUrl: string, meta?: FigmaConnectMeta<P>): void

/**
* Defines a code snippet that displays in Figma when a component is selected. This function has two signatures:
* - When called with a component reference as the first argument, it will infer metadata such as the import statement
* and location in source file from the component reference
* - When called only with a Figma node URL, this metadata will not be included. This is useful when you want to display a code snippet
* for something that isn't a React component, such as a `<button>` element
*
* @param figmaNodeUrl A link to the node in Figma, for example:`https://www.figma.com/file/123abc/My-Component?node-id=123:456`
* @param meta {@link FigmaConnectMeta}
*/
connect<P = {}>(figmaNodeUrl: string, meta?: FigmaConnectMeta<P>): void

/**
* Maps a Figma property to a boolean value for the connected component. This prop is replaced
* with values from the Figma instance when viewed in Dev Mode. For example:
Expand Down Expand Up @@ -41,10 +57,13 @@ export interface FigmaConnectAPI {
* @param figmaPropName The name of the property on the Figma component
* @param valueMapping A mapping of values for `true` and `false`
*/
boolean<V extends string | boolean | number | symbol | undefined | JSX.Element>(
boolean<TrueT extends EnumValue, FalseT extends EnumValue>(
figmaPropName: string,
valueMapping?: Record<'true' | 'false', V>,
): ValueOf<Record<'true' | 'false', V>>
valueMapping?: {
true?: TrueT
false?: FalseT
},
): ValueOf<Record<'true' | 'false', TrueT | FalseT>>

/**
* Maps a Figma Variant property to a set if values for the connected component. This prop is replaced
Expand All @@ -62,7 +81,7 @@ export interface FigmaConnectAPI {
* @param figmaPropName The name of the property on the Figma component
* @param valueMapping A mapping of values for the Figma Variant
*/
enum<V extends string | boolean | number | symbol | undefined | JSX.Element>(
enum<V extends EnumValue>(
figmaPropName: string,
valueMapping: PropMapping<Record<string, V>>,
): ValueOf<Record<string, V>>
Expand Down
8 changes: 7 additions & 1 deletion react/src/common/external.ts → cli/src/common/external.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { FigmaConnectMeta, PropMapping, ValueOf } from './api'
import * as React from 'react'

function connectType<P = {}>(_component: any, _figmaNodeUrl: string, _meta?: FigmaConnectMeta<P>) {}
function connectType<P = {}>(_figmaNodeUrl: string, _meta?: FigmaConnectMeta<P>): void
function connectType<P = {}>(
_component: any,
_figmaNodeUrl: string,
_meta?: FigmaConnectMeta<P>,
): void
function connectType(_component: unknown, _figmaNodeUrl: unknown, _meta?: unknown): void {}

function booleanType(_figmaPropName: string): boolean
function booleanType<V extends string | boolean | number | symbol | undefined | JSX.Element>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Intrinsic } from './intrinsics'

export interface CodeConnectJSON {
figmaNode: string
component: string
component?: string
variant?: Record<string, any>
source: string
sourceLocation: { line: number }
Expand Down
14 changes: 9 additions & 5 deletions react/src/common/intrinsics.ts → cli/src/common/intrinsics.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as ts from 'typescript'
import { InternalError, ParserContext, ParserError } from './parser'
import { assertIsStringLiteral, stripQuotes } from './compiler'
import { convertObjectLiteralToJs } from './compiler'
import { assertIsObjectLiteralExpression } from './compiler'
import { InternalError, ParserContext, ParserError } from '../react/parser'
import { assertIsStringLiteral, stripQuotes } from '../typescript/compiler'
import { convertObjectLiteralToJs } from '../typescript/compiler'
import { assertIsObjectLiteralExpression } from '../typescript/compiler'
import { FigmaConnectAPI } from './api'

export const API_PREFIX = 'figma'
Expand Down Expand Up @@ -265,7 +265,11 @@ export function valueMappingToString(valueMapping: Record<string, ValueMappingKi
if (typeof value === 'object' && 'kind' in value) {
// Mappings can be nested, e.g. an enum value can be figma.instance(...)
return `"${key}": ${intrinsicToString(value as Intrinsic)}`
} else if (typeof value === 'boolean' || typeof value === 'undefined') {
} else if (
typeof value === 'boolean' ||
typeof value === 'number' ||
typeof value === 'undefined'
) {
return `"${key}": ${value}`
} else {
return `"${key}": '${value}'`
Expand Down
File renamed without changes.
29 changes: 29 additions & 0 deletions react/src/common/project.ts → cli/src/common/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ export interface CodeConnectConfig {
*/
paths?: Record<string, string[]>
}
/**
* Storybook specific configuration
*/
storybook?: {
/**
* The URL of the Storybook instance for the project
*/
url: string
}
}

interface FigmaConfig {
Expand Down Expand Up @@ -127,6 +136,26 @@ export function getRemoteFileUrl(filePath: string, repoURL?: string) {
return `${url}/tree/master${relativeFilePath}`
}

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 index = filePath.indexOf(repoAbsPath)
if (index === -1) {
return ''
}
const relativeFilePath = filePath.substring(index + repoAbsPath.length + 1) // +1 to remove the leading slash
const storybookComponentPath = relativeFilePath
.trim()
.replace(/[\s|_]/g, '-')
.replace(/\.[jt]sx?$/, '')
.split('/')
.join('-')

return `${storybookUrl}/?path=/docs/${storybookComponentPath}`
}

export interface ProjectInfo {
/**
* Absolute path of the project directory
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion react/src/connect/upload.ts → cli/src/connect/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface Args {
}

function codeConnectStr(doc: CodeConnectJSON) {
return `${highlight(doc.component)}${doc.variant ? `(${Object.entries(doc.variant).map(([key, value]) => `${key}=${value}`)})` : ''} ${underline(doc.figmaNode)}`
return `${highlight(doc.component ?? '')}${doc.variant ? `(${Object.entries(doc.variant).map(([key, value]) => `${key}=${value}`)})` : ''} ${underline(doc.figmaNode)}`
}

export async function upload({ accessToken, docs }: Args) {
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
6 changes: 6 additions & 0 deletions cli/src/react/__test__/NoComponentArg.figma.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as React from 'react'
import figma from '../..'

figma.connect('ui/button', {
example: () => <div>Click Me</div>,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import figma from '../..'

figma.connect('ui/button')
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const figma = require('figma')

export default figma.tsx`<div>Click Me</div>`
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ParserError, parse } from '../parser'
import ts from 'typescript'
import path from 'path'
import { CodeConnectConfig } from '../project'
import { CodeConnectConfig } from '../../common/project'
import { readFileSync } from 'fs'

async function testParse(file: string, extraFiles: string[] = [], config?: CodeConnectConfig) {
Expand Down Expand Up @@ -40,7 +40,7 @@ describe('Parser (JS templates)', () => {
{
figmaNode: 'ui/button',
source:
'https://github.com/figma/code-connect/tree/master/react/src/common/__test__/TestComponents.tsx',
'https://github.com/figma/code-connect/tree/master/cli/src/react/__test__/TestComponents.tsx',
sourceLocation: { line: 12 },
template: getExpectedTemplate('Button'),
templateData: {
Expand Down Expand Up @@ -275,7 +275,7 @@ describe('Parser (JS templates)', () => {
language: 'typescript',
component: 'Button',
source:
'https://github.com/figma/code-connect/tree/master/react/src/common/__test__/TestComponents.tsx',
'https://github.com/figma/code-connect/tree/master/cli/src/react/__test__/TestComponents.tsx',
sourceLocation: { line: 12 },
template: getExpectedTemplate(indented ? 'PropMappings_indented' : 'PropMappings'),
templateData: {
Expand Down Expand Up @@ -358,7 +358,7 @@ describe('Parser (JS templates)', () => {
language: 'typescript',
component: 'Button',
source:
'https://github.com/figma/code-connect/tree/master/react/src/common/__test__/TestComponents.tsx',
'https://github.com/figma/code-connect/tree/master/cli/src/react/__test__/TestComponents.tsx',
sourceLocation: { line: 12 },
template: getExpectedTemplate('EnumLikeBooleanFalseProp'),
templateData: {
Expand Down Expand Up @@ -529,7 +529,7 @@ describe('Parser (JS templates)', () => {
{
component: 'Button',
source:
'https://github.com/figma/code-connect/tree/master/react/src/common/__test__/TestComponents.tsx',
'https://github.com/figma/code-connect/tree/master/cli/src/react/__test__/TestComponents.tsx',
},
])
})
Expand Down Expand Up @@ -635,4 +635,24 @@ describe('Parser (JS templates)', () => {
},
])
})

it('Supports not passing a component as the first arg', async () => {
const result = await testParse('NoComponentArg.figma.tsx')

expect(result).toMatchObject([
{
component: undefined,
template: getExpectedTemplate('Div'),
templateData: {
imports: [],
},
},
])
})

it('Throws an error if you pass neither the component reference or an example function', async () => {
await expect(() => testParse('NoComponentArgOrExampleFunction.figma.tsx')).rejects.toThrowError(
ParserError,
)
})
})
File renamed without changes.

0 comments on commit e0e48e2

Please sign in to comment.