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
2 changes: 2 additions & 0 deletions .claude/rules/lint-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ Uses `FigmaAPI.Client.request(SomeEndpoint(...))` directly (no convenience metho
- `NodesEndpoint` supports `geometry: .paths` parameter — returns `fillGeometry`/`strokeGeometry` with SVG path data on vector nodes. **Not suitable for pathData validation** — Figma's SVG export flattens masks/booleans into different paths than raw geometry
- `PathDataLengthRule` checks ALL platform icon entries (iOS/Android/Flutter/Web), deduplicates by fileId+frame+page. Downloads SVGs via `ImageEndpoint` + `URLSession`, parses with `SVGParser`, validates with `PathDataValidator`. Only reports critical >32,767 byte errors (800-char threshold removed as too noisy). Groups by fileId, batches ImageEndpoint by 50, parallelizes SVG downloads (max 10 concurrent) and fileIds
- `InvalidRTLVariantValueRule` validates RTL variant property values against configured `rtlActiveValues` (default `["On"]`) and their known counterpart pairs (On↔Off, true↔false, True↔False, Yes↔No, 0↔1). Uses Components API only (no ImageEndpoint). Collects icon entries from all platforms with `rtlProperty` and `rtlActiveValues`, deduplicates by fileId+frame+page+rtlProperty. `validateRTLValues` and `validValues(for:)` are internal for testability. Suggests either renaming in Figma or adding value to `rtlActiveValues` config
- `MockClient` keys responses by endpoint TYPE by default — multi-file rule tests need the fileId-aware overloads `setResponse(_:for:fileId:)` / `setError(_:for:fileId:)` (file-specific takes precedence over file-agnostic; fileId extracted from the `…/v1/files/{fileId}/…` request path)
- `IconColorVariablesLintRule` resolves expected variables by MERGING name indices from ALL candidate files (not requiring one file to hold all) — expected-but-absent variables emit `.warning` (config drift) while binding validation still runs against the merged index. Selector / empty-allow-list / no-component-match mismatches are `.warning` (config mistakes), not `.error`. `IconColorPolicy` (failable init rejects empty allow-list) and `PaintRole` are internal for testability; `variableLookupKeys` parses `VariableID:` prefix + `library-key/localId` cross-file refs. Samples `prefix(50)` like `DarkModeVariablesRule`
8 changes: 8 additions & 0 deletions .claude/rules/pkl-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ If `PklProject.deps.json` is missing, run: `cd Sources/ExFigCLI/Resources/Schema
Schemas: `Sources/ExFigCLI/Resources/Schemas/{ExFig,Common,Figma,iOS,Android,Flutter,Web}.pkl`
Output: `Sources/ExFigConfig/Generated/*.pkl.swift` (committed to repo)

## Codegen Side Effects

`codegen:pkl` rewrites ALL 9 `Generated/*.pkl.swift` even when one schema changed, and the
generator omits the trailing newline. This (a) breaks the `hk` `newlines` (end-of-file-fixer)
hook and (b) leaves 8 unrelated files with a 1-line newline-only diff.
Fix: `git checkout --` the files whose schema you didn't touch, then
`./bin/mise exec -- hk util end-of-file-fixer --fix Sources/ExFigConfig/Generated/<Changed>.pkl.swift`.

## Type Mapping

| PKL | Swift Generated |
Expand Down
2 changes: 2 additions & 0 deletions .claude/rules/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,5 @@ Common problems and solutions when working with ExFig.
| DocC `+=` infinite redirect | `--hosting-base-path` adds prefix — omit for local preview, use only for GitHub Pages |
| DocC preview port 8080 | `widget-simulator-service` occupies 8080 on macOS — use port 9090 or kill it |
| DocC icon invisible dark mode | `currentColor` in SVG doesn't work in `<img>` — use `~dark.svg` with `#ffffff` stroke |
| hk newlines hook fails on commit | `codegen:pkl` strips trailing newline from `Generated/*.pkl.swift`. Fix: `hk util end-of-file-fixer --fix <file>`; verify with `hk check --all` (exit 0). Hooks run via `bin/mise x -- hk run pre-commit` |
| `Document`/`Paint` has no `visible` | swift-figma-api `Document`/`Paint` expose only `opacity` (not `visible`); only `Effect` has `visible`. Detect hidden layers via `opacity == 0` |
26 changes: 15 additions & 11 deletions Sources/ExFigCLI/ExFig.docc/Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,9 @@ Validate your Figma file structure against your PKL config before exporting:
# Lint with default rules
exfig lint -i exfig.pkl

# Add lint-only policies from a separate config
exfig lint -i exfig.pkl --lint-config lint.pkl

# Only check specific rules
exfig lint -i exfig.pkl --rules naming-convention,deleted-variables

Expand All @@ -307,17 +310,18 @@ exfig lint -i exfig.pkl --format json --severity error

### Available Rules

| Rule | Severity | Description |
| -------------------------- | -------- | ------------------------------------------------------ |
| `frame-page-match` | error | Frame/page names in config exist in Figma file |
| `naming-convention` | error | Component names match `nameValidateRegexp` patterns |
| `component-not-frame` | error | Configured frames contain published components |
| `duplicate-component-names`| error | No duplicate component names in configured frames |
| `deleted-variables` | warning | No `deletedButReferenced` variables in collections |
| `alias-chain-integrity` | warning | Variable alias chains resolve without broken refs |
| `dark-mode-variables` | error | With `variablesDarkMode`, fills bound to Variables |
| `dark-mode-suffix` | warning | With `suffixDarkMode`, light components have dark pair |
| `path-data-length` | error | Icon SVG pathData within 32,767-byte AAPT limit |
| Rule | Severity | Description |
| --------------------------- | -------- | ------------------------------------------------------ |
| `frame-page-match` | error | Frame/page names in config exist in Figma file |
| `naming-convention` | error | Component names match `nameValidateRegexp` patterns |
| `component-not-frame` | error | Configured frames contain published components |
| `duplicate-component-names` | error | No duplicate component names in configured frames |
| `deleted-variables` | warning | No `deletedButReferenced` variables in collections |
| `alias-chain-integrity` | warning | Variable alias chains resolve without broken refs |
| `dark-mode-variables` | error | With `variablesDarkMode`, fills bound to Variables |
| `dark-mode-suffix` | warning | With `suffixDarkMode`, light components have dark pair |
| `path-data-length` | error | Icon SVG pathData within 32,767-byte AAPT limit |
| `icon-color-variables` | error | Icon paints use configured Figma Variables |

## Help and Version

Expand Down
29 changes: 29 additions & 0 deletions Sources/ExFigCLI/Lint/LintConfigLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import ExFigConfig
import Foundation

/// Loads optional lint-only overlay configuration for `exfig lint`.
enum LintConfigLoader {
static let defaultFileName = "lint.pkl"

static func resolvePath(explicitPath: String?, mainConfigPath: String?) -> URL? {
if let explicitPath, !explicitPath.isEmpty {
return URL(fileURLWithPath: explicitPath)
}

let mainPath = mainConfigPath ?? ExFigOptions.defaultConfigFilename
let mainURL = URL(fileURLWithPath: mainPath)
let lintURL = mainURL.deletingLastPathComponent().appendingPathComponent(defaultFileName)

guard FileManager.default.fileExists(atPath: lintURL.path) else {
return nil
}
return lintURL
}

static func load(explicitPath: String?, mainConfigPath: String?) async throws -> ExFigConfig.Lint.ModuleImpl? {
guard let path = resolvePath(explicitPath: explicitPath, mainConfigPath: mainConfigPath) else {
return nil
}
return try await PKLEvaluator.evaluateLintConfig(configPath: path)
}
}
1 change: 1 addition & 0 deletions Sources/ExFigCLI/Lint/LintEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,6 @@ extension LintEngine {
DarkModeSuffixRule(),
PathDataLengthRule(),
InvalidRTLVariantValueRule(),
IconColorVariablesLintRule(),
])
}
2 changes: 2 additions & 0 deletions Sources/ExFigCLI/Lint/LintTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ actor LintDataCache {
struct LintContext {
/// The resolved PKL configuration.
let config: ExFig.ModuleImpl
/// Optional lint-only overlay configuration.
let lintConfig: ExFigConfig.Lint.ModuleImpl?
/// Figma API client.
let client: any FigmaAPI.Client
/// Shared cache for Figma API responses across rules.
Expand Down
Loading
Loading