Skip to content

Commit 05443c9

Browse files
author
figma-bot
committed
Code Connect v1.3.11
1 parent 8f08eb6 commit 05443c9

File tree

7 files changed

+224
-38
lines changed

7 files changed

+224
-38
lines changed

CHANGELOG.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1+
# Code Connect v1.3.12 (TBD)
2+
3+
## Fixed
4+
5+
### General
6+
7+
# Code Connect v1.3.11 (26th November 2025)
8+
9+
### Swift
10+
11+
- Fixed a corner case where the swift parser generated invalid code-connect code.
12+
113
# Code Connect v1.3.10 (19th November 2025)
214

315
## Fixed
416

517
### General
618

7-
- Updated glob dependency to 11.0.4 to fix security vulnerability (https://github.com/figma/figma/pull/623532)
19+
- Updated glob dependency to 11.0.4 to fix security vulnerability
820

921
# Code Connect v1.3.9 (14th November 2025)
1022

Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/npm_catalog.toml

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -85,19 +85,19 @@
8585
"@aws-sdk/util-utf8-browser" = "3.664.0"
8686
"@aws-sdk/util-waiter" = "3.374.0"
8787
"@aws-sdk/xml-builder" = "3.664.0"
88-
"@babel/core" = "7.28.0"
89-
"@babel/code-frame" = "7.27.1"
90-
"@babel/generator" = "7.28.0"
91-
"@babel/helpers" = "7.28.2"
92-
"@babel/helper-string-parser" = "7.28.2"
93-
"@babel/helper-validator-identifier" = "7.28.2"
94-
"@babel/parser" = "7.28.0"
95-
"@babel/preset-env" = "7.28.0"
96-
"@babel/runtime" = "7.28.2"
97-
"@babel/standalone" = "7.28.2"
98-
"@babel/template" = "7.27.2"
99-
"@babel/traverse" = "7.28.0"
100-
"@babel/types" = "7.28.2"
88+
"@babel/core" = "7.28.5"
89+
"@babel/code-frame" = "7.28.5"
90+
"@babel/generator" = "7.28.5"
91+
"@babel/helpers" = "7.28.4"
92+
"@babel/helper-string-parser" = "7.28.5"
93+
"@babel/helper-validator-identifier" = "7.28.5"
94+
"@babel/parser" = "7.28.5"
95+
"@babel/preset-env" = "7.28.5"
96+
"@babel/runtime" = "7.28.4"
97+
"@babel/standalone" = "7.28.5"
98+
"@babel/template" = "7.28.5"
99+
"@babel/traverse" = "7.28.5"
100+
"@babel/types" = "7.28.5"
101101
"eslint" = "^9.36.0"
102102
"eslint-config-prettier" = "^9.1.0"
103103
"eslint-plugin-import" = "^2.32.0"
@@ -106,19 +106,22 @@
106106
"eslint-plugin-react-hooks" = "^5.2.0"
107107
"eslint-plugin-simple-import-sort" = "^12.1.1"
108108
"eslint-plugin-unused-imports" = "^4.1.4"
109+
"eslint-import-resolver-typescript" = "^4.4.4"
109110
"eslint-plugin-testing-library" = "^7.12.0"
110111
"eslint-plugin-workspaces" = "^0.11.0"
111112
"@eslint/js" = "^9.23.0"
112113
"@eslint/config-helpers" = "^0.4.0"
113-
"@typescript-eslint/eslint-plugin" = "8.45.0"
114-
"@typescript-eslint/parser" = "8.45.0"
115-
"@typescript-eslint/rule-tester" = "8.45.0"
116-
"@typescript-eslint/scope-manager" = "8.45.0"
117-
"@typescript-eslint/types" = "8.45.0"
118-
"@typescript-eslint/typescript-estree" = "8.45.0"
119-
"@typescript-eslint/utils" = "8.45.0"
120-
"@typescript-eslint/visitor-keys" = "8.45.0"
121-
"typescript-eslint" = "8.45.0"
114+
"@typescript-eslint/eslint-plugin" = "8.47.0"
115+
"@typescript-eslint/parser" = "8.47.0"
116+
"@typescript-eslint/project-service" = "8.47.0"
117+
"@typescript-eslint/rule-tester" = "8.47.0"
118+
"@typescript-eslint/scope-manager" = "8.47.0"
119+
"@typescript-eslint/tsconfig-utils" = "8.47.0"
120+
"@typescript-eslint/types" = "8.47.0"
121+
"@typescript-eslint/typescript-estree" = "8.47.0"
122+
"@typescript-eslint/utils" = "8.47.0"
123+
"@typescript-eslint/visitor-keys" = "8.47.0"
124+
"typescript-eslint" = "8.47.0"
122125
"@jest/core" = "^29.7.0"
123126
"@jest/create-cache-key-function" = "^29.7.0"
124127
"@jest/fake-timers" = "^29.7.0"
@@ -177,6 +180,8 @@
177180
"webpack" = "5.91.0"
178181
"webpack-cli" = "5.1.4"
179182
"@bazel/runfiles" = "^6.3.1"
183+
"@figma/plugin-typings" = "1.121.0"
184+
"@figma/widget-typings" = "1.12.1"
180185
"@modelcontextprotocol/sdk" = "1.18.0"
181186
"@types/mocha" = "10.0.10"
182187
"@types/node" = "22.17.2"

cli/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@figma/code-connect",
3-
"version": "1.3.10",
3+
"version": "1.3.11",
44
"description": "A tool for connecting your design system components in code with your design system in Figma",
55
"keywords": [],
66
"author": "Figma",
@@ -63,10 +63,10 @@
6363
"benchmarking:run": "npx tsx ./src/connect/wizard/__test__/prop_mapping/prop_mapping_benchmarking.ts"
6464
},
6565
"devDependencies": {
66-
"@babel/core": "7.28.0",
67-
"@babel/generator": "7.28.0",
68-
"@babel/parser": "7.28.0",
69-
"@babel/types": "7.28.2",
66+
"@babel/core": "7.28.5",
67+
"@babel/generator": "7.28.5",
68+
"@babel/parser": "7.28.5",
69+
"@babel/types": "7.28.5",
7070

7171
"@storybook/csf-tools": "8.5.1",
7272
"@types/cross-spawn": "^6.0.6",
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import Foundation
2+
import XCTest
3+
4+
@testable import CodeConnectParser
5+
6+
class CodeConnectParserInvalidJSTest: XCTestCase {
7+
func testDoubleWrappedTemplateVariables() throws {
8+
// Test that double wrapped template variables are fixed
9+
// Before this fix, the template would be rendered as ${${foo}} instead of ${foo}, which is invalid JavaScript
10+
// happens because the element _before_ it has hideDefault = true, so the template writer adds an extra wrapper.
11+
let expectedDoc = CodeConnectDoc(
12+
figmaNode: "https://figma.com/file/abc/Test?node-id=123",
13+
source: "See below",
14+
sourceLocation: CodeConnectDoc.SourceLocation(line: 141),
15+
component: "ListRowChevron",
16+
variant: [:],
17+
template: JSTemplateTestHelpers.templateWithInitialBoilerplate("""
18+
const disabled = figma.properties.enum('State', {
19+
'Active': false,
20+
'Disabled': true,
21+
'Hover': false,
22+
'Idle': false
23+
})
24+
const showDivider = figma.properties.boolean('Show divider', {
25+
'true': true,
26+
'false': false
27+
})
28+
const size = figma.properties.enum('Size', {
29+
'Large': '.large',
30+
'Medium': '.medium',
31+
'Small': '.small'
32+
})
33+
const title = figma.properties.string('Title')
34+
export default figma.swift`ListRowChevron {\n Text(\"${title.replace(/\\n/g, \'\\\\n\')}\")\n}\n.size(${size})\n.paddingHorizontal(paddingHorizontal)${showDivider === false ? undefined : `\\n.divider(${showDivider})`}\n.disabled(${disabled})`
35+
"""),
36+
templateData: TemplateData(
37+
props: [
38+
"showDivider": .propMap(PropMap(
39+
kind: .boolean,
40+
args: PropMapArgs(
41+
figmaPropName: "Show divider",
42+
valueMapping: nil
43+
),
44+
hideDefault: true,
45+
defaultValue: .bool(false)
46+
)),
47+
"title": .propMap(PropMap(
48+
kind: .string,
49+
args: PropMapArgs(
50+
figmaPropName: "Title",
51+
valueMapping: nil
52+
),
53+
hideDefault: false,
54+
defaultValue: .string("\"Title\"")
55+
)),
56+
"disabled": .propMap(PropMap(
57+
kind: .enumerable,
58+
args: PropMapArgs(
59+
figmaPropName: "State",
60+
valueMapping: [
61+
.string("Idle"): .bool(false),
62+
.string("Active"): .bool(false),
63+
.string("Disabled"): .bool(true),
64+
.string("Hover"): .bool(false)
65+
]
66+
),
67+
hideDefault: false,
68+
defaultValue: .bool(false)
69+
)),
70+
"size": .propMap(PropMap(
71+
kind: .enumerable,
72+
args: PropMapArgs(
73+
figmaPropName: "Size",
74+
valueMapping: [
75+
.string("Small"): .string(".small"),
76+
.string("Large"): .string(".large"),
77+
.string("Medium"): .string(".medium")
78+
]
79+
),
80+
hideDefault: false,
81+
defaultValue: .string(".small")
82+
))
83+
],
84+
imports: [],
85+
nestable: true
86+
),
87+
functionName: "ListRowChevron_connection"
88+
)
89+
90+
let url = try XCTUnwrap(Bundle.module.url(forResource: "Samples.figma", withExtension: "test"))
91+
var figmadoc = try XCTUnwrap(CodeConnectParser.createCodeConnects([url], importMapping: [:]).docs.first(where: {
92+
$0.component == "ListRowChevron"
93+
}))
94+
95+
// The source is not easily predictable as it depends on the location on disk,
96+
// so instead check it looks sensible, then set the doc's source to match the
97+
// expectation so we can still XCTAssertEqual instead of manually testing each field
98+
XCTAssertTrue(figmadoc.source.hasSuffix("/Samples.figma.test"))
99+
figmadoc.source = expectedDoc.source
100+
101+
XCTAssertEqual(figmadoc, expectedDoc)
102+
}
103+
}

swiftui/Tests/CodeConnectParserTest/Samples.figma.test

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,18 @@ struct FigmaButton_doc: FigmaConnect {
3939
]
4040
)
4141
var buttonVariant: ButtonVariant = .primary
42-
42+
4343
var variant = ["Has Icon": true]
4444

4545
@FigmaString("🎛️ Label")
4646
var title: String = "Submit"
4747

4848
@FigmaBoolean("🎛️ Disabled")
4949
var disabled: Bool = false
50-
50+
5151
@FigmaBoolean("Is Destructive", mapping: [true: .destructive, false: .none])
5252
var role: ButtonRole = .none
53-
53+
5454
@FigmaInstance("Icon")
5555
var icon: Icon? = nil
5656

@@ -112,18 +112,63 @@ struct LegacyFigmaButton_doc: FigmaConnect {
112112
struct NonNestable: FigmaConnect {
113113
let component = AnyView.self
114114
let figmaNodeUrl: String = "https://figma.com/file/abc/Test?node-id=123"
115-
115+
116116
var body: some View {
117117
@State var isOn = true
118-
118+
119119
ElementWithBinding(isOn: $isOn)
120120
}
121121
}
122122

123123
struct ComponentlessDefinition: FigmaConnect {
124124
let figmaNodeUrl: String = "https://figma.com/file/abc/Test?node-id=123"
125-
125+
126126
var body: some View {
127127
SomeElement()
128128
}
129129
}
130+
131+
enum ListRowSize {
132+
case small
133+
case medium
134+
case large
135+
}
136+
137+
enum ListRowPaddingHorizontal {
138+
case medium
139+
}
140+
141+
struct ListRowChevron: View {
142+
var body: some View {
143+
Text("List Row")
144+
}
145+
}
146+
147+
struct ListRowChevron_connection: FigmaConnect {
148+
let component = ListRowChevron.self
149+
let figmaNodeUrl: String = "https://figma.com/file/abc/Test?node-id=123"
150+
151+
@FigmaBoolean("Show divider", hideDefault: true)
152+
var showDivider: Bool = false
153+
154+
@FigmaString("Title")
155+
var title: String = "Title"
156+
157+
@FigmaEnum("Size", mapping: ["Small": .small, "Medium": .medium, "Large": .large])
158+
var size: ListRowSize = .small
159+
160+
@FigmaEnum("State", mapping: ["Idle": false, "Hover": false, "Active": false, "Disabled": true])
161+
var disabled: Bool = false
162+
163+
var paddingHorizontal: ListRowPaddingHorizontal = .medium
164+
165+
var body: some View {
166+
ListRowChevron {
167+
Text(title)
168+
}
169+
.size(self.size)
170+
.paddingHorizontal(self.paddingHorizontal)
171+
.divider(self.showDivider)
172+
.disabled(self.disabled)
173+
}
174+
}

swiftui/lib/CodeConnectTemplateWriter.swift

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,18 +242,39 @@ struct CodeConnectTemplateWriter {
242242
newSyntaxTree = rewriter.visit(newSyntaxTree)
243243
}
244244

245-
let rewrittenCode = newSyntaxTree.description.replaceConditionalTemplates(
245+
var rewrittenCode = newSyntaxTree.description.replaceConditionalTemplates(
246246
conditionalTemplates: rewriter.conditionalTemplates
247247
).replaceNestedInstancePlaceholders(
248248
nestedInstanceCalls: rewriter.nestedInstanceCalls
249249
).trimmingCharacters(in: .whitespacesAndNewlines)
250-
250+
251+
// Fix double-wrapped template variables like ${${foo}} -> ${foo}
252+
// This happens when ExprSyntax(stringLiteral: "${foo}") adds an extra wrapper (foo was already visited)
253+
rewrittenCode = rewrittenCode.fixDoubleWrappedTemplateVariables()
254+
251255
return "export default figma.swift`\(rewrittenCode)`"
252256
}
253257
}
254258

255259
fileprivate extension String {
256-
260+
// Fix double-wrapped template variables like ${${foo}} -> ${foo}
261+
func fixDoubleWrappedTemplateVariables() -> String {
262+
var result = self
263+
var changed = true
264+
while changed {
265+
let before = result
266+
// Match ${${ followed by anything that's not }, then }}
267+
if let range = result.range(of: #"\$\{\$\{([^}]+)\}\}"#, options: .regularExpression) {
268+
let match = String(result[range])
269+
// Extract the variable name from ${${VAR}}
270+
let varName = match.dropFirst(4).dropLast(2) // Remove ${ at start and }} at end, leaving ${VAR
271+
result.replaceSubrange(range, with: "${\(varName)}")
272+
}
273+
changed = (result != before)
274+
}
275+
return result
276+
}
277+
257278
/// In order to replace `figmaApply` and `hideDefault` calls, we use string substitution to find replacement strings and
258279
/// Insert the appropriate ternary expression.
259280
///

0 commit comments

Comments
 (0)