Created
October 10, 2025 15:48
-
-
Save nautilytics/43a63ecf7ea89f7de6d1f3ce272caffb to your computer and use it in GitHub Desktop.
Bring Percy snapshots into Figma
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
| // This file holds the main code for plugins. Code in this file has access to | |
| // the *figma document* via the figma global object. | |
| // You can access browser APIs in the <script> tag inside "ui.html" which has a | |
| // full browser environment (See https://www.figma.com/plugin-docs/how-plugins-run). | |
| const PERCY_TOKEN = '' | |
| const BASE_PERCY_API_URL = '' | |
| type PercyBuild = { | |
| data: [{ id: string }] | |
| } | |
| type PercySnapshots = { | |
| data: [{ attributes: { name: string }; type: 'snapshots'; id: string }] | |
| } | |
| type PercySnapshot = { | |
| included: [ | |
| { | |
| type: 'images' | 'browser-families' | |
| attributes: { slug: string; shortName: string; url: string } | |
| relationships: { image: { data: { id: string } } } | |
| id: string | |
| } | |
| ] | |
| } | |
| const getData = async (url: string) => { | |
| try { | |
| const response = await fetch(url, { | |
| method: 'GET', | |
| headers: { | |
| Authorization: `Token token=${PERCY_TOKEN}`, | |
| }, | |
| }) | |
| const data = await response.json() | |
| return data | |
| } catch (error) { | |
| console.error('Failed to retrieve data') | |
| return | |
| } | |
| } | |
| figma.showUI(__html__) | |
| // Calls to "parent.postMessage" from within the HTML page will trigger this | |
| // callback. The callback will be passed the "pluginMessage" property of the | |
| // posted message. | |
| figma.ui.onmessage = async (msg) => { | |
| if (msg.type === 'update-percy-snapshots') { | |
| const nodes: SceneNode[] = [] | |
| // Search for all the rectangles in the current page. | |
| const allRectangles = figma.currentPage.findAllWithCriteria({ | |
| types: ['RECTANGLE'], | |
| }) | |
| const nodeNames = allRectangles.map((node) => node.name) | |
| const releaseBuilds = (await getData(`${BASE_PERCY_API_URL}/builds`)) as PercyBuild | |
| if (releaseBuilds.data.length) { | |
| const releaseBuildId = releaseBuilds.data[0].id | |
| // Get snapshots for release build | |
| const { data } = (await getData( | |
| `${BASE_PERCY_API_URL}/snapshots?build_id=${releaseBuildId}` | |
| )) as PercySnapshots | |
| const availableSnapshots = nodeNames.map((node) => node.replace(/\s-\smobile/g, '')) | |
| // Go through each available snapshot name and get the image links | |
| for (const availableSnapshot of availableSnapshots) { | |
| const selectedSnapshot = data.find( | |
| (row) => row.type === 'snapshots' && row.attributes.name === availableSnapshot | |
| ) | |
| // Get snapshot details | |
| const { included } = (await getData( | |
| `${BASE_PERCY_API_URL}/snapshots/${selectedSnapshot!.id}` | |
| )) as PercySnapshot | |
| // Get image links for mobile snapshots | |
| const browser = { slug: 'chrome_on_android', shortName: 'Mobile' } | |
| const snapshotId = `${availableSnapshot} - ${browser.shortName.toLowerCase()}` | |
| if (included.length > 0) { | |
| const browserFamilyIdx = included.findIndex( | |
| (row) => row.type === 'browser-families' && row.attributes.slug === browser.slug | |
| ) | |
| const screenshotImageId = included[browserFamilyIdx + 1].relationships.image.data.id // the screenshot for the browser family is always the next row | |
| const screenshotImageUrl = included.find( | |
| (row) => row.type === 'images' && row.id === screenshotImageId | |
| )?.attributes?.url | |
| if (screenshotImageUrl) { | |
| let image | |
| try { | |
| image = await figma.createImageAsync(screenshotImageUrl) | |
| } catch (error) { | |
| console.log(`Error with ${snapshotId} skipping`) | |
| console.error(error) | |
| continue | |
| } | |
| // Go through each image on the page and replace the image with the most recent image | |
| for (let i = 0; i < allRectangles.length; i++) { | |
| const node = allRectangles[i] | |
| if (node.name === snapshotId) { | |
| console.log(`Updating ${snapshotId} rectangle`) | |
| const { width, height } = await image.getSizeAsync() | |
| node.resize(width, height) | |
| node.fills = [ | |
| { | |
| type: 'IMAGE', | |
| imageHash: image.hash, | |
| scaleMode: 'FILL', | |
| }, | |
| ] | |
| } | |
| nodes.push(node) | |
| } | |
| } | |
| } | |
| } | |
| // Zoom to the current selection of nodes | |
| figma.currentPage.selection = nodes | |
| figma.viewport.scrollAndZoomIntoView(nodes) | |
| // Make sure to close the plugin when you're done. Otherwise the plugin will | |
| // keep running, which shows the cancel button at the bottom of the screen. | |
| figma.closePlugin() | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment