/** * Codemod: migrate-attachments * * Migrates from the deprecated image-upload API to the new attachments API. * * Transformations: * 1. JSX props on CopilotChat / CopilotSidebar / CopilotPopup: * - imageUploadsEnabled={true} → attachments={{ enabled: true }} * - inputFileAccept="..." → merged into attachments={{ accept: "..." }} * - Both props present → attachments={{ enabled: true, accept: "..." }} * * 2. Named imports from "@copilotkit/react-ui": * - ImageUploadQueue → AttachmentQueue * - ImageUpload (type) → Attachment (type) * * Usage: * npx jscodeshift -t ./codemods/migrate-attachments.ts --extensions=tsx,ts ./src */ import type { API, FileInfo, JSXElement, JSXAttribute, JSXExpressionContainer, ImportSpecifier, } from "jscodeshift"; const COPILOTKIT_PACKAGE = "@copilotkit/react-ui"; const TARGET_COMPONENTS = new Set([ "CopilotChat", "CopilotSidebar", "CopilotPopup", ]); const IMPORT_RENAMES: Record = { ImageUploadQueue: "AttachmentQueue", ImageUpload: "Attachment", }; export default function transform(file: FileInfo, api: API) { const j = api.jscodeshift; const root = j(file.source); let changed = false; // ----------------------------------------------------------------------- // 1. Rename imports from @copilotkit/react-ui // ----------------------------------------------------------------------- root .find(j.ImportDeclaration, { source: { value: COPILOTKIT_PACKAGE } }) .forEach((path) => { const specifiers = path.node.specifiers; if (!specifiers) return; for (const spec of specifiers) { if (spec.type !== "ImportSpecifier") continue; const imported = (spec as ImportSpecifier).imported; if (imported.type !== "Identifier") continue; const newName = IMPORT_RENAMES[imported.name]; if (!newName) continue; const localName = spec.local?.name ?? imported.name; const isAliased = localName !== imported.name; // Rename the imported identifier imported.name = newName; // If the local name matched the old imported name (not aliased), // update all references in the file to use the new name. if (!isAliased) { root.find(j.Identifier, { name: localName }).forEach((idPath) => { // Skip the import specifier itself — already renamed above if (idPath.parent.node === spec) return; idPath.node.name = newName; }); // Also update JSX element names (opening + closing tags) root.find(j.JSXIdentifier, { name: localName }).forEach((idPath) => { idPath.node.name = newName; }); if (spec.local) { spec.local.name = newName; } } changed = true; } }); // ----------------------------------------------------------------------- // 2. Transform JSX props on CopilotChat / CopilotSidebar / CopilotPopup // ----------------------------------------------------------------------- root.find(j.JSXOpeningElement).forEach((path) => { const nameNode = path.node.name; if (nameNode.type !== "JSXIdentifier") return; if (!TARGET_COMPONENTS.has(nameNode.name)) return; const attrs = path.node.attributes; if (!attrs) return; // Find the deprecated props let imageUploadsAttr: JSXAttribute | null = null; let inputFileAcceptAttr: JSXAttribute | null = null; let existingAttachmentsAttr: JSXAttribute | null = null; for (const attr of attrs) { if ( attr.type !== "JSXAttribute" || !attr.name || attr.name.type !== "JSXIdentifier" ) continue; if (attr.name.name === "imageUploadsEnabled") imageUploadsAttr = attr; if (attr.name.name === "inputFileAccept") inputFileAcceptAttr = attr; if (attr.name.name === "attachments") existingAttachmentsAttr = attr; } // Skip if neither deprecated prop is present if (!imageUploadsAttr && !inputFileAcceptAttr) return; // Skip if attachments prop already exists (already migrated or manual) if (existingAttachmentsAttr) return; // Build the attachments object properties const properties = []; if (imageUploadsAttr) { properties.push( j.objectProperty(j.identifier("enabled"), j.booleanLiteral(true)), ); } if (inputFileAcceptAttr) { let acceptValue: string | null = null; if (inputFileAcceptAttr.value) { if (inputFileAcceptAttr.value.type === "StringLiteral") { acceptValue = inputFileAcceptAttr.value.value; } else if ( inputFileAcceptAttr.value.type === "JSXExpressionContainer" && inputFileAcceptAttr.value.expression.type === "StringLiteral" ) { acceptValue = inputFileAcceptAttr.value.expression.value; } } if (acceptValue) { properties.push( j.objectProperty( j.identifier("accept"), j.stringLiteral(acceptValue), ), ); } } if (properties.length === 0) return; // Create: attachments={{ enabled: true, accept: "..." }} const attachmentsAttr = j.jsxAttribute( j.jsxIdentifier("attachments"), j.jsxExpressionContainer(j.objectExpression(properties)), ); // Remove old props, add new one path.node.attributes = attrs.filter( (attr) => attr !== imageUploadsAttr && attr !== inputFileAcceptAttr, ); path.node.attributes.push(attachmentsAttr); changed = true; }); return changed ? root.toSource() : undefined; }