Skip to content

Add symbol name serialization and inputs to level props#1787

Merged
NKoech123 merged 10 commits intomainfrom
nkoech/symbols-investigation-fixes
Jan 13, 2026
Merged

Add symbol name serialization and inputs to level props#1787
NKoech123 merged 10 commits intomainfrom
nkoech/symbols-investigation-fixes

Conversation

@NKoech123
Copy link
Contributor

@NKoech123 NKoech123 commented Dec 2, 2025

Symbol AI Improvements: Named Components and Top-Level Input Props

https://www.loom.com/share/6fac3831aa1d4ff5bec03afd211f1c67

Summary

Improves Builder Symbol serialization to Mitosis JSX to make symbols more understandable for LLMs in Visual Editor AI. Symbols now serialize with meaningful component names and inputs as top-level props instead of nested in symbol.data.

Motivation

Currently, all Builder Symbols serialize to generic <Symbol> tags with inputs buried in nested symbol.data objects. This creates several challenges for LLMs:

  1. No distinguishability: <Symbol> tags all look identical, making it impossible for LLMs to differentiate between a header symbol and a button symbol
  2. Unusual prop structure: Inputs nested in symbol.data don't follow standard JSX patterns that LLMs are trained on
  3. Reduced clarity: LLMs struggle to understand what properties are available or how to modify them

Changes Made

Phase 1: Symbol Name Serialization

  • Added sanitizeSymbolName() helper function that converts symbol names to valid JSX component names (e.g., "Header Navigation" → "SymbolHeaderNavigation")
  • Updated the Symbol component mapper to use sanitized names instead of generic 'Symbol'
  • Updated extractSymbols() function to use actual symbol names when creating subComponents
  • Fixed mapper lookup to handle symbols renamed by extractSymbols() by checking if component name starts with "Symbol"

Phase 2: Inputs as Top-Level Props

  • Extracted inputs from symbol.data and created individual bindings for each input
  • Removed extracted inputs from the symbol binding to avoid duplication
  • Preserved empty data: {} objects to prevent data loss for symbols without inputs
  • Only extracts inputs when they exist (non-empty data object)

Files Changed

  • packages/core/src/parsers/builder/builder.ts - Core implementation
  • packages/core/src/__tests__/builder/builder.test.ts - Integration tests
  • packages/core/src/__tests__/data/builder/symbol-*.json - Test fixtures (4 new files)

Testing

Added comprehensive test coverage:

  • ✅ Symbol with basic metadata
  • ✅ Symbol with entry name
  • ✅ Symbol with inputs as top-level props
  • ✅ Multiple symbols with different names
  • ✅ Symbol roundtrip: Builder → Mitosis → Builder
  • ✅ No data loss for symbols without inputs (existing test)

All 10,236 tests passing.

Before & After

Before:

<Symbol symbol={{
  entry: "abc123",
  data: {
    buttonText: "Click me!",
    variant: "primary",
    disabled: false
  }
}} />
<Symbol symbol={{
  entry: "def456",
  data: {
    logoUrl: "/logo.png",
    showSearch: true
  }
}} />

After:

<SymbolButtonComponent 
  buttonText="Click me!"
  variant="primary"
  disabled={false}
  symbol={{
    entry: "abc123",
    model: "symbol",
    ownerId: "..."
  }}
/>
<SymbolHeaderNavigation 
  logoUrl="/logo.png"
  showSearch={true}
  symbol={{
    entry: "def456",
    model: "symbol",
    ownerId: "..."
  }}
/>

Impact on Visual Editor AI

This change significantly improves LLM understanding of symbols:

  1. Better targeting: LLMs can now distinguish between different symbols by name

    • "Change the header navigation logo" → targets <SymbolHeaderNavigation>
    • "Update the button text" → targets <SymbolButtonComponent>
  2. Natural JSX patterns: Top-level props follow standard JSX conventions that LLMs are trained on

    • LLMs can easily understand buttonText="Click me!" is a prop
    • Matches patterns from React, Vue, and other frameworks
  3. Improved editability: LLMs can more easily modify symbol inputs

    • Direct prop access vs nested object manipulation
    • Clear property names at the component level

Testing

Symbol Roundtrip Documentation

This document describes how Builder Symbols are serialized through the Mitosis JSX roundtrip process used by Editor AI.

Overview

The roundtrip flow is:

Builder JSON → Mitosis Component → Mitosis JSX → Mitosis Component → Builder JSON

This allows the AI to see and manipulate symbols as readable JSX, while preserving all metadata when converting back to Builder format.

Roundtrip Example

Step 1: Original Builder JSON (from MCP/API)

This is what Builder stores and what the visual editor expects:

{
  "@type": "@builder.io/sdk:Element",
  "id": "builder-abc123",
  "component": {
    "name": "Symbol",
    "options": {
      "symbol": {
        "entry": "2f27304b0ca04f578466218e27ae6d9b",
        "model": "symbol",
        "name": "Copyright Reserved",
        "data": {
          "buttonText": "Click me!",
          "year": 2025
        }
      }
    }
  },
  "responsiveStyles": {
    "large": {
      "display": "flex",
      "flexDirection": "column",
      "position": "relative",
      "flexShrink": "0",
      "boxSizing": "border-box"
    }
  }
}

Key properties:

  • component.name: Always "Symbol" - required by Builder visual editor
  • symbol.entry: Unique ID linking to the symbol content
  • symbol.name: Human-readable display name
  • symbol.data: Input values for this symbol instance

Step 2: Mitosis Component (internal representation)

After builderContentToMitosisComponent():

{
  name: "SymbolCopyrightReserved",  // Sanitized from "Copyright Reserved"
  bindings: {
    symbol: { 
      code: '{"entry":"2f27304b0ca04f578466218e27ae6d9b","model":"symbol","name":"Copyright Reserved"}',
      type: "single"
    },
    buttonText: { code: "'Click me!'", type: "single" },  // Extracted as top-level
    year: { code: "2025", type: "single" }                 // Extracted as top-level
  }
}

Transformations:

  • Component name becomes SymbolCopyrightReserved (sanitized, prefixed with "Symbol")
  • Inputs from symbol.data are extracted as top-level bindings
  • symbol.data is removed from the symbol binding (to avoid duplication)

Step 3: Mitosis JSX String (what AI sees)

After componentToMitosis():

<SymbolCopyrightReserved
  symbol={{
    entry: "2f27304b0ca04f578466218e27ae6d9b",
    model: "symbol",
    name: "Copyright Reserved",
  }}
  buttonText={"Click me!"}
  year={2025}
/>

Benefits for AI:

  • Named component (SymbolCopyrightReserved) is distinguishable from other symbols
  • Inputs are visible as standard JSX props
  • AI can add, remove, or move the entire element

Step 4: After parseJsx() (after AI edits)

After parseJsx():

{
  name: "SymbolCopyrightReserved",
  bindings: {
    symbol: { 
      code: '{ entry: "2f27304b...", model: "symbol", name: "Copyright Reserved" }',
      type: "single"
    }
  },
  properties: {
    buttonText: "Click me!",  // Simple strings become properties
    year: "2025"
  }
}

Note: Simple string values like buttonText become properties instead of bindings after JSX parsing. The generator handles both.


Step 5: Back to Builder JSON (for visual editor)

After componentToBuilder():

{
  "@type": "@builder.io/sdk:Element",
  "component": {
    "name": "Symbol",
    "options": {
      "symbol": {
        "entry": "2f27304b0ca04f578466218e27ae6d9b",
        "model": "symbol",
        "name": "Copyright Reserved",
        "data": {
          "buttonText": "Click me!",
          "year": "2025"
        }
      }
    }
  }
}

Transformations:

  • component.name is reset to "Symbol" (required by Builder)
  • symbol.name is preserved for future roundtrips
  • Inputs from both bindings and properties are merged back into symbol.data

Key Implementation Details

Name Sanitization

The sanitizeSymbolName() function converts display names to valid JSX component names:

"Copyright Reserved" → "SymbolCopyrightReserved"
"My Button"          → "SymbolMyButton"
"Footer"             → "SymbolFooter"

Rules:

  • Prefix with "Symbol" to avoid collisions with other components
  • Remove non-alphanumeric characters
  • Capitalize first letter

Input Extraction

Inputs are extracted from symbol.data as top-level props for AI readability:

// Before (hard for AI to understand)
<Symbol symbol={{ data: { buttonText: "Click" }, entry: "..." }} />

// After (standard JSX pattern)
<SymbolCopyrightReserved symbol={{ entry: "..." }} buttonText="Click" />

Input Merging (on way back)

The generator merges inputs from both sources:

  1. Bindings - Complex values like objects/arrays
  2. Properties - Simple strings (after JSX parse converts them)
// From bindings
for (const key of Object.keys(json.bindings)) {
  if (key !== 'symbol' && key !== 'css' && key !== 'style') {
    inputData[key] = json5.parse(json.bindings[key].code);
  }
}

// From properties (simple strings after JSX roundtrip)
for (const key of Object.keys(json.properties)) {
  if (!key.startsWith('$') && !key.startsWith('_') && !key.startsWith('data-')) {
    inputData[key] = json.properties[key];
  }
}

AI Interaction Rules

The AI is instructed:

  1. DO NOT modify the symbol prop (entry, model, name)
  2. DO NOT modify input props (buttonText, year, etc.)
  3. CAN add new symbol instances (copy from MCP)
  4. CAN remove symbol instances (delete element)
  5. CAN move symbol instances (change position)

Files Involved

File Purpose
parsers/builder/builder.ts Builder JSON → Mitosis Component
generators/mitosis/index.ts Mitosis Component → JSX String
parsers/jsx/index.ts JSX String → Mitosis Component
generators/builder/generator.ts Mitosis Component → Builder JSON
ai-services/.../parse-content-value.ts Post-processing (adds default styles)

@NKoech123 NKoech123 self-assigned this Dec 2, 2025
@changeset-bot
Copy link

changeset-bot bot commented Dec 2, 2025

🦋 Changeset detected

Latest commit: a35bbb5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@builder.io/mitosis Minor
@builder.io/mitosis-cli Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@nx-cloud
Copy link

nx-cloud bot commented Dec 2, 2025

View your CI Pipeline Execution ↗ for commit a35bbb5

Command Status Duration Result
nx run-many --target test ✅ Succeeded 4m 33s View ↗
nx e2e @builder.io/e2e-app ✅ Succeeded 1m 8s View ↗
nx run-many --target build --exclude @builder.i... ✅ Succeeded 2s View ↗
nx build @builder.io/mitosis-site ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-13 15:47:52 UTC

Copy link
Contributor

@liamdebeasi liamdebeasi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious to hear how your testing in Editor AI went today!

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 3, 2025

Deploying mitosis with  Cloudflare Pages  Cloudflare Pages

Latest commit: a35bbb5
Status: ✅  Deploy successful!
Preview URL: https://9a080661.mitosis-9uh.pages.dev
Branch Preview URL: https://nkoech-symbols-investigation.mitosis-9uh.pages.dev

View logs

@NKoech123 NKoech123 marked this pull request as ready for review December 9, 2025 07:37
@NKoech123 NKoech123 requested a review from samijaber as a code owner December 9, 2025 07:37
Copy link
Contributor

@liamdebeasi liamdebeasi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the approach seems reasonable to me, but I'd love to have @samijaber take a look since he has a better grasp on how things should be architected in Mitosis than I do.

@NKoech123 NKoech123 changed the title WIP: add symbol name serialization and inputs to level props Add symbol name serialization and inputs to level props Dec 13, 2025
Copy link
Contributor

@samijaber samijaber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat PR!

I am requesting changes because I worry about overloading the json.name with secret expectations like "if it starts with Symbol then its a special user symbol". It would be very easy for some user code to accidentally have that name format.

I think the way to go here would be to encode this information in a prop, like json.type: 'user-symbol' and rely on if (json.type === 'user-symbol') to accurately handle user symbols.

@NKoech123
Copy link
Contributor Author

NKoech123 commented Jan 6, 2026

json.type === 'user-symbol') to accurately handle user symbols.

@samijaber Added this change and tested it.

@NKoech123 NKoech123 requested a review from samijaber January 6, 2026 21:13
@NKoech123 NKoech123 enabled auto-merge (squash) January 6, 2026 21:14
@NKoech123 NKoech123 disabled auto-merge January 13, 2026 15:34
@NKoech123 NKoech123 force-pushed the nkoech/symbols-investigation-fixes branch from 518896b to a35bbb5 Compare January 13, 2026 15:41
@NKoech123 NKoech123 merged commit 720e30b into main Jan 13, 2026
14 checks passed
@NKoech123 NKoech123 deleted the nkoech/symbols-investigation-fixes branch January 13, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants