Skip to content

Commit

Permalink
Code Connect v1.0.4
Browse files Browse the repository at this point in the history
  • Loading branch information
figma-bot committed Aug 7, 2024
1 parent 444f438 commit c890f16
Show file tree
Hide file tree
Showing 45 changed files with 2,244 additions and 894 deletions.
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# Code Connect v1.0.4 (7th August 2024)

## Fixed

### React
- Fixed rendering of identifiers, functions and objects when used as children

### SwiftUI
- Updated the `component` definition in FigmaConnect protocol to be optional and have a default implementation.

### Compose
- Added a more helpful error message when the JDK version is too low.

## Features

### General
- Added error message to suggest splitting publish when request too large
- CLI assistant support for selecting file exports to use in Code Connect template
- New --batch-size argument for publish command in order to split uploading into smaller "batches". This will allow for large uploads without having to split running the publish command with different directories.

# Code Connect v1.0.3 (23th July 2024)

## Fixed
Expand All @@ -18,7 +38,7 @@

### General
- Added support for SwiftUI and Compose in the CLI Assistant
- Added `--skip-update-check` flag
- Added `--skip-update-check` flag
- Added `--label` flag to the `publish` and `unpublish` commands to publish or unpublish to a custom label
- We now print the label used when running the `publish` command
- Improved autolinking algorithm
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ let package = Package(
],
path: "swiftui/Tests/CodeConnectParserTest",
resources: [
.copy("Button.figma.test"),
.copy("Samples.figma.test"),
]
)
]
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,7 @@ would change Figma node URLs like `https://figma.com/design/1234abcd/File-1/?nod
### Connectivity issues due to proxies or network security software

Some proxies or network security software can prevent Code Connect from communicating with Figma's servers. If you encounter issues, you may need to explicitly allow connections to `https://api.figma.com/`. Please reach out to [Figma support](https://help.figma.com/hc/en-us/requests/new) if you are still unable to use Code Connect.

### 413 errors due to too large uploads

Please rerun with the `--batch-size` parameter. This will upload the Code Connect in batches of documents of batch_size length. We suggest starting with 50 and decreasing until converging on a size that works for your Code Connect.
19 changes: 19 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,25 @@ figma.enum('Options', {

// result is true for disabled variants otherwise undefined
figma.enum('Variant', { Disabled: true })

// enums mappings can be used to show a component based on a Figma variant
figma.connect(Modal, 'https://...', {
props: {
cancelButton: figma.enum('Type', {
'Cancellable': <CancelButton />
}),
// ...
},
example: ({ cancelButton }) => {
return (
<Modal>
<Title>Title</Title>
<Content>Some content</Content>
{cancelButton}
</Modal>
)
},
})
```
Mapping objects for `figma.enum` as well as `figma.boolean` allows nested references, which is useful if you want to conditionally render a nested instance for example. (see the next section for how to use `figma.instance`)
Expand Down
3 changes: 2 additions & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@figma/code-connect",
"version": "1.0.3",
"version": "1.0.4",
"description": "A tool for connecting your design system components in code with your design system in Figma",
"keywords": [],
"author": "Figma",
Expand Down Expand Up @@ -32,6 +32,7 @@
"test:swift": "npm run test -- --runInBand --testPathPattern=e2e_connect_command_swift.test.ts --testPathPattern=e2e_wizard_swift.test.ts",
"test:non-mac": "npm run test -- --testPathIgnorePatterns=e2e_connect_command_swift.test.ts --testPathIgnorePatterns=e2e_wizard_swift.test.ts",
"bundle": "npm run build && npm pack && mkdir -p bundle && mv figma-code-connect*.tgz bundle",
"bundle:local": "cp package.json package.json.bak && grep -v 'workspace:' package.json > package.json && npm run build && npm pack && mkdir -p bundle-local && mv figma-code-connect*.tgz bundle-local; mv package.json.bak package.json",
"bundle:npm": "npm run build && npm pack && mkdir -p bundle-npm && mv figma-code-connect*.tgz bundle-npm",
"bundle:cli": "npm run build:webpack && mkdir -p bundle-cli && pkg --compress Brotli webpack-dist/figma.js",
"bundle:cli:linux": "npm run bundle:cli -- -o bundle-cli/figma-linux --target node18-linux-x64,node18-linux-arm64",
Expand Down
2 changes: 1 addition & 1 deletion cli/src/__test__/e2e_connect_command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ Invalid parser specified: "does-not-exist". Valid parsers are: swift, compose, _
expect(e.code).toBe(1)
expect(tidyStdOutput(e.stderr)).toBe(
`Config file found, parsing ./e2e_connect_command/unit_test_parser_invalid_response using specified include globs
Error returned from parser: Validation error: Required at "docs[0].figmaNode"; Required at "docs[0].template"; Required at "docs[0].templateData"; Required at "docs[0].language"; Required at "docs[0].label", try re-running the command with --verbose for more information.`,
Error returned from parser: Validation error: Required at "docs[0].figmaNode"; Required at "docs[0].template"; Required at "docs[0].templateData"; Required at "docs[0].language"; Required at "docs[0].label". Try re-running the command with --verbose for more information.`,
)
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ let package = Package(
name: "CodeConnectE2ETest",
targets: ["CodeConnectE2ETest"]),
],
dependencies: [.package(path: "../../../../../../figmadoc")],
dependencies: [.package(path: "../../../../../")],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
Expand Down
10 changes: 9 additions & 1 deletion cli/src/__test__/e2e_legacy_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ import { LONG_TEST_TIMEOUT_MS, tidyStdOutput } from './utils'
describe('e2e test for legacy config handling', () => {
async function runCommandInteractively(testCase: string, answer: string) {
const command = 'npx'
const args = ['tsx', '../cli', 'connect', 'parse', '--dir', `./e2e_connect_command/${testCase}`]
const args = [
'tsx',
'../cli',
'connect',
'parse',
'--skip-update-check',
'--dir',
`./e2e_connect_command/${testCase}`,
]
const child = spawn(command, args, {
cwd: __dirname,
stdio: 'pipe',
Expand Down
21 changes: 18 additions & 3 deletions cli/src/commands/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export function addConnectCommandToProgram(program: commander.Command) {
)
.option('--skip-validation', 'skip validation of Code Connect docs')
.option('-l --label <label>', 'label to apply to the published files')
.option(
'-b --batch-size <batch_size>',
'optional batch size (in number of documents) to use when uploading. Use this if you hit "request too large" errors. See README for more information.',
)
.action(withUpdateCheck(handlePublish))

addBaseCommand(
Expand Down Expand Up @@ -218,7 +222,7 @@ export async function getCodeConnectObjects(
} catch (e) {
// zod-validation-error formats the error message into a readable format
exitWithError(
`Error returned from parser: ${fromError(e)}, try re-running the command with --verbose for more information.`,
`Error returned from parser: ${fromError(e)}. Try re-running the command with --verbose for more information.`,
)
}
}
Expand Down Expand Up @@ -277,7 +281,9 @@ async function getReactCodeConnectObjects(
return allCodeConnectObjects
}

async function handlePublish(cmd: BaseCommand & { skipValidation: boolean; label: string }) {
async function handlePublish(
cmd: BaseCommand & { skipValidation: boolean; label: string; batchSize: string },
) {
setupHandler(cmd)

let dir = getDir(cmd)
Expand Down Expand Up @@ -326,7 +332,16 @@ async function handlePublish(cmd: BaseCommand & { skipValidation: boolean; label
process.exit(0)
}

upload({ accessToken, docs: codeConnectObjects })
let batchSize
if (cmd.batchSize) {
batchSize = parseInt(cmd.batchSize, 10)
if (isNaN(batchSize)) {
logger.error('Error: failed to parse batch-size. batch-size passed must be a number')
exitWithFeedbackMessage(1)
}
}

upload({ accessToken, docs: codeConnectObjects, batchSize: batchSize, verbose: cmd.verbose })
}

async function handleUnpublish(cmd: BaseCommand & { node: string; label: string }) {
Expand Down
2 changes: 1 addition & 1 deletion cli/src/common/intrinsics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ export function intrinsicToString({ kind, args }: Intrinsic, childLayer?: string
return `[${args.className.map((className) => (typeof className === 'string' ? `"${className}"` : `${intrinsicToString(className, childLayer)}`)).join(', ')}].filter(v => !!v).join(' ')`
}
case IntrinsicKind.TextContent: {
return `${selector}.__findChildWithCriteria__({ name: '${args.layer}', type: "TEXT" }).textContent`
return `${selector}.__findChildWithCriteria__({ name: '${args.layer}', type: "TEXT" }).__render__()`
}
case IntrinsicKind.NestedProps: {
throw new ParserError(
Expand Down
25 changes: 15 additions & 10 deletions cli/src/common/updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,35 @@ import { execSync, spawnSync } from 'child_process'
import axios from 'axios'
import { compareVersions } from 'compare-versions'
import { BaseCommand } from '../commands/connect'
import { Command } from 'commander'

let updatedVersionAvailable: string | false | undefined = undefined
let message: string | undefined = undefined

type EndsWithBaseCommand<T extends BaseCommand> = [...any[], T]
// The type of the arguments passed to a command handler:
// any arguments, then the command arguments, then the Command object
type CommandArgs<T extends BaseCommand> = [...any[], T, Command]

// Wrap action handlers to check for updates or a message, and output a message
// after the action if any are available
export function withUpdateCheck<T extends BaseCommand>(
// The last argument is always the command, but I couldn't work out how to
// model this with Typescript here
// The second to last argument is always the command args, but I couldn't work
// out how to model this with Typescript here
fn: (...args: any[]) => void | Promise<void>,
) {
return (...args: EndsWithBaseCommand<T>) => {
const command = args[args.length - 1]
const restArgs = args.slice(0, -1)

if (command.skipUpdateCheck) {
return fn(...restArgs, command)
return (...args: CommandArgs<T>) => {
// Get the args passed at the command line (the second to last argument)
const commandArgs = args[args.length - 2]
// Anything before that is a regular arg
const restArgs = args.slice(0, -2)

if (commandArgs.skipUpdateCheck) {
return fn(...restArgs, commandArgs)
}

startUpdateCheck()

const result = fn(...restArgs, command)
const result = fn(...restArgs, commandArgs)
if (result instanceof Promise) {
result.finally(waitAndCheckForUpdates)
} else {
Expand Down
13 changes: 13 additions & 0 deletions cli/src/connect/parser_executable_types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod'
import { BaseCodeConnectObject } from '../common/figma_connect'
import { FigmaRestApi } from './figma_rest_api'

export type ParseRequestPayload = {
mode: 'PARSE'
Expand Down Expand Up @@ -113,6 +114,11 @@ export const ParseResponsePayload = z.object({
messages: ParserExecutableMessages,
})

export type SupportedMappingType =
| FigmaRestApi.ComponentPropertyType.Text
| FigmaRestApi.ComponentPropertyType.Boolean
export type PropMapping = Record<string, { codePropName: string; mapping: SupportedMappingType }>

export type CreateRequestPayload = {
mode: 'CREATE'
// Absolute destination directory for the created file. The parser is free to
Expand All @@ -122,6 +128,13 @@ export type CreateRequestPayload = {
// Optional destination file name. If omitted, the parser can determine the
// file name itself.
destinationFile?: string
// The filepath of the code to be connected. If present, this is used instead of
// component.normalizedName
sourceFilepath?: string
// The export to use from sourceFilepath (TypeScript only)
sourceExport?: string
// A mapping of how Figma props should map to code properties
propMapping?: PropMapping
// Information about the Figma component. This matches the REST API (except the
// figmaNodeUrl and normalizedName fields), which should make it easier to
// implement and maintain as we can just pass it through
Expand Down
39 changes: 33 additions & 6 deletions cli/src/connect/parser_executables.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { z } from 'zod'
import { exitWithError, logger } from '../common/logging'
import { CodeConnectExecutableParserConfig, FirstPartyExecutableParser } from './project'
import {
CodeConnectExecutableParserConfig,
FirstPartyExecutableParser,
FirstPartyParser,
} from './project'
import {
CreateResponsePayload,
ParserExecutableMessages,
Expand All @@ -14,6 +18,7 @@ import {
getGradleWrapperExecutablePath,
getGradleWrapperPath,
} from '../parser_scripts/get_gradlew_path'
import { getComposeErrorSuggestion } from '../parser_scripts/compose_errors'

const temporaryInputFilePath = 'tmp/figma-code-connect-parser-input.json.tmp'

Expand Down Expand Up @@ -82,6 +87,7 @@ export async function callParser(
})

let stdout = ''
let stderr = ''

child.stdout.on('data', (data) => {
stdout += data.toString()
Expand All @@ -102,18 +108,25 @@ export async function callParser(
// Non-JSON output will be logged as debug messages, as this is likely to
// be e.g. compiler output which the user doesn't need to see ordinarily.
child.stderr.on('data', (data) => {
const message = data.toString().trim()
const message = data.toString()
const trimmedMessage = message.trim()
try {
const parsed = JSON.parse(message)
const parsed = JSON.parse(trimmedMessage)
handleMessages([parsed])
} catch (e) {
logger.debug(message)
stderr += message
logger.debug(trimmedMessage)
}
})

child.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Parser exited with code ${code}`))
const errorSuggestion = determineErrorSuggestionFromStderr(stderr, config.parser)
if (errorSuggestion) {
reject(new Error(`Parser exited with code ${code}: ${errorSuggestion}`))
} else {
reject(new Error(`Parser exited with code ${code}`))
}
} else {
resolve(JSON.parse(stdout))
}
Expand All @@ -131,7 +144,7 @@ export async function callParser(
}
} catch (e) {
exitWithError(
`Error calling parser: ${e}, try re-running the command with --verbose for more information.`,
`Error calling parser: ${e}. Try re-running the command with --verbose for more information.`,
)
}
})
Expand Down Expand Up @@ -160,3 +173,17 @@ export function handleMessages(messages: z.infer<typeof ParserExecutableMessages

return { hasErrors }
}

// This function is used to determine if there is a suggestion for the error based on the output
// to stderr. Certain parsers return the same error code for different types of errors such as
// errors from invoking the gradle wrapper for Compose.
// In the future we should consider exposing a different API for having the parser return a suggestion directly.
function determineErrorSuggestionFromStderr(
stderr: string,
parser: FirstPartyParser,
): string | null {
if (parser === 'compose') {
return getComposeErrorSuggestion(stderr)
}
return null
}
Loading

0 comments on commit c890f16

Please sign in to comment.