@figma/code-connect
Version:
A tool for connecting your design system components in code with your design system in Figma
453 lines • 23.3 kB
JavaScript
;
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.groupCodeConnectObjectsByFigmaUrl = exports.migrateV1TemplateToV2 = void 0;
exports.getFilenameFromComponentName = getFilenameFromComponentName;
exports.prepareMigratedTemplate = prepareMigratedTemplate;
exports.writeTemplateFile = writeTemplateFile;
exports.migrateTemplateToUseServerSideHelpers = migrateTemplateToUseServerSideHelpers;
exports.addId = addId;
exports.addImports = addImports;
exports.addNestableToMetadata = addNestableToMetadata;
exports.removePropsDefinition = removePropsDefinition;
exports.removePropsDefinitionAndMetadata = removePropsDefinitionAndMetadata;
exports.writeVariantTemplateFile = writeVariantTemplateFile;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const prettier = __importStar(require("prettier"));
/** Prettier configuration used for all generated template files */
const PRETTIER_OPTIONS = {
parser: 'typescript',
semi: false,
trailingComma: 'all',
pluginSearchDirs: false,
};
/** Formats template code with consistent prettier configuration */
function formatTemplate(code) {
return prettier.format(code, PRETTIER_OPTIONS);
}
/**
* Common wrapper that handles path determination and file writing for both
* simple templates and variant templates.
*/
function writeTemplateFileCommon(componentName, fileContent, outputDir, baseDir, localSourcePath, filePathsCreated, useTypeScript = true) {
const suffix = useTypeScript ? '.figma.ts' : '.figma.js';
const baseOutputPath = determineOutputPath(componentName, suffix, outputDir, baseDir, localSourcePath);
return writeFileWithDuplicateHandling(baseOutputPath, fileContent, suffix, filePathsCreated);
}
function getFilenameFromComponentName(componentName) {
const allowlisted = /[a-zA-Z0-9\-\.]/;
return componentName
.split('')
.map((ch) => (allowlisted.test(ch) ? ch : '_'))
.join('')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '');
}
/**
* Determines the base output path for a template file.
* Shared logic between writeTemplateFile and writeVariantTemplateFile.
*/
function determineOutputPath(rawComponentName, suffix, outputDir, baseDir, localSourcePath) {
// Extract basename from localSourcePath when available, falling back to componentName
const componentFilename = getFilenameFromComponentName(rawComponentName);
let basename = componentFilename;
if (localSourcePath) {
let sourceBasename = path_1.default.basename(localSourcePath);
// If this is a Code Connect file, extract the base component name
// Handles patterns like: Button.figma.tsx, Button.figmadoc.tsx, Button.figma.template.js
// Should all become: Button.figma.js
const codeConnectPattern = /\.(figma|figmadoc)(\.[^.]+)+$/;
if (codeConnectPattern.test(sourceBasename)) {
// Extract everything before the .figma/.figmadoc pattern
sourceBasename = sourceBasename.replace(codeConnectPattern, '');
}
else {
// For regular component files, strip extension normally
sourceBasename = path_1.default.basename(localSourcePath, path_1.default.extname(localSourcePath));
}
basename = sourceBasename;
}
if (outputDir) {
// Use specified output directory with basename derived from source path
return path_1.default.join(outputDir, `${basename}${suffix}`);
}
else if (localSourcePath) {
// Use same directory as local source file
return path_1.default.join(path_1.default.dirname(localSourcePath), `${basename}${suffix}`);
}
else {
// No source info, use current directory
const filename = `${componentFilename}${suffix}`;
return path_1.default.join(baseDir, filename);
}
}
/**
* Handles file existence checking, duplicate name resolution, and writing.
* Shared logic between writeTemplateFile and writeVariantTemplateFile.
*/
function writeFileWithDuplicateHandling(baseOutputPath, fileContent, suffix, filePathsCreated) {
// Check if file already exists on disk (pre-existing file, not created in this run)
const existsOnDisk = fs_1.default.existsSync(baseOutputPath);
const createdInThisRun = filePathsCreated && filePathsCreated.has(baseOutputPath);
if (existsOnDisk && !createdInThisRun) {
// This file existed before the migration run, skip it
return { outputPath: baseOutputPath, skipped: true };
}
// Handle duplicate names (either created in this run or would conflict with an existing file)
let outputPath = baseOutputPath;
if (createdInThisRun || existsOnDisk) {
// Find a unique name by appending _1, _2, etc. before the suffix
const dir = path_1.default.dirname(baseOutputPath);
const basename = path_1.default.basename(baseOutputPath);
// Remove the suffix to get the base name (works for any suffix like .figma.js or .figma.ts)
const baseNameWithoutSuffix = basename.endsWith(suffix)
? basename.slice(0, -suffix.length)
: basename;
let counter = 1;
do {
outputPath = path_1.default.join(dir, `${baseNameWithoutSuffix}_${counter}${suffix}`);
counter++;
} while ((filePathsCreated && filePathsCreated.has(outputPath)) || fs_1.default.existsSync(outputPath));
}
// Ensure output directory exists
const outputDirPath = path_1.default.dirname(outputPath);
if (!fs_1.default.existsSync(outputDirPath)) {
fs_1.default.mkdirSync(outputDirPath, { recursive: true });
}
// Write the file
fs_1.default.writeFileSync(outputPath, fileContent, 'utf-8');
// Track the created file path
if (filePathsCreated) {
filePathsCreated.add(outputPath);
}
return { outputPath, skipped: false };
}
/** Migrates a doc's template (Swift helpers, server helpers, V2, id, imports, nestable) and returns the formatted string. */
function prepareMigratedTemplate(doc, includeProps = false, useTypeScript) {
let template = doc.template;
template = removeSwiftHelpers(template);
template = migrateTemplateToUseServerSideHelpers(template);
if (!includeProps) {
template = removePropsDefinitionAndMetadata(template);
}
template = (0, exports.migrateV1TemplateToV2)(template);
template = addId(template, doc.component || 'TODO');
template = addImports(template, doc.templateData?.imports);
template = addNestableToMetadata(template, !!doc.templateData?.nestable);
if (useTypeScript) {
template = convertSyntaxToTypeScript(template);
}
return formatTemplate(template);
}
function removeSwiftHelpers(template) {
return template.replace(`function __fcc_renderSwiftChildren(children, prefix) {
if (children === undefined) {
return children
}
return children.flatMap((child, index) => {
if (child.type === 'CODE') {
let code = child.code.split('\\n').map((line) => {
return line.trim() !== '' ? \`\${prefix}\${line}\` : line;
}).join('\\n')
if (index !== children.length - 1) {
code = code + '\\n'
}
return {
...child,
code: code,
}
} else {
let elements = []
const shouldAddNewline = index > 0 && children[index - 1].type === 'CODE' && !children[index - 1].code.endsWith('\\n')
elements.push({ type: 'CODE', code: \`\${shouldAddNewline ? '\\n' : ''}\${prefix}\` })
elements.push(child)
if (index !== children.length - 1) {
elements.push({ type: 'CODE', code: '\\n' })
}
return elements
}
})
}
`, '');
}
function writeTemplateFile(doc, outputDir, baseDir, { localSourcePath, filePathsCreated, includeProps = false, useTypeScript = true, } = {}) {
const componentName = doc.component || 'template';
const template = prepareMigratedTemplate(doc, includeProps, useTypeScript);
// Build comment header lines
const commentLines = [`// url=${doc.figmaNode}`];
if (doc.source) {
commentLines.push(`// source=${doc.source}`);
}
if (doc.component) {
commentLines.push(`// component=${doc.component}`);
}
commentLines.push(``);
const fileContent = commentLines.join('\n') + '\n' + template;
return writeTemplateFileCommon(componentName, fileContent, outputDir, baseDir, localSourcePath, filePathsCreated, useTypeScript);
}
// Renames must match helpers in code_connect_js_api.raw_source.ts
function migrateTemplateToUseServerSideHelpers(template) {
return (template
// React helpers
.replace(/_fcc_renderReactProp/g, 'figma.helpers.react.renderProp')
.replace(/_fcc_renderReactChildren/g, 'figma.helpers.react.renderChildren')
.replace(/_fcc_jsxElement/g, 'figma.helpers.react.jsxElement')
.replace(/_fcc_function/g, 'figma.helpers.react.function')
.replace(/_fcc_identifier/g, 'figma.helpers.react.identifier')
.replace(/_fcc_object/g, 'figma.helpers.react.object')
.replace(/_fcc_templateString/g, 'figma.helpers.react.templateString')
.replace(/_fcc_renderPropValue/g, 'figma.helpers.react.renderPropValue')
.replace(/_fcc_stringifyObject/g, 'figma.helpers.react.stringifyObject')
.replace(/_fcc_reactComponent/g, 'figma.helpers.react.reactComponent')
.replace(/_fcc_array/g, 'figma.helpers.react.array')
.replace(/isReactComponentArray/g, 'figma.helpers.react.isReactComponentArray')
// Swift helpers
.replace(/__fcc_renderSwiftChildren/g, 'figma.helpers.swift.renderChildren')
// Kotlin/Compose helpers
.replace(/__fcc_renderComposeChildren/g, 'figma.helpers.kotlin.renderChildren'));
}
function addId(template, id) {
// Don't add id if already present (e.g. when re-migrating a previously-migrated template)
if (/export default\s*\{\s*id:\s*['"]/.test(template)) {
return template;
}
return template.replace(/export default \{/, `export default { id: '${id}',`);
}
function addImports(template, imports) {
if (!imports || imports.length === 0) {
return template;
}
// Escape imports for safe insertion into JS
const importsJson = JSON.stringify(imports);
// Add imports after the id field if it exists, otherwise at the start
// Match: export default { id: '...', (with optional whitespace/newlines)
const withId = template.replace(/(export default\s*\{\s*id:\s*'[^']*',)/, `$1 imports: ${importsJson},`);
// If id replacement worked, return
if (withId !== template) {
return withId;
}
// Otherwise, add at the start (after opening brace)
return template.replace(/export default\s*\{/, `export default { imports: ${importsJson},`);
}
function addNestableToMetadata(template, nestable) {
// Find "metadata: {" and replace with "metadata: { nestable: <value>,"
return template.replace(/metadata:\s*\{/, `metadata: { nestable: ${nestable},`);
}
/**
* Migrates V1 templates to V2 API.
*
* This performs safe, incremental transformations. The following patterns
* are intentionally NOT migrated as they're still supported in V2:
*
* - __props metadata building pattern - still valid JavaScript
* - __renderWithFn__() - complex transformation, still supported
*
* These may be addressed in future migrations.
*/
const migrateV1TemplateToV2 = (template) => {
let migrated = template;
// 1. Core object rename
migrated = migrated.replace(/figma\.currentLayer/g, 'figma.selectedInstance');
// 2. Normalize template types to figma.code
migrated = migrated.replace(/figma\.html/g, 'figma.code');
migrated = migrated.replace(/figma\.tsx/g, 'figma.code');
migrated = migrated.replace(/figma\.swift/g, 'figma.code');
migrated = migrated.replace(/figma\.kotlin/g, 'figma.code');
// 3. Property accessor methods
migrated = migrated.replace(/\.__properties__\.string\(/g, '.getString(');
migrated = migrated.replace(/\.__properties__\.boolean\(/g, '.getBoolean(');
migrated = migrated.replace(/\.__properties__\.enum\(/g, '.getEnum(');
migrated = migrated.replace(/\.__properties__\.__instance__\(/g, '.getInstanceSwap(');
// .__properties__.instance() auto-renders, so we need to add .executeTemplate().example
migrated = migrated.replace(/\.__properties__\.instance\(([^)]+)\)/g, '.getInstanceSwap($1)?.executeTemplate().example');
// 4. Alias for __properties__ on selectedInstance
migrated = migrated.replace(/figma\.selectedInstance\.__properties__\./g, 'figma.properties.');
// 5. Other method renames
migrated = migrated.replace(/\.__getPropertyValue__\(/g, '.getPropertyValue(');
// 6. __findChildWithCriteria__ - migrate based on type parameter
// For TEXT type with __render__(): __findChildWithCriteria__({ name: 'X', type: "TEXT" }).__render__() → findText('X').textContent
migrated = migrated.replace(/\.__findChildWithCriteria__\(\{\s*name:\s*'([^']+)',\s*type:\s*"TEXT"\s*\}\)\.__render__\(\)/g, ".findText('$1').textContent");
migrated = migrated.replace(/\.__findChildWithCriteria__\(\{\s*type:\s*"TEXT",\s*name:\s*'([^']+)'\s*\}\)\.__render__\(\)/g, ".findText('$1').textContent");
// For INSTANCE type: __findChildWithCriteria__({ type: 'INSTANCE', name: 'X' }) → findInstance('X')
migrated = migrated.replace(/\.__findChildWithCriteria__\(\{\s*name:\s*'([^']+)',\s*type:\s*['"]INSTANCE['"]\s*\}\)/g, ".findInstance('$1')");
migrated = migrated.replace(/\.__findChildWithCriteria__\(\{\s*type:\s*['"]INSTANCE['"],\s*name:\s*'([^']+)'\s*\}\)/g, ".findInstance('$1')");
// For TEXT type without __render__(): __findChildWithCriteria__({ type: 'TEXT', name: 'X' }) → findText('X')
migrated = migrated.replace(/\.__findChildWithCriteria__\(\{\s*name:\s*'([^']+)',\s*type:\s*['"]TEXT['"]\s*\}\)/g, ".findText('$1')");
migrated = migrated.replace(/\.__findChildWithCriteria__\(\{\s*type:\s*['"]TEXT['"],\s*name:\s*'([^']+)'\s*\}\)/g, ".findText('$1')");
// 7. __find__() - migrate to findInstance()
migrated = migrated.replace(/\.__find__\(("([^"]+)"|'([^']+)')\)/g, (match, quote, doubleQuoted, singleQuoted) => {
const name = doubleQuoted || singleQuoted;
return `.findInstance("${name}")`;
});
// 8. __render__() - migrate to executeTemplate().example (but not if part of __findChildWithCriteria__)
migrated = migrated.replace(/\.__render__\(\)/g, '.executeTemplate().example');
// 9. __getProps__() - migrate to executeTemplate().metadata.props
migrated = migrated.replace(/\.__getProps__\(\)/g, '.executeTemplate().metadata.props');
// 10. Export format - simple case
// Match export default figma.code` (or tsx, html, etc) and wrap in { example: ... }
migrated = migrated.replace(/export default figma\.(code|tsx|html|swift|kotlin)`/g, 'export default { example: figma.$1`');
// Close the template literal for simple exports (look for backtick at end, handling multiline)
// Use a more robust approach: find the last backtick that's not followed by more content
migrated = migrated.replace(/(export default \{ example: figma\.\w+`[\s\S]*?)`(?=\s*$)/gm, '$1` }');
// 11. Export format - spread operator case
// { ...figma.code`...`, metadata: ... } → { example: figma.code`...`, metadata: ... }
migrated = migrated.replace(/\{\s*\.\.\.figma\.(code|tsx|html|swift|kotlin)`/g, '{ example: figma.$1`');
return migrated;
};
exports.migrateV1TemplateToV2 = migrateV1TemplateToV2;
/**
* Removes the __props definition and props assignments. These are only used by icons
* helpers and significantly bloat templates.
*/
function removePropsDefinition(template) {
// Match from "const __props = {" through everything up until (but not including) "export default {"
// Uses [\s\S] to match any character including newlines
return template.replace(/const\s+__props\s*=\s*\{[\s\S]*?(?=export\s+default\s*\{)/g, '\n');
}
/**
* Removes the __props definition/assignments and removes __props from the default export
*/
function removePropsDefinitionAndMetadata(template) {
// First remove the __props definition and assignments
let result = removePropsDefinition(template);
const exportMatch = result.match(/(export\s+default\s+\{[\s\S]*$)/);
if (exportMatch) {
const exportSection = exportMatch[1];
const cleanedExport = exportSection
.replace(/metadata:\s*\{\s*__props\s*\}/g, 'metadata: {}') // metadata: { __props }
.replace(/metadata:\s*\{\s*__props\s*,/g, 'metadata: {') // metadata: { __props, ...
.replace(/,\s*__props\s*\}/g, ' }') // metadata: { ..., __props }
.replace(/,\s*__props\s*,/g, ','); // metadata: { ..., __props, ... }
result = result.substring(0, result.indexOf(exportSection)) + cleanedExport;
}
return result;
}
/**
* For each figmaUrl in the given codeConnectObjects, return the main (non-variant)
* codeConnectObject plus a list of any variants
*/
const groupCodeConnectObjectsByFigmaUrl = (codeConnectObjects) => {
return codeConnectObjects.reduce((acc, obj) => {
const figmaUrl = obj.figmaNode;
if (!acc[figmaUrl]) {
acc[figmaUrl] = { main: null, variants: [] };
}
if (obj.variant && Object.keys(obj.variant).length > 0) {
acc[figmaUrl].variants.push(obj);
}
else {
acc[figmaUrl].main = obj;
}
return acc;
}, {});
};
exports.groupCodeConnectObjectsByFigmaUrl = groupCodeConnectObjectsByFigmaUrl;
/** One parserless file per component: branch per variant, each with its own props and template object; export default template. */
function writeVariantTemplateFile(group, figmaUrl, outputDir, baseDir, { localSourcePath, filePathsCreated, useTypeScript = true, includeProps = false, } = {}) {
const variantDocs = group.variants.map((v) => ({ doc: v, variant: v.variant }));
const defaultDoc = group.main ? { doc: group.main, variant: null } : null;
const componentName = (group.main ?? group.variants[0])?.component || 'template';
// Migrate templates and extract their full code (no deduplication)
const allDocs = [...variantDocs, ...(defaultDoc ? [defaultDoc] : [])];
const migratedTemplates = allDocs.map(({ doc }) => prepareMigratedTemplate(doc, includeProps, useTypeScript));
const exportDefaultPrefix = 'export default ';
// For each template, replace 'export default' with 'template =' and remove figma require
const branches = migratedTemplates.map((t) => {
if (!t.includes(exportDefaultPrefix)) {
throw new Error(`Variant merge: no "export default" in template for ${figmaUrl}`);
}
// Replace 'export default' with 'template ='
let branchCode = t.replace(exportDefaultPrefix, 'template = ');
// Remove only the top-level figma require/import, keep everything else
const lines = branchCode.split('\n');
const filteredLines = lines.filter((line) => !line.trim().startsWith('const figma = require') &&
!line.trim().startsWith('import figma from'));
return filteredLines.join('\n').trim();
});
// Build the variant switch structure using conditions for all variant properties
const ifParts = [];
for (let i = 0; i < variantDocs.length; i++) {
const { variant } = variantDocs[i];
const branchCode = branches[i];
if (variant && Object.keys(variant).length > 0) {
// Build condition from all properties in the variant
const condition = Object.entries(variant)
.map(([key, val]) => `figma.selectedInstance.getPropertyValue('${key}') === ${typeof val === 'string' ? `'${val}'` : val}`)
.join(' && ');
ifParts.push(`${ifParts.length ? '} else ' : ''}if (${condition}) {\n${branchCode}`);
}
}
// Add default/fallback branch (use the last doc: main if present, otherwise first variant)
const defaultBranchCode = branches[branches.length - 1];
ifParts.push(`} else {\n${defaultBranchCode}\n}`);
const variantComment = group.main != null
? `// Branch per variant combination.`
: `// Branch per variant; no default, else first.`;
const templateBody = [
useTypeScript ? `import figma from "figma"` : `const figma = require('figma')`,
'',
variantComment,
'',
'let template',
ifParts.join('\n'),
'',
'export default template',
].join('\n');
const formatted = formatTemplate(templateBody);
// Build comment header with url, source, and component
// Use the representative doc (main or first variant) for source/component values
const representativeDoc = group.main ?? group.variants[0];
const commentLines = [`// url=${figmaUrl}`];
if (representativeDoc?.source) {
commentLines.push(`// source=${representativeDoc.source}`);
}
if (representativeDoc?.component) {
commentLines.push(`// component=${representativeDoc.component}`);
}
commentLines.push(``);
const fileContent = commentLines.join('\n') + '\n' + formatted;
return writeTemplateFileCommon(componentName, fileContent, outputDir, baseDir, localSourcePath, filePathsCreated, useTypeScript);
}
function convertSyntaxToTypeScript(template) {
return template
.replace(/const figma = require\(['"]figma['"]\)/, `import figma from "figma"`)
.replace(/const __props = {}/, 'const __props: Record<string, unknown> = {}')
.replace(/if \((\w+) && \1\.type !== "ERROR"\)/g, 'if ($1 && ($1 as { type?: string }).type !== "ERROR")');
}
//# sourceMappingURL=migration_helpers.js.map