Skip to content

Commit d4c57b5

Browse files
committed
handle converting symbol components back to Symbol for Builder
1 parent 4123bfc commit d4c57b5

File tree

2 files changed

+109
-0
lines changed

2 files changed

+109
-0
lines changed

packages/core/src/__tests__/builder/builder.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2175,4 +2175,67 @@ describe('Symbol Serialization', () => {
21752175
expect(roundtripSymbol?.component?.name).toBeDefined();
21762176
expect(roundtripSymbol?.component?.options?.symbol).toBeDefined();
21772177
});
2178+
2179+
test('Symbol roundtrip: Named symbol converts back to "Symbol" component name', () => {
2180+
const original = JSON.parse(symbolWithInputs) as BuilderContent;
2181+
2182+
// Step 1: Builder -> Mitosis (named component)
2183+
const mitosisComponent = builderContentToMitosisComponent(original);
2184+
expect(mitosisComponent.children[0].name).toBe('SymbolButtonComponent');
2185+
2186+
// Step 2: Mitosis -> Builder (should be "Symbol" not "SymbolButtonComponent")
2187+
const backToBuilder = componentToBuilder()({ component: mitosisComponent });
2188+
const roundtripSymbol = backToBuilder.data?.blocks?.[0];
2189+
2190+
// CRITICAL: Builder Editor requires component.name === "Symbol"
2191+
expect(roundtripSymbol?.component?.name).toBe('Symbol');
2192+
2193+
// Verify symbol metadata is preserved
2194+
expect(roundtripSymbol?.component?.options?.symbol).toBeDefined();
2195+
expect(roundtripSymbol?.component?.options?.symbol?.entry).toBeDefined();
2196+
2197+
// Verify the display name is preserved for next roundtrip
2198+
expect(roundtripSymbol?.component?.options?.symbol?.name).toBe('Button Component');
2199+
2200+
// Verify inputs are merged back into symbol.data
2201+
expect(roundtripSymbol?.component?.options?.symbol?.data).toBeDefined();
2202+
expect(roundtripSymbol?.component?.options?.symbol?.data?.buttonText).toBe('Click me!');
2203+
});
2204+
2205+
test('Symbol roundtrip preserves symbol.name for re-conversion to JSX', () => {
2206+
// Simulate what MCP returns: symbol with name field
2207+
const builderWithSymbolName: BuilderContent = {
2208+
data: {
2209+
blocks: [
2210+
{
2211+
'@type': '@builder.io/sdk:Element',
2212+
'@version': 2,
2213+
id: 'builder-roundtrip-test',
2214+
component: {
2215+
name: 'Symbol',
2216+
options: {
2217+
symbol: {
2218+
entry: 'test-entry-123',
2219+
model: 'symbol',
2220+
name: 'Copyright Reserved', // This should be used for component naming
2221+
data: {},
2222+
},
2223+
},
2224+
},
2225+
},
2226+
],
2227+
},
2228+
};
2229+
2230+
// Builder -> Mitosis: should use symbol.name for component name
2231+
const mitosisComponent = builderContentToMitosisComponent(builderWithSymbolName);
2232+
expect(mitosisComponent.children[0].name).toBe('SymbolCopyrightReserved');
2233+
2234+
// Mitosis -> Builder: should preserve name and use "Symbol" as component name
2235+
const backToBuilder = componentToBuilder()({ component: mitosisComponent });
2236+
const symbol = backToBuilder.data?.blocks?.[0];
2237+
2238+
expect(symbol?.component?.name).toBe('Symbol');
2239+
expect(symbol?.component?.options?.symbol?.name).toBe('Copyright Reserved');
2240+
});
21782241
});

packages/core/src/generators/builder/generator.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,52 @@ export const blockToBuilder = (
656656
const element = mapper(json, options);
657657
return processLocalizedValues(element, json);
658658
}
659+
660+
// Handle Symbol* components (e.g., SymbolCopyrightReserved) - convert back to "Symbol" for Builder
661+
// These are generated by the parser for LLM readability but Builder needs component.name = "Symbol"
662+
if (json.name.startsWith('Symbol') && json.name !== 'Symbol' && json.bindings.symbol?.code) {
663+
const symbolOptions = attempt(() => json5.parse(json.bindings.symbol!.code));
664+
665+
if (!(symbolOptions instanceof Error)) {
666+
if (!symbolOptions.name) {
667+
const displayName = json.name
668+
.replace(/^Symbol/, '')
669+
.replace(/([A-Z])/g, ' $1')
670+
.trim();
671+
if (displayName) {
672+
symbolOptions.name = displayName;
673+
}
674+
}
675+
676+
// Merge any top-level input props back into symbol.data
677+
const inputData: Record<string, any> = {};
678+
for (const key of Object.keys(json.bindings)) {
679+
if (key !== 'symbol' && key !== 'css' && key !== 'style') {
680+
const value = attempt(() => json5.parse(json.bindings[key]!.code));
681+
if (!(value instanceof Error)) {
682+
inputData[key] = value;
683+
}
684+
}
685+
}
686+
if (Object.keys(inputData).length > 0) {
687+
symbolOptions.data = { ...symbolOptions.data, ...inputData };
688+
}
689+
690+
const element = el(
691+
{
692+
component: {
693+
name: 'Symbol', // Always use "Symbol" for Builder compatibility
694+
options: {
695+
symbol: symbolOptions,
696+
},
697+
},
698+
},
699+
options,
700+
);
701+
return processLocalizedValues(element, json);
702+
}
703+
}
704+
659705
if (json.properties._text || json.bindings._text?.code) {
660706
const element = el(
661707
{

0 commit comments

Comments
 (0)