Files
galaxy/resources/bundled/mcp_skills/figma/figma-generate-library/scripts/cleanupOrphans.js
T

128 lines
3.6 KiB
JavaScript

/**
* cleanupOrphans
*
* Finds and removes all Figma nodes (pages, frames, components, variables,
* and variable collections) that were tagged with the given `dsb_run_id`
* by a previous build run. This is safe cleanup: it uses plugin data tags,
* never name-prefix matching, so it cannot accidentally delete user-owned nodes.
*
* Use this when a build run fails mid-way and you need to reset to a clean
* slate before retrying. The function traverses the entire document looking
* for `dsb_run_id` plugin data matching `runId`.
*
* Variables and variable collections are handled separately (they are not
* scene nodes and cannot be discovered via node traversal).
*
* @param {string} runId - The dsb_run_id value to match (e.g. "ds-build-2024-001").
* @returns {Promise<{
* removedCount: number,
* removedIds: string[]
* }>}
*/
async function cleanupOrphans(runId) {
if (!runId) {
throw new Error('cleanupOrphans: runId is required.')
}
const removedIds = []
const originalPage = figma.currentPage
// --- Remove tagged scene nodes (pages, frames, components, etc.) ---
// Collect pages to remove (can't remove during iteration)
const pagesToRemove = []
for (const page of figma.root.children) {
if (page.getPluginData('dsb_run_id') === runId) {
pagesToRemove.push(page)
continue
}
// Traverse all nodes on this page
await figma.setCurrentPageAsync(page)
const nodesToRemove = []
page.findAll((node) => {
if (node.getPluginData('dsb_run_id') === runId) {
nodesToRemove.push(node)
return false // Don't descend — removing the parent removes its children
}
return true
})
// Remove deepest nodes first (children before parents) to avoid
// "parent no longer exists" errors
const sorted = nodesToRemove.sort((a, b) => {
// Sort by depth descending: deeper nodes first
return getDepth(b) - getDepth(a)
})
for (const node of sorted) {
if (node && node.parent) {
removedIds.push(node.id)
node.remove()
}
}
}
// Remove tagged pages last
for (const page of pagesToRemove) {
// Cannot remove the last page in the document
if (figma.root.children.length <= 1) {
break
}
removedIds.push(page.id)
page.remove()
}
// --- Remove tagged variables ---
const allVariables = await figma.variables.getLocalVariablesAsync()
for (const variable of allVariables) {
if (variable.getPluginData('dsb_run_id') === runId) {
removedIds.push(variable.id)
variable.remove()
}
}
// --- Remove tagged variable collections ---
// Must be done after variables are removed
const allCollections = await figma.variables.getLocalVariableCollectionsAsync()
for (const collection of allCollections) {
if (collection.getPluginData('dsb_run_id') === runId) {
removedIds.push(collection.id)
collection.remove()
}
}
// Restore original page (if it still exists)
try {
await figma.setCurrentPageAsync(originalPage)
} catch (_) {
// Original page was removed — switch to first available page
if (figma.root.children.length > 0) {
await figma.setCurrentPageAsync(figma.root.children[0])
}
}
return {
removedCount: removedIds.length,
removedIds,
}
}
/**
* Returns the depth of a node in the document tree.
* Root children (pages) have depth 1; their children have depth 2; etc.
*
* @param {BaseNode} node
* @returns {number}
*/
function getDepth(node) {
let depth = 0
let current = node
while (current.parent) {
depth++
current = current.parent
}
return depth
}