Skip to content

Commit

Permalink
Code Connect v1.0.2
Browse files Browse the repository at this point in the history
  • Loading branch information
figma-bot committed Jul 10, 2024
1 parent 810f271 commit 9911b18
Show file tree
Hide file tree
Showing 24 changed files with 846 additions and 125 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# Code Connect v1.0.2 (10th July 2024)

## Fixed

### General
- Improvements to CLI Assistant

### React
- Prevent rendering empty strings as prop values (Fixes: https://github.com/figma/code-connect/issues/67)
- Fix output when there are multiple return statements
- Fix wildcard importPaths mappings with nested folders
- Fix boolean mappings for lowercase boolean-like strings (Fixes: https://github.com/figma/code-connect/issues/70)
- Fix boolean-like keys in enums (Fixes: https://github.com/figma/code-connect/issues/74)

### SwiftUI
- Fix spaces in Xcode file path

# Code Connect v1.0.1 (20th June 2024)

## Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Code Connect is easy to set up, easy to maintain, type-safe, and extensible. Out
![image](https://static.figma.com/uploads/d98e747613e01685d6a0f9dd3e2dcd022ff289c0.png)

> [!NOTE]
> Code Connect is available on Organization and Enterprise plans and requires a full Design or Dev Mode seat to use. Code Connect is currently in beta, so you can expect this feature to change. You may also experience bugs or performance issues during this time.
> Code Connect is available on Organization and Enterprise plans and requires a full Design or Dev Mode seat to use.
## CLI installation

Expand Down
25 changes: 20 additions & 5 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ figma.string('Title')
### Booleans
Booleans work similar to strings. However Code Connect also provides helpers for mapping booleans in Figma to more complex types in code. For example you may want to map a Figma boolean to the existence of a specific sublayer in code.
Booleans work similar to strings. However Code Connect also provides helpers for mapping booleans in Figma to more complex types in code. For example you may want to map a Figma boolean to the existence of a specific sublayer in code. In addition to mapping boolean props, `figma.boolean` can be used to map boolean Variants in Figma. A boolean Variant is a Variant with only two options that are either "Yes"/"No", "True"/"False" or "On"/Off". For `figma.boolean` these values are normalized to `true` and `false`.
```tsx
// simple mapping of boolean from figma to code
Expand All @@ -287,15 +287,16 @@ figma.boolean('Has Icon', {
true: <Icon />,
false: <Spacer />,
})

```
In some cases, you only want to render a certain prop if it matches some value in Figma. You can do this either by passing a partial mapping object, or setting the value to `undefined`.
```tsx
// Don't render the prop if 'Has Icon' in figma is `false`
figma.boolean('Has Icon', {
true: <Icon />,
false: undefined,
// Don't render the prop if 'Has label' in figma is `false`
figma.boolean("Has label", {
true: figma.string("Label"),
false: undefined
})
```
Expand Down Expand Up @@ -330,6 +331,20 @@ figma.enum('Type', {
})
```
Note that in contrast to `figma.boolean`, values are _not_ normalized for `figma.enum`. You always need to pass the exact literal values to the mapping object.
```tsx
// These two are equivalent for a variant with the options "Yes" and "No"
disabled: figma.enum("Boolean Variant", {
Yes: // ...
No: // ...
})
disabled: figma.boolean("Boolean Variant", {
true: // ...
false: // ...
})
```
### Instances
Instances is a Figma term for nested component references. For example, in the case of a `Button` containing an `Icon` as a nested component, we would call the `Icon` an instance. In Figma instances can be properties, (that is, inputs to the component), just like we have render props in code. Similarly to how we can map booleans, enums, and strings from Figma to code, we can also map these to instance props.
Expand Down
6 changes: 4 additions & 2 deletions cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@figma/code-connect",
"version": "1.0.1",
"version": "1.0.2",
"description": "A tool for connecting your design system components in code with your design system in Figma",
"keywords": [],
"author": "Figma",
Expand All @@ -25,6 +25,7 @@
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules npx jest --coverage",
"test:fast": "npm run test -- --testPathIgnorePatterns=template_rendering.test.ts --testPathIgnorePatterns=e2e_connect_command_swift.test.ts",
"test:ci": "npm run test:non-mac -- --runInBand",
"test:wizard": "npm run test -- --runInBand --testPathPattern=e2e_wizard.test.ts",
"test:swift": "npm run test -- --runInBand --testPathPattern=e2e_connect_command_swift.test.ts",
"test:non-mac": "npm run test -- --testPathIgnorePatterns=e2e_connect_command_swift.test.ts",
"bundle": "npm run build && npm pack && mkdir -p bundle && mv figma-code-connect*.tgz bundle",
Expand All @@ -40,7 +41,7 @@
"@types/cross-spawn": "^6.0.6",
"@types/jest": "^29.5.5",
"@types/lodash": "^4.17.0",
"@types/node": "^18.17.1",
"@types/node": "^20.14.0",
"@types/prettier": "2.7.3",
"@types/prompts": "^2.4.9",
"@types/react": "18.0.26",
Expand Down Expand Up @@ -73,6 +74,7 @@
"glob": "^10.3.10",
"lodash": "^4.17.21",
"minimatch": "^9.0.3",
"ora": "^5.4.1",
"prettier": "^2.8.8",
"prompts": "^2.4.2",
"strip-ansi": "^6.0.0",
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react'

interface ButtonProps {
disabled: boolean
children: any
}

/**
* @description This is a button
* @param children text to render
* @param disabled disable the button
* @returns JSX element
*/
export const PrimaryButton = ({ children, disabled = false }: ButtonProps) => {
return <button disabled={disabled}>{children}</button>
}
15 changes: 15 additions & 0 deletions cli/src/__test__/e2e_connect_command/react_wizard/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "tests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"react": "^18.3.1"
}
}
11 changes: 11 additions & 0 deletions cli/src/__test__/e2e_connect_command/react_wizard/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"outDir": "dist",
"module": "commonjs",
"target": "es6",
"esModuleInterop": true,
"skipLibCheck": true,
},
"include": ["**/*.tsx"],
}
70 changes: 70 additions & 0 deletions cli/src/__test__/e2e_wizard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { promisify } from 'util'
import { exec } from 'child_process'
import { LONG_TEST_TIMEOUT_MS, tidyStdOutput } from './utils'
import fs from 'fs'
import path from 'path'

describe('e2e test for the wizard', () => {
let result: {
stdout: string
stderr: string
}

const testPath = path.join(__dirname, 'e2e_connect_command/react_wizard')

beforeAll(async () => {
const mockDocPath = path.join(
__dirname,
'e2e_connect_command/dummy_api_response_for_wizard.json',
)

const wizardAnswers = [
'figd_123', // Access token
'./e2e_connect_command/react_wizard/components', // Top-level components directory
'https://www.figma.com/design/abc123/my-design-system', // Design system URL
'yes', // Confirm create a new config file
'', // Don't select any links to edit
'', // co-locate CC files
]

const escapedStringifiedJson = JSON.stringify(wizardAnswers).replace(/"/g, '\\"')

result = await promisify(exec)(
`npx cross-env CODE_CONNECT_MOCK_DOC_RESPONSE=${mockDocPath} WIZARD_ANSWERS_TO_PREFILL="${escapedStringifiedJson}" npx tsx ../cli connect --dir ${testPath}`,
{
cwd: __dirname,
},
)
}, LONG_TEST_TIMEOUT_MS)

afterAll(() => {
fs.rmSync(path.join(testPath, 'figma.config.json'), { force: true })
fs.rmSync(path.join(testPath, 'components/PrimaryButton.figma.tsx'), { force: true })
})

it('starts the wizard', () => {
expect(tidyStdOutput(result.stderr)).toContain('Welcome to Code Connect')
})

it('creates config file from given answers', () => {
const configPath = path.join(testPath, 'figma.config.json')
expect(fs.readFileSync(configPath, 'utf8')).toBe(`\
{
"codeConnect": {
"include": ["components/**/*.{tsx,jsx}"]
}
}
`)
})

it('reaches linking step', () => {
expect(tidyStdOutput(result.stderr)).toContain('Connecting your components')
})

it('creates Code Connect file at correct location', () => {
const codeConnectPath = path.join(testPath, 'components/PrimaryButton.figma.tsx')
expect(tidyStdOutput(result.stderr)).toContain(`Created ${codeConnectPath}`)
const exists = fs.existsSync(codeConnectPath)
expect(exists).toBe(true)
})
})
1 change: 1 addition & 0 deletions cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { updateCli } from './common/updates'
require('dotenv').config()

async function run() {

const program = new commander.Command().version(require('./../package.json').version)
program.enablePositionalOptions()

Expand Down
76 changes: 76 additions & 0 deletions cli/src/connect/__test__/project.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { CodeConnectReactConfig, resolveImportPath } from '../project'

describe('Project helper functions', () => {
function getConfig(importPaths: {}): CodeConnectReactConfig {
return {
parser: 'react',
...importPaths,
}
}

describe('importPath mappings', () => {
it('Matches a simple import path', () => {
const mapped = resolveImportPath(
'/Users/test/app/src/button.tsx',
getConfig({ importPaths: { 'src/button.tsx': '@ui/button' } }),
)
expect(mapped).toEqual('@ui/button')
})

it('Matches a wildcard import path', () => {
const mapped = resolveImportPath(
'/Users/test/app/src/button.tsx',
getConfig({ importPaths: { 'src/*': '@ui' } }),
)
expect(mapped).toEqual('@ui')
})

it('Matches a wildcard import path with a wildcard output path', () => {
const mapped = resolveImportPath(
'/Users/test/app/src/button.tsx',
getConfig({ importPaths: { 'src/*': '@ui/*' } }),
)
expect(mapped).toEqual('@ui/button')
})

it('Matches a wildcard import path with a nested directory', () => {
const mapped = resolveImportPath(
'/Users/test/app/src/components/button.tsx',
getConfig({ importPaths: { 'src/*': '@ui' } }),
)
expect(mapped).toEqual('@ui')
})

it('Matches a wildcard import path and output path with a nested directory', () => {
const mapped = resolveImportPath(
'/Users/test/app/src/components/button.tsx',
getConfig({ importPaths: { 'src/*': '@ui/*' } }),
)
expect(mapped).toEqual('@ui/button')
})

it('Passing only a wildcard matches any import', () => {
const mapped = resolveImportPath(
'/Users/test/app/src/components/button.tsx',
getConfig({ importPaths: { '*': '@ui' } }),
)
expect(mapped).toEqual('@ui')
})

it('Returns null for non-matching paths', () => {
const mapped = resolveImportPath(
'/Users/test/app/src/button.tsx',
getConfig({ importPaths: { 'src/components/*': '@ui' } }),
)
expect(mapped).toBeNull()
})

it('Should pick the first match if there are multiple mappings', () => {
const mapped = resolveImportPath(
'/Users/test/app/src/icons/icon.tsx',
getConfig({ importPaths: { 'icons/*': '@ui/icons', 'src/*': '@ui' } }),
)
expect(mapped).toEqual('@ui/icons')
})
})
})
48 changes: 40 additions & 8 deletions cli/src/connect/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,17 +521,20 @@ export async function getProjectInfoFromConfig(
? DEFAULT_INCLUDE_GLOBS_BY_PARSER[config.parser]
: undefined

// always ignore any `node_modules` folders in react projects
const defaultExcludeGlobs = config.parser
? {
react: [`${absPath}/node_modules/**`],
swift: [''],
compose: [''],
__unit_test__: [''],
}[config.parser]
: ''
react: ['node_modules/**'],
swift: [],
compose: [],
__unit_test__: [],
}[config.parser] ?? []
: []

const includeGlobs = config.include || defaultIncludeGlobs
const excludeGlobs = config.exclude || defaultExcludeGlobs || []
const excludeGlobs = config.exclude
? [...config.exclude, ...defaultExcludeGlobs]
: defaultExcludeGlobs

if (!includeGlobs) {
exitWithError('No include globs specified in config file')
Expand All @@ -544,6 +547,12 @@ export async function getProjectInfoFromConfig(
absolute: true,
})

if (files.length > 10000) {
logger.warn(
`Matching number of files was excessively large (${files.length}) - consider using more specific include/exclude globs in your config file.`,
)
}

return {
absPath,
remoteUrl,
Expand Down Expand Up @@ -579,6 +588,7 @@ export function getReactProjectInfo(
paths: projectInfo.config.paths ?? {},
allowJs: true,
}

const tsProgram = ts.createProgram(projectInfo.files, compilerOptions)

return {
Expand All @@ -588,9 +598,31 @@ export function getReactProjectInfo(
}

export function resolveImportPath(filePath: string, config: CodeConnectReactConfig): string | null {
// Takes the reversed path and pattern parts and check if they match
function isMatch(patternParts: string[], pathParts: string[]) {
if (patternParts[0] === '*') {
// if the path is just a wildcard and nothing else, match any import
if (patternParts.length === 1) {
return true
}

// if the _next_ part in the pattern does not exist in the path, it's not
// a match.
const index = pathParts.indexOf(patternParts[1])
if (index === -1) {
return false
}

// Skip to the matching part in the path and match the rest of
// the pattern. E.g if the pattern is `*/ui/src` (reversed) and the path is
// `button.tsx/components/ui/src`, we skip to `ui` and match the rest of the
// pattern.
patternParts = patternParts.slice(1)
pathParts = pathParts.slice(index)
}

for (let i = 0; i < patternParts.length; i++) {
if (patternParts[i] !== '*' && patternParts[i] !== pathParts[i]) {
if (patternParts[i] !== pathParts[i]) {
return false
}
}
Expand Down
Loading

0 comments on commit 9911b18

Please sign in to comment.