UNPKG

@figma/code-connect

Version:

A tool for connecting your design system components in code with your design system in Figma

360 lines (357 loc) 14.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.isBooleanKind = isBooleanKind; exports.generateExpressionFromIntrinsic = generateExpressionFromIntrinsic; exports.getSetOfAllPropsReferencedInPropMapping = getSetOfAllPropsReferencedInPropMapping; exports.generateProps = generateProps; exports.createReactCodeConnect = createReactCodeConnect; const lodash_1 = require("lodash"); const prettier = __importStar(require("prettier")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const create_1 = require("../connect/create"); const intrinsics_1 = require("../connect/intrinsics"); const create_common_1 = require("../connect/create_common"); function isBooleanKind(propValue) { const normalized = propValue.toLowerCase(); return (normalized === 'true' || normalized === 'false' || normalized === 'yes' || normalized === 'no' || normalized === 'on' || normalized === 'off'); } function normalizePropName(name) { return name.replace(/#[0-9:]*/g, ''); } function generateCodePropName(name) { const cleanedName = name // Remove any #node:id from the end of the string .replace(/#[0-9:]+$/g, '') // Remove any special characters .replace(/[^a-zA-Z0-9\s]/g, ''); return (0, lodash_1.camelCase)(cleanedName); } function normalizePropValue(name) { // Convert the string to kebab-case return name.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase(); } function generateExpressionValueMappingKind(valueMappingKind) { if (valueMappingKind && typeof valueMappingKind === 'object' && 'kind' in valueMappingKind) { return generateExpressionFromIntrinsic(valueMappingKind); } if (typeof valueMappingKind === 'string') { return `"${valueMappingKind}"`; } if (typeof valueMappingKind === 'number' || typeof valueMappingKind === 'boolean' || typeof valueMappingKind === 'undefined') { return `${valueMappingKind}`; } } function generateExpressionValueMapping(valueMapping) { return `{ ${Object.entries(valueMapping) .map(([k, v]) => `"${k}": ${generateExpressionValueMappingKind(v)}`) .join(`,\n`)} }`; } // Not an exhaustive list of intrinsics but can add others as/when they're supported in prop mapping gen function generateExpressionFromIntrinsic({ kind, args }) { if (kind === intrinsics_1.IntrinsicKind.String) { return `figma.string("${args.figmaPropName}")`; } if (kind === intrinsics_1.IntrinsicKind.Boolean) { return `figma.boolean("${args.figmaPropName}"${args.valueMapping ? `, ${generateExpressionValueMapping(args.valueMapping)}` : ''})`; } if (kind === intrinsics_1.IntrinsicKind.Enum) { return `figma.enum("${args.figmaPropName}"${args.valueMapping ? `, ${generateExpressionValueMapping(args.valueMapping)}` : ''})`; } if (kind === intrinsics_1.IntrinsicKind.Instance) { return `figma.instance("${args.figmaPropName}")`; } if (kind === intrinsics_1.IntrinsicKind.Children) { return `figma.children(${args.layers.length > 1 ? `[${args.layers.map((layer) => `"${layer}"`).join(', ')}]` : `"${args.layers[0]}"`})`; } if (kind === intrinsics_1.IntrinsicKind.TextContent) { return `figma.textContent("${args.layer}")`; } // should never reach here as we create prop mappings. throw new Error(`kind ${kind} not supported for prop mapping`); } function generateSinglePropMappingFromFigmaProp(propName, propDef) { const codePropName = generateCodePropName(propName); const figmaPropName = normalizePropName(propName); if (propDef.type === 'BOOLEAN') { return `"${codePropName}": figma.boolean('${figmaPropName}')`; } if (propDef.type === 'TEXT') { return `"${codePropName}": figma.string('${figmaPropName}')`; } if (propDef.type === 'VARIANT') { const isBooleanVariant = propDef.variantOptions?.length === 2 && propDef.variantOptions.every(isBooleanKind); if (isBooleanVariant) { return `"${codePropName}": figma.boolean('${figmaPropName}')`; } else { return `"${codePropName}": figma.enum('${figmaPropName}', { \n${propDef.variantOptions ?.map((value) => ` "${value}": "${normalizePropValue(value)}"`) .join(',\n')}\n})`; } } if (propDef.type === 'INSTANCE_SWAP') { return `"${codePropName}": figma.instance('${figmaPropName}')`; } return null; } function getSetOfAllPropsReferencedInPropMapping(obj) { const mappedProps = []; Object.entries(obj).forEach(([k, v]) => { if (k === 'figmaPropName') { mappedProps.push(v); } if (typeof v === 'object') { mappedProps.push(...getSetOfAllPropsReferencedInPropMapping(v)); } }); return new Set(mappedProps); } function generatePropsFromMapping(component, propMapping) { const mappedProps = []; const unmappedProps = []; for (const [propName, intrinsic] of Object.entries(propMapping)) { const expr = generateExpressionFromIntrinsic(intrinsic); if (expr) { mappedProps.push(`"${propName}": ${expr}`); } } const usedFigmaPropsSet = getSetOfAllPropsReferencedInPropMapping(propMapping); for (const [propName, propDef] of Object.entries(component.componentPropertyDefinitions || {})) { if (!usedFigmaPropsSet.has(propName)) { const propMapping = generateSinglePropMappingFromFigmaProp(propName, propDef); if (propMapping) { unmappedProps.push(propMapping); } } } return `{ ${mappedProps.length ? `// These props were automatically mapped based on your linked code: ${mappedProps.join(',\n')},` : ''} ${unmappedProps.length ? `// No matching props could be found for these Figma properties: ${unmappedProps .map((prop) => { // Comment out to make clear these are suggested. Singly-commented out lines for ease of uncommenting return `// ${prop.replace(/\n/g, '\n// ')}`; }) .join(',\n')}` : ''} }`; } function generateProps(component) { const props = []; if (!component.componentPropertyDefinitions || Object.keys(component.componentPropertyDefinitions).length === 0) { return `{}`; } for (const [propName, propDef] of Object.entries(component.componentPropertyDefinitions)) { const propMapping = generateSinglePropMappingFromFigmaProp(propName, propDef); if (propMapping) { props.push(propMapping); } } return `{ ${props.join(',\n ')} }`; } function generateExample(component, signature, propMapping) { if (!signature) { return `<${component} />`; } const props = Object.entries(signature) .map(([propName, propDef]) => { // Children are rendered inside of the example body rather than in a prop if (propName === 'children') { return null; } else if (propMapping && propMapping[propName]) { return `${propName}={props.${propName}}`; } else if (!propDef.startsWith('?')) { return `${propName}={/* TODO */} `; } else { return null; } }) .filter(Boolean) .join('\n'); // const childProp = propMapping?['children'] ? `{props.${propMapping['children']}}` : null // Nest child props inside of the element if (signature['children'] && propMapping?.['children']) { return `<${component} ${props}> {props.children} </${component}>`; } else { return `<${component} ${props}/>`; } } // returns ES-style import path from given system path function formatImportPath(systemPath) { // use forward slashes for import paths let formattedImportPath = systemPath.replaceAll(path_1.default.sep, '/'); // prefix current dir paths with ./ (node path does not) if (!formattedImportPath.startsWith('.')) { formattedImportPath = `./${formattedImportPath}`; } // assume not using ESM imports return formattedImportPath.replace(/\.(jsx|tsx)$/, ''); } function getImportsPath({ codeConnectFilePath, sourceFilepath, normalizedName, }) { if (!sourceFilepath) { return `./${normalizedName}`; } const codeConnectFolder = path_1.default.dirname(codeConnectFilePath); const pathToComponentFile = path_1.default.relative(codeConnectFolder, sourceFilepath); return formatImportPath(pathToComponentFile); } async function createReactCodeConnect(payload) { const { figmaConnections, destinationFile, destinationDir, sourceFilepath, normalizedName } = payload; const comments = { MAPPED_PROPS: ` * \`props\` includes a mapping from your code props to Figma properties. * You should check this is correct, and update the \`example\` function * to return the code example you'd like to see in Figma`, NO_MAPPED_PROPS: ` * None of your props could be automatically mapped to Figma properties. * You should update the \`props\` object to include a mapping from your * code props to Figma properties, and update the \`example\` function to * return the code example you'd like to see in Figma`, DEFAULT: ` * \`props\` includes a mapping from Figma properties and variants to * suggested values. You should update this to match the props of your * code component, and update the \`example\` function to return the * code example you'd like to see in Figma`, }; const sourceFilename = sourceFilepath ? path_1.default.parse(sourceFilepath).name.split('.')[0] : normalizedName; const filePath = (0, create_common_1.getOutFileName)({ outFile: destinationFile, outDir: destinationDir, sourceFilename, extension: 'tsx', }); const importsPath = getImportsPath({ codeConnectFilePath: filePath, sourceFilepath, normalizedName, }); let defaultImport = ''; const namedImports = {}; for (const figmaConnection of figmaConnections) { const { sourceExport, component } = figmaConnection; const importName = sourceFilepath && sourceExport ? sourceExport === 'default' ? (0, create_1.normalizeComponentName)(sourceFilename) : sourceExport : normalizedName; if (sourceExport === 'default') { defaultImport = importName; } else { namedImports[component.id] = importName; } } if (defaultImport !== '') { defaultImport = Object.values(namedImports).includes(defaultImport) ? `${defaultImport}Default` : defaultImport; } let codeConnectCode = ''; for (const figmaConnection of figmaConnections) { const { propMapping, sourceExport, component, reactTypeSignature } = figmaConnection; const hasAnyMappedProps = propMapping && Object.keys(propMapping).length > 0; const commentType = propMapping && hasAnyMappedProps ? 'MAPPED_PROPS' : propMapping && !hasAnyMappedProps ? 'NO_MAPPED_PROPS' : 'DEFAULT'; let comment = comments[commentType]; let componentName = sourceExport === 'default' ? defaultImport : namedImports[component.id]; const snippet = `figma.connect(${componentName}, "${component.figmaNodeUrl}", { props: ${propMapping ? generatePropsFromMapping(component, propMapping) : generateProps(component)}, example: (props) => ${generateExample(componentName, reactTypeSignature, propMapping)}, })`; codeConnectCode += ` /** * -- This file was auto-generated by Code Connect --${comment} */ ${snippet} `; } const comma = Object.keys(namedImports).length > 0 && defaultImport ? ',' : ''; const namedImportList = Object.values(namedImports).length > 0 ? `{${Object.values(namedImports).join(',')}}` : ''; let codeConnect = ` import React from 'react' import ${defaultImport} ${comma} ${namedImportList} from '${importsPath}' import figma from '@figma/code-connect' ${codeConnectCode} `; let formatted = prettier.format(codeConnect, { parser: 'typescript', semi: false, trailingComma: 'all', }); if (fs_1.default.existsSync(filePath)) { return { createdFiles: [], messages: [{ message: `File ${filePath} already exists, skipping creation`, level: 'ERROR' }], }; } fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true }); fs_1.default.writeFileSync(filePath, formatted); return { createdFiles: [{ filePath }], messages: [] }; } //# sourceMappingURL=create.js.map