Skip to content

Commit

Permalink
fix: dynamic styles are mapped to bindings when generating builder (#…
Browse files Browse the repository at this point in the history
…1673)

* test

* fix: map dynamic styles when generating builder

* fix types

* update test

* changeset
  • Loading branch information
liamdebeasi authored Jan 31, 2025
1 parent 5594fac commit a38e5bb
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/large-yaks-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@builder.io/mitosis': patch
---

Builder: dynamic styles are mapped to bindings when generating
29 changes: 29 additions & 0 deletions packages/core/src/__tests__/builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,35 @@ describe('Builder', () => {
}
"
`);

const json = componentToBuilder()({ component: mitosis });
expect(json).toMatchInlineSnapshot(`
{
"data": {
"blocks": [
{
"@type": "@builder.io/sdk:Element",
"actions": {},
"bindings": {
"responsiveStyles.large.color": "state.color",
"responsiveStyles.small.left": "state.left",
"responsiveStyles.small.top": "state.top",
"style.fontSize": "state.fontSize",
},
"children": [],
"code": {
"actions": {},
"bindings": {},
},
"properties": {},
"tagName": "div",
},
],
"jsCode": "",
"tsCode": "",
},
}
`);
});
});

Expand Down
152 changes: 151 additions & 1 deletion packages/core/src/generators/builder/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import { replaceNodes } from '@/helpers/replace-identifiers';
import { checkHasState } from '@/helpers/state';
import { isBuilderElement, symbolBlocksAsChildren } from '@/parsers/builder';
import { hashCodeAsString } from '@/symbols/symbol-processor';
import { ForNode, MitosisNode } from '@/types/mitosis-node';
import { Binding, ForNode, MitosisNode } from '@/types/mitosis-node';
import { MitosisStyles } from '@/types/mitosis-styles';
import { TranspilerArgs } from '@/types/transpiler';
import { traverse as babelTraverse, types } from '@babel/core';
import generate from '@babel/generator';
import { parseExpression } from '@babel/parser';
import type { Node } from '@babel/types';
import { BuilderContent, BuilderElement } from '@builder.io/sdk';
import json5 from 'json5';
import { attempt, mapValues, omit, omitBy, set } from 'lodash';
Expand Down Expand Up @@ -333,6 +335,150 @@ const processLocalizedValues = (element: BuilderElement, node: MitosisNode) => {
return element;
};

/**
* Turns a stringified object into an object that can be looped over.
* Since values in the stringified object could be JS expressions, all
* values in the resulting object will remain strings.
* @param input - The stringified object
*/
const parseJSObject = (
input: string,
): {
parsed: Record<string, string>;
unparsed?: string;
} => {
const unparsed: string[] = [];
let parsed: Record<string, string> = {};

try {
const ast = parseExpression(`(${input})`, {
plugins: ['jsx', 'typescript'],
sourceType: 'module',
});

if (ast.type !== 'ObjectExpression') {
return { parsed, unparsed: input };
}

for (const prop of ast.properties) {
/**
* If the object includes spread or method, we stop. We can't really break the component into Key/Value
* and the whole expression is considered dynamic. We return `false` to signify that.
*/
if (prop.type === 'ObjectMethod' || prop.type === 'SpreadElement') {
if (!!prop.start && !!prop.end) {
if (typeof input === 'string') {
unparsed.push(input.slice(prop.start - 1, prop.end - 1));
}
}
continue;
}

/**
* Ignore shorthand objects when processing incomplete objects. Otherwise we may
* create identifiers unintentionally.
* Example: When accounting for shorthand objects, "{ color" would become
* { color: color } thus creating a "color" identifier that does not exist.
*/
if (prop.type === 'ObjectProperty') {
if (prop.extra?.shorthand) {
if (typeof input === 'string') {
unparsed.push(input.slice(prop.start! - 1, prop.end! - 1));
}
continue;
}

let key = '';
if (prop.key.type === 'Identifier') {
key = prop.key.name;
} else if (prop.key.type === 'StringLiteral') {
key = prop.key.value;
} else {
continue;
}

if (typeof input === 'string') {
const [val, err] = extractValue(input, prop.value);
if (err === null) {
parsed[key] = val;
}
}
}
}

return {
parsed,
unparsed: unparsed.length > 0 ? `{${unparsed.join('\n')}}` : undefined,
};
} catch (err) {
return {
parsed,
unparsed: unparsed.length > 0 ? `{${unparsed.join('\n')}}` : undefined,
};
}
};

const extractValue = (input: string, node: Node | null): [string, null] | [null, string] => {
const start = node?.loc?.start;
const end = node?.loc?.end;
const startIndex =
start !== undefined && 'index' in start && typeof start['index'] === 'number'
? start['index']
: undefined;
const endIndex =
end !== undefined && 'index' in end && typeof end['index'] === 'number'
? end['index']
: undefined;

if (startIndex === undefined || endIndex === undefined || node === null) {
const err = `bad value: ${node}`;
return [null, err];
}

const value = input.slice(startIndex - 1, endIndex - 1);
return [value, null];
};

/**
* Maps and styles that are bound with dynamic values onto their respective
* binding keys for Builder elements. This function also maps media queries
* with dynamic values.
* @param - bindings - The bindings object that has your styles. This param
* will be modified in-place, and the old "style" key will be removed.
*/
const mapBoundStyles = (bindings: { [key: string]: Binding | undefined }) => {
const styles = bindings['style'];
if (!styles) {
return;
}
const { parsed } = parseJSObject(styles.code);

for (const key in parsed) {
const mediaQueryMatch = key.match(mediaQueryRegex);

if (mediaQueryMatch) {
const { parsed: mParsed } = parseJSObject(parsed[key]);
const [_, pixelSize] = mediaQueryMatch;
const size = sizes.getSizeForWidth(Number(pixelSize));
for (const mKey in mParsed) {
bindings[`responsiveStyles.${size}.${mKey}`] = {
code: mParsed[mKey],
bindingType: 'expression',
type: 'single',
};
}
} else {
bindings[`style.${key}`] = {
code: parsed[key],
bindingType: 'expression',
type: 'single',
};
}
}

delete bindings['style'];
};

export const blockToBuilder = (
json: MitosisNode,
options: ToBuilderOptions = {},
Expand Down Expand Up @@ -393,6 +539,10 @@ export const blockToBuilder = (
actionBody;
delete bindings[key];
}

if (key === 'style') {
mapBoundStyles(bindings);
}
}

const builderBindings: Record<string, string> = {};
Expand Down

0 comments on commit a38e5bb

Please sign in to comment.