feat(figma): REST API client, URL parser, normalization (Phase B — T2F.0–T2F.4)#40
Open
canonical-muhammadbassiony wants to merge 3 commits into
Open
Conversation
- T2F.0: verify-figma Taskfile task, .env.example Figma vars - T2F.1: internal/figma/link.go - ParseLink for /file/ and /design/ URLs - T2F.2: FigmaToken/FigmaURL config wiring, FIGMA_TOKEN fallback - T2F.3: internal/figma/client.go - GetMeta, GetNodes, GetComments, GetImages, DownloadImage - T2F.4: internal/figma/normalize.go - Normalize() into NormalizedDesign
Contributor
There was a problem hiding this comment.
Pull request overview
Adds an initial internal/figma integration layer (URL parsing, REST client, raw/normalized types, normalization + tests) and begins wiring Figma into the source/config plumbing to serve as the foundation for later Figma→prompt mapping.
Changes:
- Introduces
internal/figmawithParseLink, REST client methods, raw API types, and a normalization layer (+ unit tests). - Updates
SourceBundle.Designto*figma.NormalizedDesignand addssource.Manager.FetchFigma. - Adds config validation for requiring a Figma token when a Figma URL is provided, plus a
verify-figmaTaskfile helper.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| Taskfile.yml | Adds/adjusts verify-figma task output and error handling. |
| internal/source/types.go | Types Design as *figma.NormalizedDesign instead of any. |
| internal/source/manager.go | Adds FetchFigma method to fetch meta/nodes/comments/images and normalize. |
| internal/figma/types.go | Defines raw Figma API response structs and Bauer normalized types. |
| internal/figma/normalize.go | Normalizes meta/nodes/comments/screenshot paths into NormalizedDesign. |
| internal/figma/normalize_test.go | Unit tests for normalization behavior. |
| internal/figma/link.go | Parses Figma URLs into LinkRef (file key + optional node-id). |
| internal/figma/link_test.go | Unit tests for URL parsing. |
| internal/figma/client.go | REST API client for meta/nodes/comments/images and image downloads. |
| internal/figma/client_test.go | HTTP-mocked unit tests for client methods and error handling. |
| internal/config/manager.go | Plumbs FigmaURL through FlagsSource.Load(). |
| internal/config/config.go | Validates Figma token presence when FigmaURL is set. |
| internal/config/cli.go | Adds --figma-url to this flag parser (but see PR comment about actual CLI entrypoint). |
| docs/implementation-log.md | Marks Phase B branch as done and records summary/files. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+46
to
+54
| meta, err := client.GetMeta(ctx, ref.FileKey) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("fetching figma metadata: %w", err) | ||
| } | ||
|
|
||
| nodeIDs := []string{} | ||
| if ref.NodeID != "" { | ||
| nodeIDs = []string{ref.NodeID} | ||
| } |
Comment on lines
+81
to
+83
| // DownloadImage downloads a pre-signed image URL (no auth needed) to destPath. | ||
| // The file is created with 0o644 permissions. | ||
| func (c *Client) DownloadImage(ctx context.Context, presignedURL, destPath string) error { |
Comment on lines
+73
to
+77
| // extractAnchors recursively extracts DesignAnchor values from a document node subtree. | ||
| // path is the breadcrumb from the root to the current node (used for NodePath). | ||
| func extractAnchors(nodeID string, doc *DocumentNode, path []string) []DesignAnchor { | ||
| currentPath := append(append([]string{}, path...), doc.Name) | ||
| anchor := DesignAnchor{ |
Comment on lines
+21
to
+25
| func ParseLink(rawURL string) (*LinkRef, error) { | ||
| matches := figmaFilePattern.FindStringSubmatch(rawURL) | ||
| if len(matches) < 2 { | ||
| return nil, fmt.Errorf("not a valid Figma link: %q (expected figma.com/file/... or figma.com/design/...)", rawURL) | ||
| } |
Comment on lines
45
to
+46
| artifactsDir := flag.String("artifacts-dir", "", "Directory for run artifacts (default: ./bauer-artifacts)") | ||
| figmaURL := flag.String("figma-url", "", "Figma file or design URL for design reference (optional)") |
Comment on lines
+22
to
+24
| u, err := url.Parse(rawURL) | ||
| if err != nil || (u.Host != "www.figma.com" && u.Host != "figma.com") { | ||
| return nil, fmt.Errorf("not a valid Figma link: %q (expected figma.com/file/... or figma.com/design/...)", rawURL) |
Comment on lines
+38
to
+42
| // Whole-file fetch: collect anchors from every node returned. | ||
| // Use a sorted iteration order if stability matters for tests. | ||
| for nodeID, entry := range nodes.Nodes { | ||
| design.Anchors = append(design.Anchors, extractAnchors(nodeID, &entry.Document, nil)...) | ||
| } |
Comment on lines
+59
to
+68
| // Map screenshot paths to ScreenshotArtifact records. | ||
| now := time.Now().UTC().Format(time.RFC3339) | ||
| for nodeID, localPath := range screenshotPaths { | ||
| design.Screenshots = append(design.Screenshots, ScreenshotArtifact{ | ||
| NodeID: nodeID, | ||
| LocalPath: localPath, | ||
| Scale: 2, | ||
| FetchedAt: now, | ||
| }) | ||
| } |
Comment on lines
+56
to
+60
| var nodes *figma.NodesResponse | ||
| if len(nodeIDs) == 0 { | ||
| fmt.Printf("warning: whole-file Figma link — no specific node requested, skipping node fetch\n") | ||
| nodes = &figma.NodesResponse{} | ||
| } else { |
Comment on lines
+82
to
+86
| continue | ||
| } | ||
| destPath := filepath.Join(screenshotDir, fmt.Sprintf("shot-node-%s.png", strings.ReplaceAll(nodeID, ":", "-"))) | ||
| if err := client.DownloadImage(ctx, imgURL, destPath); err != nil { | ||
| fmt.Printf("warning: could not download screenshot for node %s: %v\n", nodeID, err) |
| case http.StatusOK: | ||
| // success path; continue | ||
| case http.StatusUnauthorized, http.StatusForbidden: | ||
| return nil, fmt.Errorf("figma API authentication failed (status %d): check BAUER_FIGMA_TOKEN", resp.StatusCode) |
Comment on lines
+156
to
+162
| func (t *prefixTransport) RoundTrip(req *http.Request) (*http.Response, error) { | ||
| // Rewrite the host to point to the test server. | ||
| req2 := req.Clone(req.Context()) | ||
| req2.URL.Scheme = "http" | ||
| req2.URL.Host = t.base[len("http://"):] | ||
| return http.DefaultTransport.RoundTrip(req2) | ||
| } |
Comment on lines
+81
to
+109
| // DownloadImage downloads a pre-signed image URL (no auth needed) to destPath. | ||
| // The file is created with default permissions (0666 & ~umask). | ||
| func (c *Client) DownloadImage(ctx context.Context, presignedURL, destPath string) error { | ||
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, presignedURL, nil) | ||
| if err != nil { | ||
| return fmt.Errorf("creating download request: %w", err) | ||
| } | ||
|
|
||
| resp, err := c.http.Do(req) | ||
| if err != nil { | ||
| return fmt.Errorf("downloading image: %w", err) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return fmt.Errorf("image download failed: status %d", resp.StatusCode) | ||
| } | ||
|
|
||
| f, err := os.Create(destPath) | ||
| if err != nil { | ||
| return fmt.Errorf("creating image file: %w", err) | ||
| } | ||
| defer f.Close() | ||
|
|
||
| if _, err := io.Copy(f, resp.Body); err != nil { | ||
| return fmt.Errorf("writing image: %w", err) | ||
| } | ||
| return nil | ||
| } |
Collaborator
Author
There was a problem hiding this comment.
Acknowledged — adding tests for DownloadImage is out of scope for this PR. Tracked as a follow-up.
| ClientMeta CommentClientMeta `json:"client_meta"` | ||
| CreatedAt string `json:"created_at"` | ||
| User CommentUser `json:"user"` | ||
| ParentID string `json:"parent_id,omitempty"` |
Comment on lines
+28
to
+32
| if !strings.EqualFold(host, "www.figma.com") && !strings.EqualFold(host, "figma.com") { | ||
| return nil, fmt.Errorf("not a valid Figma link: %q (expected figma.com/file/... or figma.com/design/...)", rawURL) | ||
| } | ||
|
|
||
| matches := figmaFilePattern.FindStringSubmatch(rawURL) |
| case http.StatusTooManyRequests: | ||
| return nil, fmt.Errorf("figma API rate limit exceeded (status 429): retry after a delay") | ||
| case http.StatusNotFound: | ||
| return nil, fmt.Errorf("figma resource not found (status 404): check the file key and node ID") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces the
internal/figmapackage with URL parsing, REST API client, raw types, and a normalization layer. This is the foundation for all Figma integration.Tasks Implemented
verify-figmaTaskfile commandParseLink) — extracts file key and node ID from Figma/file/and/design/URLs--figma-urlCLI flag +Config.Validate()for token requirementGetMeta,GetNodes,GetComments,GetImages,DownloadImagewith structured error handling (401/403/429/404)Normalize()converts raw API types intoNormalizedDesignwithDesignAnchor,DesignComment,ScreenshotArtifactFiles Changed
internal/figma/link.go+link_test.go— URL parser with table-driven testsinternal/figma/types.go— raw API types + Bauer-owned normalized typesinternal/figma/client.go+client_test.go— REST client with mock HTTP testsinternal/figma/normalize.go+normalize_test.go— normalization logicinternal/source/types.go—Designfield typed as*figma.NormalizedDesigninternal/source/manager.go—FetchFigmamethodinternal/config/config.go—Validate()checks Figma token presencePart of the Bauer v2 stacked PR series (Branch 5 of 12).