Skip to content

Commit a57ef14

Browse files
committed
chore(pkg): add schema versioning and iOS xcassetsPath handling
1 parent 6214d1c commit a57ef14

20 files changed

Lines changed: 248 additions & 22 deletions

.github/workflows/release.yml

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ jobs:
135135
VERSION="${GITHUB_REF#refs/tags/}"
136136
FILE="Sources/ExFigCLI/ExFigCommand.swift"
137137
sed -i "s/static let version = \".*\"/static let version = \"$VERSION\"/" "$FILE"
138+
sed -i "s/version = \".*\"/version = \"${VERSION#v}\"/" "Sources/ExFigCLI/Resources/Schemas/PklProject"
138139
139140
- name: Update CHANGELOG.md
140141
uses: orhun/git-cliff-action@v4
@@ -148,7 +149,7 @@ jobs:
148149
run: |
149150
git config user.name "github-actions[bot]"
150151
git config user.email "github-actions[bot]@users.noreply.github.com"
151-
git add Sources/ExFigCLI/ExFigCommand.swift CHANGELOG.md
152+
git add Sources/ExFigCLI/ExFigCommand.swift Sources/ExFigCLI/Resources/Schemas/PklProject CHANGELOG.md
152153
git diff --staged --quiet || git commit -m "chore(release): bump version to ${GITHUB_REF#refs/tags/v}"
153154
git push origin main
154155
@@ -175,12 +176,21 @@ jobs:
175176
env:
176177
GITHUB_REPO: ${{ github.repository }}
177178

178-
- name: Create schemas archive
179+
- name: Install Pkl CLI
180+
run: |
181+
curl -sL https://github.com/apple/pkl/releases/download/0.30.2/pkl-linux-amd64 -o /usr/local/bin/pkl
182+
chmod +x /usr/local/bin/pkl
183+
184+
- name: Update PklProject version
179185
run: |
180186
VERSION="${GITHUB_REF#refs/tags/v}"
181-
cd Sources/ExFigCLI/Resources/Schemas
182-
zip -r "../../../../exfig-schemas@${VERSION}.zip" \
183-
ExFig.pkl Figma.pkl Common.pkl iOS.pkl Android.pkl Flutter.pkl Web.pkl PklProject
187+
sed -i "s/version = \".*\"/version = \"$VERSION\"/" Sources/ExFigCLI/Resources/Schemas/PklProject
188+
189+
- name: Package PKL schemas
190+
run: |
191+
pkl project package \
192+
--output-path ".pkl-out/%{name}@%{version}/" \
193+
Sources/ExFigCLI/Resources/Schemas
184194
185195
- name: Create GitHub Release
186196
uses: softprops/action-gh-release@v2
@@ -190,7 +200,7 @@ jobs:
190200
files: |
191201
artifacts/exfig-macos/exfig-macos.zip
192202
artifacts/exfig-linux-x64/exfig-linux-x64.tar.gz
193-
exfig-schemas@*.zip
203+
.pkl-out/exfig@*/*
194204
195205
update-homebrew:
196206
name: Update Homebrew Tap

CONFIG.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ This creates a `.exfig/schemas/` directory with all schema files (`ExFig.pkl`, `
1616
`Android.pkl`, `Flutter.pkl`, `Web.pkl`). Your config file references these schemas via `amends` and `import`
1717
statements.
1818

19+
Alternatively, you can reference schemas directly via the published PKL package URI (no local extraction needed):
20+
21+
```pkl
22+
amends "package://github.com/alexey1312/ExFig/releases/download/v2.0.0/exfig@2.0.0#/ExFig.pkl"
23+
24+
import "package://github.com/alexey1312/ExFig/releases/download/v2.0.0/exfig@2.0.0#/iOS.pkl"
25+
import "package://github.com/alexey1312/ExFig/releases/download/v2.0.0/exfig@2.0.0#/Figma.pkl"
26+
import "package://github.com/alexey1312/ExFig/releases/download/v2.0.0/exfig@2.0.0#/Common.pkl"
27+
```
28+
29+
Replace `2.0.0` with your ExFig version. Using local schemas (`exfig schemas`) is recommended for faster evaluation
30+
and offline support.
31+
1932
## Quick Start
2033

2134
Generate a working configuration file for your platform:
@@ -309,13 +322,16 @@ ios = new iOS.iOSConfig {
309322
| ------------------------ | ------------------ | -------- | ------------------------------------------------------ |
310323
| `xcodeprojPath` | `String` | Yes | Path to `.xcodeproj` file |
311324
| `target` | `String` | Yes | Xcode target for resources and Swift code |
312-
| `xcassetsPath` | `String` | Yes | Path to `Assets.xcassets` directory |
325+
| `xcassetsPath` | `String?` | No* | Path to `Assets.xcassets` directory |
313326
| `xcassetsInMainBundle` | `Boolean` | Yes | Whether assets are in the main bundle |
314327
| `xcassetsInSwiftPackage` | `Boolean?` | No | Whether assets are in a Swift package (default: false) |
315328
| `resourceBundleNames` | `Listing<String>?` | No | Resource bundle names for SPM packages |
316329
| `addObjcAttribute` | `Boolean?` | No | Add `@objc` to generated properties (default: false) |
317330
| `templatesPath` | `String?` | No | Path to custom Stencil templates |
318331

332+
*Required when exporting colors (with `useColorAssets`), icons, or images. Can be omitted in base configs used only
333+
for inheritance.
334+
319335
### iOS Colors
320336

321337
```pkl
@@ -1002,6 +1018,52 @@ configuration.
10021018

10031019
---
10041020

1021+
## Common PKL Patterns
1022+
1023+
### Union Types: Single vs. Multiple Entries
1024+
1025+
ExFig schema fields like `colors`, `icons`, and `images` accept either a single entry or a `Listing` (array). PKL
1026+
cannot infer the type from `new { ... }` for union types, so you must use the typed constructor when writing multiple
1027+
entries.
1028+
1029+
**Single entry** — type is inferred, no explicit type needed:
1030+
1031+
```pkl
1032+
colors = new iOS.ColorsEntry {
1033+
figmaFrameName = "Colors"
1034+
useColorAssets = true
1035+
assetsFolder = "Colors"
1036+
nameStyle = "camelCase"
1037+
}
1038+
```
1039+
1040+
**Multiple entries** — MUST use typed constructor (`new Type { ... }`):
1041+
1042+
```pkl
1043+
colors = new Listing {
1044+
new iOS.ColorsEntry {
1045+
figmaFrameName = "Light Colors"
1046+
useColorAssets = true
1047+
assetsFolder = "Colors/Light"
1048+
nameStyle = "camelCase"
1049+
}
1050+
new iOS.ColorsEntry {
1051+
figmaFrameName = "Dark Colors"
1052+
useColorAssets = true
1053+
assetsFolder = "Colors/Dark"
1054+
nameStyle = "camelCase"
1055+
}
1056+
}
1057+
```
1058+
1059+
Without the typed constructor (e.g., `new { ... }` inside a Listing), PKL will report:
1060+
`Expected type iOS.ColorsEntry, but got Dynamic`.
1061+
1062+
This pattern applies to all platforms: `iOS.ColorsEntry`, `iOS.IconsEntry`, `iOS.ImagesEntry`,
1063+
`Android.ColorsEntry`, `Android.IconsEntry`, `Android.ImagesEntry`, `Flutter.ColorsEntry`, etc.
1064+
1065+
---
1066+
10051067
## Validation
10061068

10071069
Validate your config file before running exports:

Sources/ExFig-iOS/Config/iOSPlatformConfig.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ public struct iOSPlatformConfig: Sendable {
1616
public let target: String
1717

1818
/// Path to the .xcassets directory.
19-
public let xcassetsPath: URL
19+
/// Required for colors (with useColorAssets), icons, and images export.
20+
/// Can be nil in base configs that don't directly export assets.
21+
public let xcassetsPath: URL?
2022

2123
/// Whether assets are in the main bundle.
2224
public let xcassetsInMainBundle: Bool
@@ -42,7 +44,7 @@ public struct iOSPlatformConfig: Sendable {
4244
public init(
4345
xcodeprojPath: String,
4446
target: String,
45-
xcassetsPath: URL,
47+
xcassetsPath: URL? = nil,
4648
xcassetsInMainBundle: Bool = true,
4749
xcassetsInSwiftPackage: Bool? = nil,
4850
resourceBundleNames: [String]? = nil,

Sources/ExFig-iOS/Export/iOSColorsExporter.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ public struct iOSColorsExporter: ColorsExporter {
112112
guard let folder = entry.assetsFolder else {
113113
throw iOSColorsExportError.assetsFolderNotSpecified
114114
}
115-
colorsURL = platformConfig.xcassetsPath.appendingPathComponent(folder)
115+
guard let xcassetsPath = platformConfig.xcassetsPath else {
116+
throw iOSColorsExportError.xcassetsPathNotSpecified
117+
}
118+
colorsURL = xcassetsPath.appendingPathComponent(folder)
116119
}
117120

118121
// Create output configuration
@@ -148,18 +151,24 @@ public struct iOSColorsExporter: ColorsExporter {
148151
public enum iOSColorsExportError: LocalizedError {
149152
/// Assets folder not specified when useColorAssets is true.
150153
case assetsFolderNotSpecified
154+
/// xcassetsPath not specified when exporting color assets.
155+
case xcassetsPathNotSpecified
151156

152157
public var errorDescription: String? {
153158
switch self {
154159
case .assetsFolderNotSpecified:
155160
"assetsFolder is required when useColorAssets is true"
161+
case .xcassetsPathNotSpecified:
162+
"xcassetsPath is required for iOS colors export with useColorAssets"
156163
}
157164
}
158165

159166
public var recoverySuggestion: String? {
160167
switch self {
161168
case .assetsFolderNotSpecified:
162169
"Add 'assetsFolder' to your iOS colors configuration"
170+
case .xcassetsPathNotSpecified:
171+
"Add 'xcassetsPath' to your iOS configuration"
163172
}
164173
}
165174
}

Sources/ExFig-iOS/Export/iOSIconsExporter.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,10 @@ public struct iOSIconsExporter: IconsExporter {
132132
let iconPairs = processResult.iconPairs
133133

134134
// 3. Generate files
135-
let assetsURL = platformConfig.xcassetsPath.appendingPathComponent(entry.assetsFolder)
135+
guard let xcassetsPath = platformConfig.xcassetsPath else {
136+
throw iOSIconsExportError.xcassetsPathNotSpecified
137+
}
138+
let assetsURL = xcassetsPath.appendingPathComponent(entry.assetsFolder)
136139

137140
let output = XcodeImagesOutput(
138141
assetsFolderURL: assetsURL,
@@ -213,4 +216,26 @@ public struct iOSIconsExporter: IconsExporter {
213216
}
214217
}
215218

219+
// MARK: - Errors
220+
221+
/// Errors that can occur during iOS icons export.
222+
public enum iOSIconsExportError: LocalizedError {
223+
/// xcassetsPath not specified when exporting icons.
224+
case xcassetsPathNotSpecified
225+
226+
public var errorDescription: String? {
227+
switch self {
228+
case .xcassetsPathNotSpecified:
229+
"xcassetsPath is required for iOS icons export"
230+
}
231+
}
232+
233+
public var recoverySuggestion: String? {
234+
switch self {
235+
case .xcassetsPathNotSpecified:
236+
"Add 'xcassetsPath' to your iOS configuration"
237+
}
238+
}
239+
}
240+
216241
// swiftlint:enable type_name file_length

Sources/ExFig-iOS/Export/iOSImagesExporter.swift

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,10 @@ public struct iOSImagesExporter: ImagesExporter {
372372

373373
if let warning = processResult.warning { context.warning(warning) }
374374

375-
let assetsURL = platformConfig.xcassetsPath.appendingPathComponent(entry.assetsFolder)
375+
guard let xcassetsPath = platformConfig.xcassetsPath else {
376+
throw iOSImagesExportError.xcassetsPathNotSpecified
377+
}
378+
let assetsURL = xcassetsPath.appendingPathComponent(entry.assetsFolder)
376379
return (processResult.imagePairs, assetsURL, loadResult)
377380
}
378381

@@ -409,11 +412,36 @@ public struct iOSImagesExporter: ImagesExporter {
409412

410413
if let warning = processResult.warning { context.warning(warning) }
411414

412-
let assetsURL = platformConfig.xcassetsPath.appendingPathComponent(entry.assetsFolder)
415+
guard let xcassetsPath = platformConfig.xcassetsPath else {
416+
throw iOSImagesExportError.xcassetsPathNotSpecified
417+
}
418+
let assetsURL = xcassetsPath.appendingPathComponent(entry.assetsFolder)
413419
return (processResult.imagePairs, assetsURL, loadResult)
414420
}
415421
}
416422

423+
// MARK: - Errors
424+
425+
/// Errors that can occur during iOS images export.
426+
public enum iOSImagesExportError: LocalizedError {
427+
/// xcassetsPath not specified when exporting images.
428+
case xcassetsPathNotSpecified
429+
430+
public var errorDescription: String? {
431+
switch self {
432+
case .xcassetsPathNotSpecified:
433+
"xcassetsPath is required for iOS images export"
434+
}
435+
}
436+
437+
public var recoverySuggestion: String? {
438+
switch self {
439+
case .xcassetsPathNotSpecified:
440+
"Add 'xcassetsPath' to your iOS configuration"
441+
}
442+
}
443+
}
444+
417445
// MARK: - Entry Helpers
418446

419447
private extension iOSImagesEntry {

Sources/ExFigCLI/ExFigCommand.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,25 @@ extension ExFigCommand {
100100
terminalUI = TerminalUI(outputMode: outputMode)
101101
terminalUI.installSignalHandlers()
102102
}
103+
104+
/// Checks if extracted schemas match the current CLI version and warns if not.
105+
static func checkSchemaVersionIfNeeded() {
106+
guard let ui = terminalUI else { return }
107+
108+
switch SchemaExtractor.checkVersion() {
109+
case .matched, .noSchemasDirectory:
110+
break
111+
case let .mismatch(schemasVersion, cliVersion):
112+
ui.warning(
113+
"Extracted schemas (\(schemasVersion)) don't match ExFig version (\(cliVersion)). "
114+
+ "Run `exfig schemas --force` to update."
115+
)
116+
case .noVersionFile:
117+
// Schemas exist but no version file — extracted by an older version
118+
ui.warning(
119+
"Schema version unknown (extracted by an older ExFig version). "
120+
+ "Run `exfig schemas --force` to update."
121+
)
122+
}
123+
}
103124
}

Sources/ExFigCLI/Input/PKLConfig.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ struct PKLConfig: Decodable {
233233

234234
let xcodeprojPath: String
235235
let target: String
236-
let xcassetsPath: URL
236+
let xcassetsPath: URL?
237237
let xcassetsInMainBundle: Bool
238238
let xcassetsInSwiftPackage: Bool?
239239
let resourceBundleNames: [String]?

Sources/ExFigCLI/Resources/Schemas/ExFig.pkl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
///
66
/// Usage:
77
/// ```pkl
8-
/// amends "package://github.com/alexey1312/ExFig@2.0.0#/ExFig.pkl"
8+
/// amends "package://github.com/alexey1312/ExFig/releases/download/v2.0.0/exfig@2.0.0#/ExFig.pkl"
99
///
1010
/// figma {
1111
/// lightFileId = "xxx"

Sources/ExFigCLI/Resources/Schemas/PklProject

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ amends "pkl:Project"
22

33
package {
44
name = "exfig"
5-
baseUri = "package://github.com/alexey1312/ExFig"
5+
baseUri = "package://github.com/alexey1312/ExFig/releases/download/v\(version)/exfig"
66
version = "2.0.0"
7-
packageZipUrl = "https://github.com/alexey1312/ExFig/releases/download/v\(version)/exfig-schemas@\(version).zip"
7+
packageZipUrl = "https://github.com/alexey1312/ExFig/releases/download/v\(version)/\(name)@\(version).zip"
88
description = "ExFig configuration schemas for exporting Figma assets to iOS, Android, Flutter, and Web"
99
authors {
10-
"Alexey"
10+
"Alexey <alexey1312@users.noreply.github.com>"
1111
}
12+
sourceCode = "https://github.com/alexey1312/ExFig"
13+
sourceCodeUrlScheme = "https://github.com/alexey1312/ExFig/blob/v\(version)/Sources/ExFigCLI/Resources/Schemas%{path}#L%{line}-L%{endLine}"
1214
license = "MIT"
1315
}

0 commit comments

Comments
 (0)