Commit b882d0a
authored
feat(penpot): add Penpot as alternative design source (#76)
* feat(core): add Penpot API client and design source integration
Implement Penpot as a second design data source alongside Figma:
- New PenpotAPI module: HTTP client for Penpot RPC API with endpoint
protocol, retry logic, and response models (colors, components,
typographies) supporting dual String/Double decoding
- PenpotColorsSource, PenpotComponentsSource, PenpotTypographySource
wired into SourceFactory for seamless dispatch via sourceKind
- PenpotColorsConfig added to ColorsSourceConfig protocol family
- PenpotSource class in Common.pkl with auto-detection (penpotSource
set → sourceKind "penpot") and codegen
- Entry bridge methods updated to use resolvedSourceKind across all
four platform plugins
- 21 unit tests for model decoding, endpoint construction, and error
recovery suggestions
* docs: update CLAUDE.md with PenpotAPI module and source kind patterns
* docs: update module docs for PenpotAPI (15 modules, source dispatch)
* docs: add Penpot support to README, DocC articles, and llms-full.txt
* fix(penpot): resolve URL construction, base URL, error handling, and type safety issues
- Fix operator precedence bug in PenpotComponentsSource URL construction
- Use configurable base URL from penpotSource instead of hardcoded design.penpot.app
- Add resolvedFileId/resolvedPenpotBaseURL computed properties to Common_FrameSource
- Add penpotBaseURL field to IconsSourceInput, ImagesSourceInput, TypographySourceInput
- Replace JSONSerialization with Codable+CodingKeys+YYJSONEncoder in thumbnails endpoint
- Validate fileId before API calls instead of falling back to empty string
- Add ColorsConfigError.missingPenpotSource for correct error messaging
- Return nil from hexToRGBA on invalid hex with warning instead of silent black
- Handle CancellationError in retry loop to respect cooperative cancellation
- Include response body in download error messages for diagnostics
- Add preconditions for maxRetries and accessToken in BasePenpotClient
- Change PenpotTypography numeric fields from var to let (immutable)
* test(penpot): add tests for resolvedSourceKind, hexToRGBA, SourceFactory dispatch, and fileId validation
- Add FrameSourceResolvedSourceKindTests: auto-detect penpot, explicit override,
resolvedFileId priority, resolvedPenpotBaseURL
- Add PenpotColorsSourceInputTests: auto-detected penpot config, missingPenpotSource error
- Add HexToRGBATests: valid/invalid hex, opacity, whitespace, lowercase
- Add SourceFactoryPenpotTests: .penpot dispatch for all 3 source types + unsupported throws
- Add PenpotComponentsSourceValidationTests: nil/empty fileId throws descriptive error
- Extract Penpot tests from DesignSourceTests.swift to stay under file_length limit
* fix(penpot): fix GetProfile empty body and duplicate --timeout in fetch command
- GetProfileEndpoint now sends `{}` body instead of nil (Penpot returns
400 "malformed-json" for empty body)
- Resolve duplicate --timeout flag conflict in `exfig fetch` by inlining
HeavyFaultToleranceOptions fields and constructing via computed property
- Document API path difference (/api/main/methods/ vs /api/rpc/command/)
and Cloudflare behavior in PenpotAPI CLAUDE.md
- Add E2E test data documentation (README.md) for Penpot test file
- Exclude README.md from PenpotAPITests SPM resources
* docs(penpot): add Penpot file structure guide, MCP resources, and prompt enhancements
- Expand DesignRequirements.md with Penpot section: library colors,
components, typography, file organization, limitations, troubleshooting
- Add MCP guide resource (exfig://guides/DesignRequirements.md) served
from Resources/Guides/ since DocC articles aren't in Bundle.module
- Add `source` argument to setup-config MCP prompt (figma/penpot branches)
- Update troubleshoot-export prompt to check both auth tokens
- Add Penpot typography config example to Configuration.md
- Regenerate llms-full.txt with updated DocC content
- Update CLAUDE.md with timeout, DocC bundle, and Penpot API gotchas
* feat(cli): add Penpot support to init and fetch wizards
- Add WizardDesignSource enum — first wizard question is now "Figma or Penpot?"
- InitWizard Penpot flow: file UUID, base URL, path filters (no dark mode/variables)
- FetchWizard Penpot flow: file UUID, path filter, output directory
- Add extractPenpotFileId() for Penpot workspace URL parsing (file-id= query param)
- Add applyPenpotResult() in InitWizardTransform — removes figma section, inserts
penpotSource blocks into platform entries
- Add runPenpotFetch() in DownloadImages — downloads component thumbnails via PenpotAPI
- Update GenerateConfigFile to show PENPOT_ACCESS_TOKEN in next-steps for Penpot source
- Update test helpers with new InitWizardResult fields (designSource, penpotBaseURL)
* fix(penpot): fix API client bugs, improve error handling, and add missing tests
- Fix download() URL mangling by detecting absolute URLs
- Fix retry loop returning error response on final attempt (now throws)
- Fix asset download auth conflict with S3 presigned URLs (no Authorization header)
- Fix asset path: assets/by-id/ instead of assets/by-file-media-id/
- Extract PenpotClientFactory from PenpotColorsSource for shared usage
- Sort dictionary iteration for deterministic export order
- Replace force-unwrap wizardResult! with safe binding
- Wire iOS PluginIconsExport through SourceFactory for Penpot dispatch
- Add recovery suggestions for 500-level and network errors
- Wrap decoding errors with HTTP context (endpoint, status code)
- Add baseURL validation precondition in BasePenpotClient.init
- Add precondition(!fileId.isEmpty) to GetFileEndpoint
- Add warnings for empty API responses, unknown text transforms
- Remove unused mainInstanceId/mainInstancePage from PenpotComponent
- Fix doc comments (PenpotEndpoint, GetProfileEndpoint, PenpotTypography)
- Fix DesignTokens.md false Penpot claim, CLAUDE.md module count
- Add tests: applyPenpotResult, extractPenpotFileId, VariablesSource.resolvedSourceKind
* feat(cli): make FIGMA_PERSONAL_TOKEN optional and add --source penpot to fetch
ExFigOptions.validate() no longer throws when FIGMA_PERSONAL_TOKEN is missing.
Token is read lazily and only required when a Figma source is actually used.
SourceFactory guards the .figma branch and throws accessTokenNotFound there.
Added --source penpot/figma flag and --penpot-base-url to exfig fetch for
non-interactive Penpot usage without the wizard.
* feat(penpot): add SVG reconstruction from shape tree for icon/image export
Replace thumbnail-based component export with SVG reconstruction from
Penpot's shape tree data. This enables vector export (SVG/PNG/PDF) at
any scale without headless Chrome or CDN — shapes are parsed directly
from the get-file API response.
- Add PenpotShape model for shape tree decoding (path, rect, circle, bool, group, frame)
- Add PenpotShapeRenderer — pure function that converts shape tree → SVG string
- Add PenpotPage/SVGAttributes models, extend PenpotFileData with pagesIndex
- Rewrite PenpotComponentsSource to use SVG reconstruction instead of thumbnails
- Update runPenpotFetch to support --format svg/png with SVG→PNG via resvg
- Wire SourceFactory through all remaining platform exports (Android/Flutter/Web icons + all images)
- Restore mainInstanceId/mainInstancePage on PenpotComponent for shape tree lookup
- Handle mixed-type svgAttrs (strings + nested style dicts) via SVGAttributes wrapper
* docs(penpot): update docs for SVG reconstruction — remove raster-only limitation
- Remove "v1 limitation: raster thumbnails only" from DesignRequirements, Configuration, MCP prompts
- Add Penpot icons PKL config example to Configuration.md
- Add "Penpot Fetch" section to Usage.md with --source penpot examples
- Add "Quick Penpot Icons Export" to GettingStarted.md
- Update MCP prompt: "SVG reconstructed from shape tree" replaces raster limitation
- Update Known Limitations: add SVG reconstruction scope (no blur/shadow/gradients)
- Regenerate llms-full.txt
* fix(penpot): allow file:// URLs in FileDownloader for local SVG assets
Penpot SVG reconstruction writes SVGs to temp files and passes file://
URLs through the download pipeline. FileDownloader now skips HTTP
download for file:// URLs and returns the local path directly.
* fix(penpot): handle file:// URLs before download phase instead of weakening validation
Move file:// URL handling from validateDownloadURL (security gate) to
fetch() method — file:// URLs are filtered as local files before the
download loop, keeping validateDownloadURL strictly HTTPS-only.
* docs: add FileDownloader file:// pattern and iOS PKL xcassetsPath gotcha
* fix(penpot): improve type safety, error handling, and diagnostics across Penpot support
Replace placeholder FigmaClient("no-token") with NoTokenFigmaClient that
throws accessTokenNotFound on any call — prevents silent HTTP requests with
invalid credentials. Add ShapeType enum replacing stringly-typed shape
dispatch, MainInstance struct enforcing paired id+page invariant, and
structured RenderResult/RenderFailure for SVG reconstruction diagnostics.
Fix SVG arc command normalization to correctly offset only endpoint
coordinates (params 5-6 of 7-param groups). Add warnings for skipped
gradient colors, empty path filter results, and unknown shape types.
Remove force unwrap in FileDownloader, fix inverted letterSpacing warning
condition, and throw error for unsupported Penpot export formats instead
of silently saving SVG.
Includes 9 new tests covering nested groups, rotation transforms, arc
normalization, relative path commands, ShapeType decoding, and
RenderFailure diagnostics.
* ci: trigger DocC deploy on tag push instead of release event
Aligns deploy-docc.yml trigger with release.yml so both workflows
run in parallel when a version tag is pushed.
* chore(hk): disable stash in pre-commit hook
Switch from patch-file to none to avoid stash overhead during commits.
* refactor(penpot): extract PenpotAPI into external package swift-penpot-api
Move the standalone PenpotAPI module to DesignPipe/swift-penpot-api (v0.1.0),
mirroring the swift-figma-api extraction pattern. No code changes in consumer
files — `import PenpotAPI` continues to work via the external SPM dependency.
Also fix pre-existing test failure in ExFigErrorFormatterTests where the
assertion text didn't match the updated accessTokenNotFound error message.
* docs: update module structure, org name, and external package references
- Development.md: ExFig/ → ExFigCLI/, expand module list from 7 to 12,
add Source/, MCP/, JinjaSupport, WebExport, update test targets,
replace inline FigmaAPI guide with link to external package
- ARCHITECTURE.md: ExFig (CLI) → ExFigCLI, add External Packages section
(FigmaAPI, PenpotAPI, SVGKit), Stencil → Jinja2, update file structure
- PKL.md, MIGRATION.md: niceplaces → DesignPipe, fix Schemas path
Sources/ExFig/ → Sources/ExFigCLI/
* docs: consolidate docs/ into ExFig.docc, remove duplicate directory
Move ARCHITECTURE.md, PKL.md, MIGRATION.md from docs/ (which conflicts
with DocC output directory) into Sources/ExFigCLI/ExFig.docc/. Move
app-release-secrets.md to .github/. Update cross-references to use
DocC links and hosted documentation URLs.
* docs: clarify that docs/ is DocC output, source docs live in ExFig.docc
* docs: document nil-token behavior in resolveClient and list all source families
Update doc-comment for the HeavyFaultToleranceOptions resolveClient overload
to describe NoTokenFigmaClient fallback when accessToken is nil, matching the
existing FaultToleranceOptions overload. Update Source/ directory description
in CLAUDE.md to list all three source families (Figma, Penpot, TokensFile).
* fix(penpot): improve error handling, validation, and DI across Penpot support
- Validate Penpot format (svg/png) before API call instead of inside component loop
- Make resolvedFileId source-kind-aware to prevent passing Figma keys to Penpot API
- Replace silent .figma fallback on empty entries with explicit guard + error
- Add URL validation in PenpotClientFactory before client creation
- Remove synthetic FetchWizardResult with empty strings, pass penpotBaseURL directly
- Replace default branch with explicit cases in VariablesSourceValidation
- Add ColorsConfigError.unsupportedSourceKind for proper error at config layer
- Inject TerminalUI explicitly into SourceFactory instead of global static access
- Remove unused sourceKind parameter from PenpotComponentsSource.loadComponents
- Fix leaked swiftlint:disable cyclomatic_complexity in DownloadImages
* chore: update cover1 parent 3e31767 commit b882d0a
81 files changed
Lines changed: 5513 additions & 442 deletions
File tree
- .github
- workflows
- Sources
- ExFig-Android/Config
- ExFig-Flutter/Config
- ExFig-Web/Config
- ExFig-iOS/Config
- ExFigCLI
- Batch
- ExFig.docc
- Input
- MCP
- Output
- Resources
- Guides
- Schemas
- Source
- Subcommands
- Export
- ExFigConfig
- Generated
- PKL
- ExFigCore
- Protocol
- Tests/ExFigTests
- Input
- Source
- Subcommands
- TerminalUI
- openspec/changes/add-penpot-support
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
5 | | - | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
6 | 8 | | |
7 | 9 | | |
8 | 10 | | |
| |||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
260 | 260 | | |
261 | 261 | | |
262 | 262 | | |
263 | | - | |
264 | | - | |
| 263 | + | |
| 264 | + | |
265 | 265 | | |
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
| |||
36 | 37 | | |
37 | 38 | | |
38 | 39 | | |
| 40 | + | |
39 | 41 | | |
40 | 42 | | |
41 | 43 | | |
| |||
60 | 62 | | |
61 | 63 | | |
62 | 64 | | |
| 65 | + | |
63 | 66 | | |
64 | 67 | | |
65 | 68 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
| 6 | + | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| |||
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
27 | | - | |
| 28 | + | |
28 | 29 | | |
29 | 30 | | |
30 | 31 | | |
| |||
34 | 35 | | |
35 | 36 | | |
36 | 37 | | |
37 | | - | |
| 38 | + | |
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
18 | | - | |
| 17 | + | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
| 27 | + | |
| 28 | + | |
28 | 29 | | |
29 | 30 | | |
30 | 31 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
35 | | - | |
| 34 | + | |
| 35 | + | |
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
| 45 | + | |
| 46 | + | |
46 | 47 | | |
47 | 48 | | |
48 | 49 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
15 | | - | |
| 14 | + | |
| 15 | + | |
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
23 | | - | |
| 23 | + | |
| 24 | + | |
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
| |||
0 commit comments