Skip to content

Commit 9596b2a

Browse files
authored
feat(lint): Add configurable rtlActiveValues and invalid-rtl-variant-value rule (#81)
* feat(lint): add configurable rtlActiveValues and invalid-rtl-variant-value rule Add `rtlActiveValues` PKL config field (default `["On"]`) that controls which RTL variant values are skipped during export. Previously hardcoded to `"On"`, now designers using `true/false` or other conventions can configure this per-entry. New lint rule `invalid-rtl-variant-value` validates that RTL variant property values match the configured active values and their known counterparts (On↔Off, true↔false, True↔False, Yes↔No, 0↔1). Also improves the empty component error message to show both the icon name and variant name: `car (RTL=true)` instead of just `RTL=true`. * docs: add rtlActiveValues documentation to Configuration.md, examples, and rules * fix(lint): address review findings for rtlActiveValues - Fix hardcoded "RTL=On" in ImageLoaderBase log message and comments - Include rtlActiveValues in dedup key for InvalidRTLVariantValueRule - Add tests for shouldSkipAsRTLVariant with custom activeValues - Add rtlActiveValues pass-through assertion in IconsLoaderConfigTests - Test validValues with custom value not in knownPairs - Fix doc comment accuracy in InvalidRTLVariantValueRule - Improve Configuration.md example with separate default/alternative * fix(lint): address Gemini review — nil rtlProperty, images entries, fallbacks - Skip entries with nil rtlProperty (user explicitly disabled RTL) - Collect both icons and images entries for validation - Apply common.icons/images frame/page name fallbacks - Pre-calculate validValues outside component loop - Add test for nil rtlProperty skipping
1 parent 50f857d commit 9596b2a

37 files changed

Lines changed: 747 additions & 44 deletions

‎.claude/rules/lint-patterns.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ Uses `FigmaAPI.Client.request(SomeEndpoint(...))` directly (no convenience metho
2626
- `LintEngineTests.defaultEngineHasAllRules` checks exact set of rule IDs — must add new rule ID when registering in `LintEngine.default`
2727
- `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
2828
- `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
29+
- `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

‎.claude/rules/source-patterns.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Do NOT inject `colorsSource` at context construction time — it breaks multi-so
2828
- `Component.iconName`: uses `containingComponentSet.name` for variants, own `name` otherwise
2929
- `Component.codeConnectNodeId`: uses `containingComponentSet.nodeId` for variants, own `nodeId` otherwise (Figma Code Connect rejects variant node IDs)
3030
- `Component.defaultRTLProperty = "RTL"`: shared constant in ExFigCLI for the magic string
31+
- `rtlActiveValues: Listing<String>? = new { "On" }`: configurable per-entry list of variant values that mean "active RTL" (skipped during export). `shouldSkipAsRTLVariant(propertyName:activeValues:)` checks against this list. Known pairs (case-sensitive): Off↔On, off↔on, false↔true, False↔True, No↔Yes, no↔yes, 0↔1
3132
- PNG images intentionally do NOT carry `isRTL` — raster images skip mirroring by design
3233
- `buildPairedComponents` must use `iconName` (not `name`) — variant `name` is `"RTL=Off"`, not the icon name
3334

‎Sources/ExFig-Android/Config/AndroidIconsEntry.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public extension Android.IconsEntry {
2323
useSingleFile: darkFileId == nil && variablesDarkMode == nil,
2424
darkModeSuffix: "_dark",
2525
rtlProperty: rtlProperty,
26+
rtlActiveValues: rtlActiveValues,
2627
nameValidateRegexp: nameValidateRegexp,
2728
nameReplaceRegexp: nameReplaceRegexp,
2829
penpotBaseURL: resolvedPenpotBaseURL,

‎Sources/ExFig-Android/Config/AndroidImagesEntry.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public extension Android.ImagesEntry {
4141
useSingleFile: darkFileId == nil,
4242
darkModeSuffix: "_dark",
4343
rtlProperty: rtlProperty,
44+
rtlActiveValues: rtlActiveValues,
4445
nameValidateRegexp: nameValidateRegexp,
4546
nameReplaceRegexp: nameReplaceRegexp,
4647
penpotBaseURL: resolvedPenpotBaseURL

‎Sources/ExFig-Android/Export/AndroidImagesExporter.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ private extension AndroidImagesExporter {
446446
useSingleFile: true,
447447
darkModeSuffix: "_dark",
448448
rtlProperty: entry.rtlProperty,
449+
rtlActiveValues: entry.rtlActiveValues,
449450
nameValidateRegexp: entry.nameValidateRegexp,
450451
nameReplaceRegexp: entry.nameReplaceRegexp
451452
)

‎Sources/ExFig-Flutter/Config/FlutterIconsEntry.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public extension Flutter.IconsEntry {
1919
useSingleFile: darkFileId == nil && variablesDarkMode == nil,
2020
darkModeSuffix: "_dark",
2121
rtlProperty: rtlProperty,
22+
rtlActiveValues: rtlActiveValues,
2223
nameValidateRegexp: nameValidateRegexp,
2324
nameReplaceRegexp: nameReplaceRegexp,
2425
penpotBaseURL: resolvedPenpotBaseURL,

‎Sources/ExFig-Flutter/Config/FlutterImagesEntry.swift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public extension Flutter.ImagesEntry {
2424
useSingleFile: darkFileId == nil,
2525
darkModeSuffix: "_dark",
2626
rtlProperty: rtlProperty,
27+
rtlActiveValues: rtlActiveValues,
2728
nameValidateRegexp: nameValidateRegexp,
2829
nameReplaceRegexp: nameReplaceRegexp,
2930
penpotBaseURL: resolvedPenpotBaseURL
@@ -55,6 +56,7 @@ public extension Flutter.ImagesEntry {
5556
useSingleFile: darkFileId == nil,
5657
darkModeSuffix: "_dark",
5758
rtlProperty: rtlProperty,
59+
rtlActiveValues: rtlActiveValues,
5860
nameValidateRegexp: nameValidateRegexp,
5961
nameReplaceRegexp: nameReplaceRegexp,
6062
penpotBaseURL: resolvedPenpotBaseURL

‎Sources/ExFig-Web/Config/WebIconsEntry.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public extension Web.IconsEntry {
1919
useSingleFile: darkFileId == nil && variablesDarkMode == nil,
2020
darkModeSuffix: "_dark",
2121
rtlProperty: rtlProperty,
22+
rtlActiveValues: rtlActiveValues,
2223
nameValidateRegexp: nameValidateRegexp,
2324
nameReplaceRegexp: nameReplaceRegexp,
2425
penpotBaseURL: resolvedPenpotBaseURL,

‎Sources/ExFig-Web/Config/WebImagesEntry.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public extension Web.ImagesEntry {
2121
useSingleFile: darkFileId == nil,
2222
darkModeSuffix: "_dark",
2323
rtlProperty: rtlProperty,
24+
rtlActiveValues: rtlActiveValues,
2425
nameValidateRegexp: nameValidateRegexp,
2526
nameReplaceRegexp: nameReplaceRegexp,
2627
penpotBaseURL: resolvedPenpotBaseURL

‎Sources/ExFig-iOS/Config/iOSIconsEntry.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public extension iOS.IconsEntry {
2626
renderModeOriginalSuffix: renderModeOriginalSuffix,
2727
renderModeTemplateSuffix: renderModeTemplateSuffix,
2828
rtlProperty: rtlProperty,
29+
rtlActiveValues: rtlActiveValues,
2930
nameValidateRegexp: nameValidateRegexp,
3031
nameReplaceRegexp: nameReplaceRegexp,
3132
penpotBaseURL: resolvedPenpotBaseURL,

0 commit comments

Comments
 (0)