Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .claude/rules/pkl-codegen.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# PKL Codegen (pkl-swift)

Config types are generated by `pkl-gen-swift` from PKL schemas. DO NOT edit `Sources/ExFigConfig/Generated/*.pkl.swift` manually.
Config types are generated from PKL schemas via `pkl run @pkl.swift/gen.pkl`. DO NOT edit `Sources/ExFigConfig/Generated/*.pkl.swift` manually.

## Regeneration

```bash
./bin/mise run codegen:pkl # requires pkl-gen-swift in PATH
# Or build from package dependency directly:
swift build --product pkl-gen-swift && .build/debug/pkl-gen-swift --output-path Sources/ExFigConfig/Generated/ Sources/ExFigCLI/Resources/Schemas/*.pkl
./bin/mise run codegen:pkl # requires pkl 0.31+ (uses pkl run @pkl.swift/gen.pkl)
```

The codegen uses `pkl run @pkl.swift/gen.pkl` (not the removed `pkl-gen-swift` binary).
Requires `PklProject.deps.json` and `generator-settings.pkl` in `Schemas/` directory.
If `PklProject.deps.json` is missing, run: `cd Sources/ExFigCLI/Resources/Schemas && pkl project resolve`

Schemas: `Sources/ExFigCLI/Resources/Schemas/{ExFig,Common,Figma,iOS,Android,Flutter,Web}.pkl`
Output: `Sources/ExFigConfig/Generated/*.pkl.swift` (committed to repo)

Expand All @@ -26,7 +28,7 @@ Output: `Sources/ExFigConfig/Generated/*.pkl.swift` (committed to repo)

## Enum Bridging

Generated enums use PKL raw values converted to Swift case names by pkl-gen-swift:
Generated enums use PKL raw values converted to Swift case names by the pkl-swift codegen:
- `"snake_case"` → `.snake_case` (underscore preserved)
- `"kebab-case"` → `.kebabCase` (hyphen converted to camelCase)
- `"SCREAMING_SNAKE_CASE"` → `.sCREAMING_SNAKE_CASE`
Expand Down
12 changes: 7 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Fourteen modules in `Sources/`:
| --------------- | --------------------------------------------------------- |
| `ExFigCLI` | CLI commands, loaders, file I/O, terminal UI |
| `ExFigCore` | Domain models (Color, Image, TextStyle), processors |
| `ExFigConfig` | PKL config parsing, evaluation, locator |
| `ExFigConfig` | PKL config parsing, evaluation, type bridging |
| `FigmaAPI` | Figma REST API client, endpoints, response models |
| `ExFig-iOS` | iOS platform plugin (ColorsExporter, IconsExporter, etc.) |
| `ExFig-Android` | Android platform plugin |
Expand Down Expand Up @@ -170,7 +170,7 @@ Sources/ExFig-{iOS,Android,Flutter,Web}/
└── Export/ # Exporters (iOSColorsExporter, AndroidImagesExporter, etc.)

Sources/ExFigConfig/
└── PKL/ # PKL locator, evaluator, error types
└── PKL/ # PKL evaluator, error types

Sources/ExFigCLI/Resources/
├── Schemas/ # PKL schemas (ExFig.pkl, iOS.pkl, Android.pkl, Flutter.pkl, Web.pkl, Common.pkl, Figma.pkl)
Expand Down Expand Up @@ -234,7 +234,7 @@ Filter predicate sites that ALL need updating:
When relocating a type (e.g., `Android.WebpOptions` → `Common.WebpOptions`), update ALL reference sites:

1. PKL schemas (`Schemas/*.pkl`) — definition + imports + field types
2. Codegen (`./bin/mise run codegen:pkl` or `.build/debug/pkl-gen-swift`)
2. Codegen (`./bin/mise run codegen:pkl`)
3. Swift bridging (`Sources/ExFig-*/Config/*Entry.swift`) — typealiases + extensions
4. Init-template configs (`Sources/ExFigCLI/Resources/*Config.swift`) — `new Type { }` refs
5. PKL examples (`Schemas/examples/*.pkl`)
Expand Down Expand Up @@ -352,13 +352,15 @@ NooraUI.formatLink("url", useColors: true) // underlined primary
| swift-resvg | 0.45.1 | SVG parsing/rendering |
| swift-docc-plugin | 1.4.5+ | DocC documentation |
| swift-yyjson | 0.5.0+ | High-performance JSON codec |
| pkl-swift | 0.7.2+ | PKL config evaluation & codegen |
| pkl-swift | 0.8.0+ | PKL config evaluation & codegen |

## Troubleshooting

| Problem | Solution |
| --------------------------- | ------------------------------------------------------------------------------------------------------------ |
| pkl-gen-swift not found | Build from SPM: `swift build --product pkl-gen-swift`, then `.build/debug/pkl-gen-swift` |
| codegen:pkl gen.pkl error | gen.pkl `read?` bug: needs `--generator-settings` + `--project-dir` flags (see mise.toml) |
| xcsift "signal code 5" | False positive when piping `swift test` through xcsift; run `swift test` directly to verify |
| PKL tests need Pkl 0.31+ | Schemas use `isNotEmpty`; run tests via `./bin/mise exec -- swift test` to get correct Pkl in PATH |
| PKL FrameSource change | Update ALL entry init calls in tests (EnumBridgingTests, IconsLoaderConfigTests) |
| Build fails | `swift package clean && swift build` |
| Tests fail | Check `FIGMA_PERSONAL_TOKEN` is set |
Expand Down
20 changes: 20 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,26 @@ Available per-entry overrides:
| Flutter | `figmaFileId`, `templatesPath` |
| Web | `figmaFileId`, `templatesPath` |

## Breaking: Pkl 0.31+ Required (v2.7.0)

ExFig now requires **Pkl 0.31+** (previously 0.30.x). The PKL schemas use `isNotEmpty` / `isNotBlank` constraint builtins introduced in Pkl 0.31 — older versions will fail with a parse error during config evaluation.

**How to upgrade:**

- **mise (recommended):** Automatic — `mise.toml` pins `pkl = "0.31.0"`, no action needed
- **Homebrew:** `brew upgrade pkl`
- **Manual:** Download from [pkl-lang.org/main/current/pkl-cli/index.html](https://pkl-lang.org/main/current/pkl-cli/index.html#installation)

Verify: `pkl --version` should show `0.31.0` or higher.

**What changed internally:**

- pkl-swift upgraded from 0.7.2 to 0.8.0 (new `registerPklTypes` API for faster type resolution)
- Codegen switched from removed `pkl-gen-swift` binary to `pkl run @pkl.swift/gen.pkl`
- PKL schemas use `isNotEmpty` instead of `!isEmpty` for constraint validation

**Impact on consumer configs:** None — `.pkl` config files are fully compatible. Only the Pkl CLI version requirement changed.

## Cleanup

After successful migration:
Expand Down
6 changes: 3 additions & 3 deletions Package.resolved

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

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ let package = Package(
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.4.5"),
.package(url: "https://github.com/alexey1312/swift-resvg.git", exact: "0.45.1-swift.3"),
.package(url: "https://github.com/mattt/swift-yyjson", from: "0.5.0"),
.package(url: "https://github.com/apple/pkl-swift", from: "0.7.2"),
.package(url: "https://github.com/apple/pkl-swift", from: "0.8.0"),
],
targets: [
// Main target
Expand Down
2 changes: 1 addition & 1 deletion Sources/ExFigCLI/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,5 +196,5 @@ Never add new `@TaskLocal` properties. Add fields to `BatchSharedState` actor or
- `ExFigCommand.terminalUI` is `nonisolated(unsafe) static var` — initialized once per subcommand via `initializeTerminalUI()`
- `ExFigCommand.fileWriter`, `.svgFileConverter`, `.logger` are `static let` — safe shared instances
- `resolveClient()` is a free function (in a separate file) that creates `FigmaClient` → wraps in `RateLimitedClient`
- PKL config type is `ExFig.ModuleImpl` (generated by pkl-gen-swift), aliased as `PKLConfig` in `PKLConfigCompat.swift`
- PKL config type is `ExFig.ModuleImpl` (generated by `pkl run @pkl.swift/gen.pkl`), aliased as `PKLConfig` in `PKLConfigCompat.swift`
- `BatchContextStorage` is a legacy shim — use `BatchSharedState.current` directly
8 changes: 4 additions & 4 deletions Sources/ExFigCLI/Resources/Schemas/Android.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class ThemeAttributes {
stylesNightFile: String? = "values-night/styles.xml"

/// Theme name used in markers (e.g., "Theme.MyApp.Main").
themeName: String(!isEmpty)
themeName: String(isNotEmpty)

/// Custom marker start text.
markerStart: String?
Expand Down Expand Up @@ -100,7 +100,7 @@ class IconsEntry extends Common.FrameSource {
templatesPath: String?

/// Output directory for vector drawables.
output: String(!isEmpty)
output: String(isNotEmpty)

/// Package name for generated Compose icons.
composePackageName: String?
Expand Down Expand Up @@ -140,7 +140,7 @@ class ImagesEntry extends Common.FrameSource {
scales: Listing<Number>? = new Listing { 1.0; 1.5; 2.0; 3.0; 4.0 }

/// Output directory for images.
output: String(!isEmpty)
output: String(isNotEmpty)

/// Output format for images.
format: ImageFormat = "png"
Expand Down Expand Up @@ -183,7 +183,7 @@ class Typography {
/// Root Android platform configuration.
class AndroidConfig {
/// Path to main res directory.
mainRes: String(!isEmpty)
mainRes: String(isNotEmpty)

/// Resource package name (R class package).
resourcePackage: String?
Expand Down
8 changes: 4 additions & 4 deletions Sources/ExFigCLI/Resources/Schemas/Common.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class WebpOptions {
/// When set on a colors entry, bypasses Figma API and reads tokens from a local file.
class TokensFile {
/// Path to the .tokens.json file.
path: String(!isEmpty)
path: String(isNotEmpty)

/// Optional dot-path prefix to filter tokens (e.g., "Brand.Colors").
groupFilter: String?
Expand Down Expand Up @@ -177,13 +177,13 @@ class Typography extends NameProcessing {
/// Used when all platforms share the same color source via common.variablesColors.
class VariablesColors extends NameProcessing {
/// Figma file ID containing the variables (required).
tokensFileId: String(!isEmpty)
tokensFileId: String(isNotEmpty)

/// Name of the variable collection (required).
tokensCollectionName: String(!isEmpty)
tokensCollectionName: String(isNotEmpty)

/// Mode name for light theme (required).
lightModeName: String(!isEmpty)
lightModeName: String(isNotEmpty)

/// Mode name for dark theme.
darkModeName: String?
Expand Down
6 changes: 3 additions & 3 deletions Sources/ExFigCLI/Resources/Schemas/Flutter.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class IconsEntry extends Common.FrameSource {
templatesPath: String?

/// Output directory for icon SVG files.
output: String(!isEmpty)
output: String(isNotEmpty)

/// Dart file path for icon class generation.
dartFile: String?
Expand All @@ -51,7 +51,7 @@ class ImagesEntry extends Common.FrameSource {
templatesPath: String?

/// Output directory for image files.
output: String(!isEmpty)
output: String(isNotEmpty)

/// Dart file path for image class generation.
dartFile: String?
Expand Down Expand Up @@ -80,7 +80,7 @@ class ImagesEntry extends Common.FrameSource {
/// Root Flutter platform configuration.
class FlutterConfig {
/// Base output directory for all generated files.
output: String(!isEmpty)
output: String(isNotEmpty)

/// Path to custom Jinja2 templates.
templatesPath: String?
Expand Down
6 changes: 6 additions & 0 deletions Sources/ExFigCLI/Resources/Schemas/PklProject
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ package {
sourceCodeUrlScheme = "https://github.com/alexey1312/ExFig/blob/v\(version)/Sources/ExFigCLI/Resources/Schemas%{path}#L%{line}-L%{endLine}"
license = "MIT"
}

dependencies {
["pkl.swift"] {
uri = "package://pkg.pkl-lang.org/pkl-swift/pkl.swift@0.8.0"
}
}
12 changes: 12 additions & 0 deletions Sources/ExFigCLI/Resources/Schemas/PklProject.deps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://pkg.pkl-lang.org/pkl-swift/pkl.swift@0": {
"type": "remote",
"uri": "projectpackage://pkg.pkl-lang.org/pkl-swift/pkl.swift@0.8.0",
"checksums": {
"sha256": "78099089810b37e32b06a0ab9afdc6e53e27f529ea766f3262ebf9c485234b3d"
}
}
}
}
6 changes: 3 additions & 3 deletions Sources/ExFigCLI/Resources/Schemas/Web.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class IconsEntry extends Common.FrameSource {
templatesPath: String?

/// Output directory for generated icon components.
outputDirectory: String(!isEmpty)
outputDirectory: String(isNotEmpty)

/// Directory for raw SVG files.
svgDirectory: String?
Expand All @@ -61,7 +61,7 @@ class ImagesEntry extends Common.FrameSource {
templatesPath: String?

/// Output directory for generated image components.
outputDirectory: String(!isEmpty)
outputDirectory: String(isNotEmpty)

/// Directory for image asset files.
assetsDirectory: String?
Expand All @@ -78,7 +78,7 @@ class ImagesEntry extends Common.FrameSource {
/// Root Web platform configuration.
class WebConfig {
/// Base output directory for all generated files.
output: String(!isEmpty)
output: String(isNotEmpty)

/// Path to custom Jinja2 templates.
templatesPath: String?
Expand Down
1 change: 1 addition & 0 deletions Sources/ExFigCLI/Resources/Schemas/generator-settings.pkl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
amends "package://pkg.pkl-lang.org/pkl-swift/pkl.swift@0.8.0#/GeneratorSettings.pkl"
6 changes: 3 additions & 3 deletions Sources/ExFigCLI/Resources/Schemas/iOS.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class IconsEntry extends Common.FrameSource {
/// iOS images entry configuration.
class ImagesEntry extends Common.FrameSource {
/// Path to .xcassets folder for images.
assetsFolder: String(!isEmpty)
assetsFolder: String(isNotEmpty)

/// Naming style for generated image names.
nameStyle: Common.NameStyle = "camelCase"
Expand Down Expand Up @@ -199,10 +199,10 @@ class Typography {
/// Root iOS platform configuration.
class iOSConfig {
/// Path to .xcodeproj file.
xcodeprojPath: String(!isEmpty)
xcodeprojPath: String(isNotEmpty)

/// Xcode target name.
target: String(!isEmpty)
target: String(isNotEmpty)

/// Path to main .xcassets folder.
/// Required for colors (with useColorAssets), icons, and images export.
Expand Down
13 changes: 10 additions & 3 deletions Sources/ExFigConfig/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

ExFigConfig evaluates PKL configuration files into strongly-typed Swift models and bridges them to ExFigCore domain types. It has three layers:

1. **Generated types** (`Generated/*.pkl.swift`) — structs, protocols, and enums produced by `pkl-gen-swift` from PKL schemas. Never edit manually.
2. **PKL evaluator** (`PKL/`) — async entry point that evaluates `.pkl` files via PklSwift's embedded evaluator (no CLI dependency).
1. **Generated types** (`Generated/*.pkl.swift`) — structs, protocols, and enums produced by `pkl run @pkl.swift/gen.pkl` from PKL schemas. Never edit manually.
2. **PKL evaluator** (`PKL/`) — async entry point that spawns `pkl` CLI and communicates via MessagePack protocol. Requires `pkl` 0.31+ in PATH.
3. **Bridging extensions** — convert PKL types to ExFigCore types (`NameStyleBridging.swift`, `VariablesSourceValidation.swift`).

## Commands
Expand All @@ -28,7 +28,7 @@ ExFigConfig evaluates PKL configuration files into strongly-typed Swift models a

```
PKL schemas (Sources/ExFigCLI/Resources/Schemas/*.pkl)
↓ pkl-gen-swift
↓ pkl run @pkl.swift/gen.pkl
Generated/*.pkl.swift (ExFig, Common, Figma, iOS, Android, Flutter, Web)
↓ PklSwift evaluator
PKLEvaluator.evaluate(configPath:) → ExFig.ModuleImpl
Expand Down Expand Up @@ -59,6 +59,13 @@ ExFigCore domain types (NameStyle, ColorsSourceInput, etc.)

`PklSwift.PklError` doesn't conform to `LocalizedError`. The `@retroactive` extension in `PKLEvaluator.swift` exposes `.message` — without it, `.localizedDescription` returns a useless generic string.

### registerPklTypes Pattern

`PKLEvaluator` uses `registerPklTypes(_:)` (pkl-swift 0.8.0+) to bypass O(N) type scanning.
Uses `static let _typeRegistration` for thread-safe dispatch_once semantics.
When adding new PKL types to schemas, regenerate with `codegen:pkl` and add the new type to the registration list in `PKLEvaluator.swift`.
`registerPklTypes` has a hard `precondition(_shared == nil)` — must be called before any `TypeRegistry.get()`.

## Codegen Gotchas

- PKL `"kebab-case"` raw values become `.kebabCase` in Swift (not `.kebab_case`)
Expand Down
Loading
Loading