figma-make-extractor

Extract source code, design tokens, and assets from Figma Make (.make) files into runnable React applications

Skill file

Preview skill file
---
name: figma-make-extractor
description: Extract source code, design tokens, and assets from Figma Make (.make) files into runnable React applications
triggers:
  - extract code from figma make file
  - decode figma make canvas
  - get source code from make file
  - extract design tokens from figma
  - convert figma make to react app
  - parse figma make binary format
  - extract assets from make file
  - reverse engineer figma make file
---

# Figma Make Extractor

> Skill by [ara.so](https://ara.so) — Design Skills collection.

Extract source code, design tokens, and assets from Figma Make (.make) files. Figma Make is an AI-powered prototyping tool that generates complete React applications. Unlike traditional Figma Design files (.fig), Make files contain React/TypeScript source code, design tokens, assets, and AI chat history - but they're not accessible via the Figma API.

This tool reverse-engineers the .make file format to extract all project contents into a runnable React application.

## Installation

```bash
git clone https://github.com/albertsikkema/figma-make-extractor.git
cd figma-make-extractor/make_extraction
```

No global installation needed - the `run-all.sh` script handles dependency installation automatically.

## Quick Start

```bash
cd make_extraction
./run-all.sh ../YourFile.make
cd output/react_app
npm install --legacy-peer-deps
npm run dev
```

This extracts everything and creates a ready-to-run React app at `output/react_app/`.

## File Structure

A `.make` file is a ZIP archive containing:

```
YourFile.make (ZIP)
├── canvas.fig          # Binary file with source code & design data
├── meta.json           # Project metadata
├── ai_chat.json        # AI conversation history
├── thumbnail.png       # Preview image
├── images/             # Image assets
│   ├── [hash1]
│   └── [hash2]
└── blob_store/         # Additional binary data
```

The `canvas.fig` file uses a custom binary format:
- Header: `fig-makee` magic bytes + padding (12 bytes)
- Chunk 1: Deflate-compressed Kiwi schema (4 bytes size + data)
- Chunk 2: Zstandard-compressed message data (4 bytes size + data)

## Key Scripts

All scripts are in the `make_extraction/` directory:

### Master Script

```bash
# Extract everything and create React app
./run-all.sh <path-to-make-file>

# Clean up all generated files
./cleanup.sh
```

### Individual Scripts

```bash
# 1. Decode canvas.fig binary to JSON
node 01-decode-canvas.js

# 2. Extract source code files
node 02-extract-source-code.js

# 3. Extract design tokens (colors, fonts, CSS vars)
node 03-extract-design-tokens.js

# 4. Create runnable React app
node 04-create-react-app.js
```

## Programmatic Usage

### Decoding canvas.fig

```javascript
const fs = require('fs');
const pako = require('pako');
const { decompress } = require('fzstd');
const kiwi = require('kiwi-schema');

// Read binary file
const buffer = fs.readFileSync('output/extracted/canvas.fig');

// Skip 12-byte header (fig-makee + padding)
let offset = 12;

// Read Chunk 1 (schema) - deflate compressed
const chunk1Size = buffer.readUInt32LE(offset);
offset += 4;
const chunk1Data = buffer.slice(offset, offset + chunk1Size);
offset += chunk1Size;
const schemaBytes = pako.inflate(chunk1Data);

// Read Chunk 2 (data) - zstandard compressed
const chunk2Size = buffer.readUInt32LE(offset);
offset += 4;
const chunk2Data = buffer.slice(offset, offset + chunk2Size);
const messageBytes = decompress(chunk2Data);

// Decode using Kiwi schema
const schema = kiwi.compileSchema(schemaBytes);
const decoded = schema.decodeMessage(messageBytes);

// Handle BigInt in JSON
const json = JSON.stringify(decoded, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
, 2);

fs.writeFileSync('decoded-message.json', json);
```

### Extracting Source Files

```javascript
const fs = require('fs');

const decoded = JSON.parse(fs.readFileSync('decoded-message.json', 'utf8'));
const outputDir = 'source_files';
fs.mkdirSync(outputDir, { recursive: true });

let fileCount = 0;

function traverse(node) {
  if (!node) return;
  
  // Extract CODE_FILE nodes with source code
  if (node.type === 'CODE_FILE' && node.sourceCode) {
    const fileName = node.name || `file_${fileCount++}`;
    const filePath = `${outputDir}/${fileName}`;
    fs.writeFileSync(filePath, node.sourceCode, 'utf8');
    console.log(`Extracted: ${fileName}`);
  }
  
  // Traverse children
  if (node.children) {
    node.children.forEach(child => traverse(child));
  }
}

traverse(decoded);
```

### Extracting Design Tokens

```javascript
const fs = require('fs');
const path = require('path');

const sourceDir = 'source_files';
const tokens = {
  colors: { hex: new Set(), rgba: new Set(), hsl: new Set() },
  fonts: new Set(),
  cssVariables: new Set()
};

// Regex patterns
const hexPattern = /#[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?/g;
const rgbaPattern = /rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+(?:\s*,\s*[\d.]+)?\s*\)/g;
const hslPattern = /hsla?\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%(?:\s*,\s*[\d.]+)?\s*\)/g;
const fontPattern = /font-family:\s*["']([^"']+)["']/g;
const cssVarPattern = /--[\w-]+/g;

// Scan all source files
fs.readdirSync(sourceDir).forEach(file => {
  const content = fs.readFileSync(path.join(sourceDir, file), 'utf8');
  
  content.match(hexPattern)?.forEach(c => tokens.colors.hex.add(c));
  content.match(rgbaPattern)?.forEach(c => tokens.colors.rgba.add(c));
  content.match(hslPattern)?.forEach(c => tokens.colors.hsl.add(c));
  
  let fontMatch;
  while ((fontMatch = fontPattern.exec(content)) !== null) {
    tokens.fonts.add(fontMatch[1]);
  }
  
  content.match(cssVarPattern)?.forEach(v => tokens.cssVariables.add(v));
});

// Convert Sets to Arrays
const result = {
  colors: {
    hex: Array.from(tokens.colors.hex),
    rgba: Array.from(tokens.colors.rgba),
    hsl: Array.from(tokens.colors.hsl)
  },
  fonts: Array.from(tokens.fonts),
  cssVariables: Array.from(tokens.cssVariables)
};

fs.writeFileSync('design-tokens.json', JSON.stringify(result, null, 2));
```

### Creating React App Structure

```javascript
const fs = require('fs');
const path = require('path');

const sourceDir = 'source_files';
const appDir = 'react_app';
const srcDir = path.join(appDir, 'src');

// Create directory structure
fs.mkdirSync(path.join(srcDir, 'components/ui'), { recursive: true });
fs.mkdirSync(path.join(srcDir, 'hooks'), { recursive: true });
fs.mkdirSync(path.join(srcDir, 'lib'), { recursive: true });
fs.mkdirSync(path.join(srcDir, 'data'), { recursive: true });
fs.mkdirSync(path.join(srcDir, 'styles'), { recursive: true });
fs.mkdirSync(path.join(appDir, 'public'), { recursive: true });

// Organize files by type
fs.readdirSync(sourceDir).forEach(file => {
  let targetDir = srcDir;
  const content = fs.readFileSync(path.join(sourceDir, file), 'utf8');
  
  // Categorize by content patterns
  if (file.startsWith('use-') && file.endsWith('.ts')) {
    targetDir = path.join(srcDir, 'hooks');
  } else if (file === 'utils.ts' || file === 'cn.ts') {
    targetDir = path.join(srcDir, 'lib');
  } else if (content.includes('@radix-ui') || content.includes('shadcn')) {
    targetDir = path.join(srcDir, 'components/ui');
  } else if (file.endsWith('.tsx')) {
    targetDir = path.join(srcDir, 'components');
  } else if (file.endsWith('.css')) {
    targetDir = path.join(srcDir, 'styles');
  }
  
  // Fix import paths
  let fixedContent = content
    .replace(/@radix-ui\/[\w-]+@[\d.]+/g, match => match.split('@')[0] + '@' + match.split('@')[1])
    .replace(/from\s+['"]\.\.\/[\w-]+@[\d.]+\//g, 'from "@/')
    .replace(/from\s+['"]figma:asset:([^'"]+)['"]/g, (match, hash) => {
      return `from "/images/${hash}.png"`;
    });
  
  fs.writeFileSync(path.join(targetDir, file), fixedContent, 'utf8');
});
```

### Creating package.json

```javascript
const packageJson = {
  "name": "figma-make-app",
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "framer-motion": "^11.0.0",
    "lucide-react": "latest"
  },
  "devDependencies": {
    "@types/react": "^18.3.1",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react": "^4.3.4",
    "typescript": "^5.6.2",
    "vite": "^6.0.1",
    "@tailwindcss/postcss": "^4.0.0"
  }
};

fs.writeFileSync(
  path.join(appDir, 'package.json'),
  JSON.stringify(packageJson, null, 2)
);
```

## Common Patterns

### Verifying .make File Format

```bash
# Check if file is ZIP
xxd -l 4 YourFile.make
# Should show: 504b 0304 (PK..)

# Check canvas.fig header
unzip -q YourFile.make
xxd -l 20 canvas.fig
# Should show: 6669 672d 6d61 6b65 65 (fig-makee)
```

### Converting Colors

```javascript
// RGBA float (0-1) to hex
function rgbaToHex(color) {
  const r = Math.round(color.r * 255);
  const g = Math.round(color.g * 255);
  const b = Math.round(color.b * 255);
  return `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`.toUpperCase();
}

// Example usage
const figmaColor = { r: 0.11764705926179886, g: 0.11764705926179886, b: 0.11764705926179886, a: 1 };
console.log(rgbaToHex(figmaColor)); // #1E1E1E
```

### Handling Image Assets

```javascript
const fs = require('fs');
const path = require('path');

// Copy images from extracted .make to React app public folder
const imagesSourceDir = 'output/extracted/images';
const imagesTargetDir = 'output/react_app/public/images';

fs.mkdirSync(imagesTargetDir, { recursive: true });

fs.readdirSync(imagesSourceDir).forEach(hash => {
  const sourceFile = path.join(imagesSourceDir, hash);
  const targetFile = path.join(imagesTargetDir, `${hash}.png`);
  fs.copyFileSync(sourceFile, targetFile);
});
```

## Output Structure

After running all scripts, the `output/` directory contains:

```
output/
├── extracted/                  # Unzipped .make contents
│   ├── canvas.fig
│   ├── images/
│   ├── meta.json
│   └── ai_chat.json
├── decoded-message.json        # Decoded canvas.fig
├── source_files/               # Raw extracted source files
├── design-tokens.json          # Colors, fonts, CSS vars
└── react_app/                  # Ready-to-run React app
    ├── package.json
    ├── vite.config.ts
    ├── tsconfig.json
    ├── index.html
    ├── public/
    │   └── images/
    └── src/
        ├── main.tsx
        ├── App.tsx
        ├── components/
        │   └── ui/             # shadcn/ui components
        ├── hooks/
        ├── lib/
        ├── data/
        └── styles/
```

## Troubleshooting

### "Invalid stored block lengths" error

The data chunk uses **Zstandard** compression, not zlib/deflate. Install the correct package:

```bash
npm install fzstd
```

### BigInt serialization error

Use a custom JSON replacer to handle BigInt values:

```javascript
JSON.stringify(decoded, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
, 2);
```

### Empty source code extraction

Verify the node type and property:

```javascript
if (node.type === 'CODE_FILE' && node.sourceCode) {
  // Extract here
}
```

Not all nodes have source code - only `CODE_FILE` nodes with the `sourceCode` property.

### Import path errors in React app

The `04-create-react-app.js` script automatically fixes:
- Version numbers in package names: `@radix-ui/react-dialog@1.1.6` → `@radix-ui/react-dialog`
- Figma asset imports: `figma:asset:hash` → `/images/hash.png`
- Relative paths: `../component` → `@/components/component`

If imports still fail, manually check the generated `vite.config.ts` has the alias:

```typescript
resolve: {
  alias: {
    '@': '/src'
  }
}
```

### npm install fails with peer dependency conflicts

Use the `--legacy-peer-deps` flag:

```bash
npm install --legacy-peer-deps
```

This is common with React 18 and older package versions.

### Tailwind CSS PostCSS error

The generated app uses Tailwind v4 with `@tailwindcss/postcss`. Ensure `postcss.config.js` exists:

```javascript
export default {
  plugins: {
    '@tailwindcss/postcss': {}
  }
};
```

### Animations not working (React StrictMode)

The generated `main.tsx` omits React.StrictMode to avoid timer/animation issues with Framer Motion. If you need StrictMode, wrap the app manually but expect animation glitches.

## Node Types Reference

When traversing the decoded message, you'll encounter these node types:

| Type | Description | Has sourceCode |
|------|-------------|----------------|
| `DOCUMENT` | Root document node | No |
| `CANVAS` | Page/canvas container | No |
| `CODE_LIBRARY` | Code library container | No |
| `CODE_FILE` | Source code file (`.tsx`, `.ts`, `.css`) | **Yes** |
| `CODE_COMPONENT` | Exported React component definition | No |
| `CODE_INSTANCE` | Instance of a component | No |
| `FRAME` | Visual frame (preview) | No |
| `RESPONSIVE_SET` | Responsive breakpoint container | No |

Only extract source code from `CODE_FILE` nodes.

## API Reference

This is a CLI tool, not a library. All functionality is accessed via scripts:

```bash
# Full extraction pipeline
./run-all.sh <path-to-make-file>

# Individual steps (after running run-all.sh or extracting manually)
node 01-decode-canvas.js
node 02-extract-source-code.js
node 03-extract-design-tokens.js
node 04-create-react-app.js

# Cleanup
./cleanup.sh
```

## Environment Variables

No environment variables required. All paths are relative to the script locations.

## License

MIT License - see the repository for full license text.

## Disclaimer

This tool documents a reverse-engineering process for educational purposes. The extracted code remains subject to Figma's terms of service and any applicable licenses. Use responsibly.

Source

Creator's repository · aradotso/design-skills

View on GitHub

Security

Security checks in progress
Results will appear here once audits complete
Checked by 3 independent security firms
Does it try to trick the AI?Not yet checkedPending · Gen Agent Trust Hub
Does it sneak in hidden code?Not yet checkedPending · Socket
Does it have known bugs?Not yet checkedPending · Snyk