@builder.io/mitosis
Version:
Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io
662 lines (657 loc) • 27.4 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.componentToBuilder = exports.blockToBuilder = void 0;
const media_sizes_1 = require("../../constants/media-sizes");
const dedent_1 = require("../../helpers/dedent");
const fast_clone_1 = require("../../helpers/fast-clone");
const filter_empty_text_nodes_1 = require("../../helpers/filter-empty-text-nodes");
const get_state_object_string_1 = require("../../helpers/get-state-object-string");
const has_props_1 = require("../../helpers/has-props");
const is_component_1 = require("../../helpers/is-component");
const is_mitosis_node_1 = require("../../helpers/is-mitosis-node");
const is_upper_case_1 = require("../../helpers/is-upper-case");
const parsers_1 = require("../../helpers/parsers");
const remove_surrounding_block_1 = require("../../helpers/remove-surrounding-block");
const replace_identifiers_1 = require("../../helpers/replace-identifiers");
const state_1 = require("../../helpers/state");
const builder_1 = require("../../parsers/builder");
const symbol_processor_1 = require("../../symbols/symbol-processor");
const core_1 = require("@babel/core");
const generator_1 = __importDefault(require("@babel/generator"));
const parser_1 = require("@babel/parser");
const json5_1 = __importDefault(require("json5"));
const lodash_1 = require("lodash");
const legacy_1 = __importDefault(require("neotraverse/legacy"));
const standalone_1 = require("prettier/standalone");
const on_mount_1 = require("../helpers/on-mount");
const omitMetaProperties = (obj) => (0, lodash_1.omitBy)(obj, (_value, key) => key.startsWith('$'));
const builderBlockPrefixes = ['Amp', 'Core', 'Builder', 'Raw', 'Form'];
const mapComponentName = (name) => {
if (name === 'CustomCode') {
return 'Custom Code';
}
for (const prefix of builderBlockPrefixes) {
if (name.startsWith(prefix)) {
const suffix = name.replace(prefix, '');
const restOfName = suffix[0];
if (restOfName && (0, is_upper_case_1.isUpperCase)(restOfName)) {
return `${prefix}:${name.replace(prefix, '')}`;
}
}
}
return name;
};
const componentMappers = {
// TODO: add back if this direction (blocks as children not prop) is desired
...(!builder_1.symbolBlocksAsChildren
? {}
: {
Symbol(node, options) {
const child = node.children[0];
const symbolOptions = (node.bindings.symbol && json5_1.default.parse(node.bindings.symbol.code)) || {};
if (child) {
(0, lodash_1.set)(symbolOptions, 'content.data.blocks', child.children.map((item) => (0, exports.blockToBuilder)(item, options)));
}
return el({
component: {
name: 'Symbol',
options: {
// TODO: forward other symbol options
symbol: symbolOptions,
},
},
}, options);
},
}),
Columns(node, options) {
const block = (0, exports.blockToBuilder)(node, options, { skipMapper: true });
const columns = block.children.map((item) => {
var _a, _b;
return ({
blocks: item.children,
width: (_b = (_a = item.component) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.width,
});
});
block.component.options.columns = columns;
block.children = [];
return block;
},
Fragment(node, options) {
const block = (0, exports.blockToBuilder)(node, options, { skipMapper: true });
block.component = { name: 'Core:Fragment' };
block.tagName = undefined;
return block;
},
PersonalizationContainer(node, options) {
const block = (0, exports.blockToBuilder)(node, options, { skipMapper: true });
const variants = [];
let defaultVariant = [];
const validFakeNodeNames = [
'Variant',
'PersonalizationOption',
'PersonalizationVariant',
'Personalization',
];
block.children.forEach((item) => {
var _a;
if (item.component && validFakeNodeNames.includes((_a = item.component) === null || _a === void 0 ? void 0 : _a.name)) {
let query;
if (item.component.options.query) {
const optionsQuery = item.component.options.query;
if (Array.isArray(optionsQuery)) {
query = optionsQuery.map((q) => ({
'@type': '@builder.io/core:Query',
...q,
}));
}
else {
query = [
{
'@type': '@builder.io/core:Query',
...optionsQuery,
},
];
}
const newVariant = {
...item.component.options,
query,
blocks: item.children,
};
variants.push(newVariant);
}
else if (item.children) {
defaultVariant.push(...item.children);
}
}
else {
defaultVariant.push(item);
}
});
delete block.properties;
delete block.bindings;
block.component.options.variants = variants;
block.children = defaultVariant;
return block;
},
For(_node, options) {
var _a;
const node = _node;
const replaceIndexNode = (str) => (0, replace_identifiers_1.replaceNodes)({
code: str,
nodeMaps: [
{
from: core_1.types.identifier(target),
to: core_1.types.memberExpression(core_1.types.identifier('state'), core_1.types.identifier('$index')),
},
],
});
// rename `index` var to `state.$index`
const target = node.scope.indexName || 'index';
const replaceIndex = (node) => {
(0, legacy_1.default)(node).forEach(function (thing) {
if (!(0, is_mitosis_node_1.isMitosisNode)(thing))
return;
for (const [key, value] of Object.entries(thing.bindings)) {
if (!value)
continue;
if (!value.code.includes(target))
continue;
if (value.type === 'single' && value.bindingType === 'function') {
try {
const code = value.code;
const programNode = (0, parsers_1.parseCodeToAst)(code);
if (!programNode)
continue;
(0, core_1.traverse)(programNode, {
Program(path) {
if (path.scope.hasBinding(target))
return;
const x = {
id: core_1.types.identifier(target),
init: core_1.types.identifier('PLACEHOLDER'),
};
path.scope.push(x);
path.scope.rename(target, 'state.$index');
path.traverse({
VariableDeclaration(p) {
if (p.node.declarations.length === 1 && p.node.declarations[0].id === x.id) {
p.remove();
}
},
});
},
});
thing.bindings[key].code = (0, generator_1.default)(programNode).code;
}
catch (error) {
console.error('Error processing function binding. Falling back to simple replacement.', error);
thing.bindings[key].code = replaceIndexNode(value.code);
}
}
else {
thing.bindings[key].code = replaceIndexNode(value.code);
}
}
});
return node;
};
return el({
component: {
name: 'Core:Fragment',
},
repeat: {
collection: (_a = node.bindings.each) === null || _a === void 0 ? void 0 : _a.code,
itemName: node.scope.forName,
},
children: node.children
.filter(filter_empty_text_nodes_1.filterEmptyTextNodes)
.map((node) => (0, exports.blockToBuilder)(replaceIndex(node), options)),
}, options);
},
Show(node, options) {
var _a, _b, _c;
const elseCase = node.meta.else;
const children = node.children.filter(filter_empty_text_nodes_1.filterEmptyTextNodes);
const showNode = children.length > 0
? el({
// TODO: the reverse mapping for this
component: {
name: 'Core:Fragment',
},
bindings: {
show: (_a = node.bindings.when) === null || _a === void 0 ? void 0 : _a.code,
},
children: children.map((node) => (0, exports.blockToBuilder)(node, options)),
}, options)
: undefined;
const elseNode = elseCase && (0, filter_empty_text_nodes_1.filterEmptyTextNodes)(elseCase)
? el({
// TODO: the reverse mapping for this
component: {
name: 'Core:Fragment',
},
bindings: {
hide: (_b = node.bindings.when) === null || _b === void 0 ? void 0 : _b.code,
},
children: [(0, exports.blockToBuilder)(elseCase, options)],
}, options)
: undefined;
if (elseNode && showNode) {
return el({
component: {
name: 'Core:Fragment',
},
children: [showNode, elseNode],
}, options);
}
else if (showNode) {
return showNode;
}
else if (elseNode) {
return elseNode;
}
return el({
// TODO: the reverse mapping for this
component: {
name: 'Core:Fragment',
},
bindings: {
show: (_c = node.bindings.when) === null || _c === void 0 ? void 0 : _c.code,
},
children: [],
}, options);
},
};
const el = (options, toBuilderOptions) => ({
'@type': '@builder.io/sdk:Element',
...(toBuilderOptions.includeIds && {
id: 'builder-' + (0, symbol_processor_1.hashCodeAsString)(options),
}),
...options,
});
function tryFormat(code) {
let str = code;
try {
str = (0, standalone_1.format)(str, {
parser: 'babel',
plugins: [
require('prettier/parser-babel'), // To support running in browsers
],
});
}
catch (err) {
console.error('Format error for code:', str);
throw err;
}
return str;
}
const processLocalizedValues = (element, node) => {
if (node.localizedValues) {
for (const [path, value] of Object.entries(node.localizedValues)) {
(0, lodash_1.set)(element, path, value);
}
}
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) => {
var _a;
const unparsed = [];
let parsed = {};
try {
const ast = (0, parser_1.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 ((_a = prop.extra) === null || _a === void 0 ? void 0 : _a.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, node) => {
var _a, _b;
const start = (_a = node === null || node === void 0 ? void 0 : node.loc) === null || _a === void 0 ? void 0 : _a.start;
const end = (_b = node === null || node === void 0 ? void 0 : node.loc) === null || _b === void 0 ? void 0 : _b.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) => {
const styles = bindings['style'];
if (!styles) {
return;
}
const { parsed, unparsed } = parseJSObject(styles.code);
for (const key in parsed) {
const mediaQueryMatch = key.match(media_sizes_1.mediaQueryRegex);
if (mediaQueryMatch) {
const { parsed: mParsed } = parseJSObject(parsed[key]);
const [_, pixelSize] = mediaQueryMatch;
const size = media_sizes_1.sizes.getSizeForWidth(Number(pixelSize));
for (const mKey in mParsed) {
bindings[`responsiveStyles.${size}.${mKey}`] = {
code: mParsed[mKey],
bindingType: 'expression',
type: 'single',
};
}
}
else {
if (isGlobalStyle(key)) {
console.warn(`The following bound styles are not supported by Builder JSON and have been removed:
"${key}": ${parsed[key]}
`);
}
else {
bindings[`style.${key}`] = {
code: parsed[key],
bindingType: 'expression',
type: 'single',
};
}
}
}
delete bindings['style'];
// unparsed data could be something else such as a function call
if (unparsed) {
try {
const ast = (0, parser_1.parseExpression)(`(${unparsed})`, {
plugins: ['jsx', 'typescript'],
sourceType: 'module',
});
// style={state.getStyles()}
if (ast.type === 'CallExpression') {
bindings['style'] = {
code: unparsed,
bindingType: 'expression',
type: 'single',
};
}
else {
throw 'unsupported style';
}
}
catch (_a) {
console.warn(`The following bound styles are invalid and have been removed: ${unparsed}`);
}
}
};
function isGlobalStyle(key) {
// These are mapped to their respective responsiveStyle and support bindings
if (/max-width: (.*?)px/gm.exec(key)) {
return false;
}
return (
// pseudo class
key.startsWith('&:') ||
key.startsWith(':') ||
// @ rules
key.startsWith('@'));
}
const blockToBuilder = (json, options = {}, _internalOptions = {}) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
const mapper = !_internalOptions.skipMapper && componentMappers[json.name];
if (mapper) {
const element = mapper(json, options);
return processLocalizedValues(element, json);
}
if (json.properties._text || ((_a = json.bindings._text) === null || _a === void 0 ? void 0 : _a.code)) {
const element = el({
tagName: 'span',
bindings: {
...(((_b = json.bindings._text) === null || _b === void 0 ? void 0 : _b.code)
? {
'component.options.text': json.bindings._text.code,
'json.bindings._text.code': undefined,
}
: {}),
},
component: {
name: 'Text',
options: {
// Mitosis uses {} for bindings, but Builder expects {{}} so we need to convert
text: (_c = json.properties._text) === null || _c === void 0 ? void 0 : _c.replace(/\{(.*?)\}/g, '{{$1}}'),
},
},
}, options);
return processLocalizedValues(element, json);
}
const thisIsComponent = (0, is_component_1.isComponent)(json);
let bindings = json.bindings;
const actions = {};
for (const key in bindings) {
const eventBindingKeyRegex = /^on([A-Z])/;
const firstCharMatchForEventBindingKey = (_d = key.match(eventBindingKeyRegex)) === null || _d === void 0 ? void 0 : _d[1];
if (firstCharMatchForEventBindingKey) {
let actionBody = ((_e = bindings[key]) === null || _e === void 0 ? void 0 : _e.async)
? `(async () => ${(_f = bindings[key]) === null || _f === void 0 ? void 0 : _f.code})()`
: (0, remove_surrounding_block_1.removeSurroundingBlock)((_g = bindings[key]) === null || _g === void 0 ? void 0 : _g.code);
const eventIdentifier = (_j = (_h = bindings[key]) === null || _h === void 0 ? void 0 : _h.arguments) === null || _j === void 0 ? void 0 : _j[0];
if (typeof eventIdentifier === 'string' && eventIdentifier !== 'event') {
actionBody = (0, replace_identifiers_1.replaceNodes)({
code: actionBody,
nodeMaps: [{ from: core_1.types.identifier(eventIdentifier), to: core_1.types.identifier('event') }],
});
}
actions[key.replace(eventBindingKeyRegex, firstCharMatchForEventBindingKey.toLowerCase())] =
actionBody;
delete bindings[key];
}
if (key === 'style') {
mapBoundStyles(bindings);
}
}
const builderBindings = {};
const componentOptions = omitMetaProperties(json.properties);
if (thisIsComponent) {
for (const key in bindings) {
if (key === 'css') {
continue;
}
const value = bindings[key];
const parsed = (0, lodash_1.attempt)(() => json5_1.default.parse(value === null || value === void 0 ? void 0 : value.code));
if (!(parsed instanceof Error)) {
componentOptions[key] = parsed;
}
else {
if (!((_k = json.slots) === null || _k === void 0 ? void 0 : _k[key])) {
builderBindings[`component.options.${key}`] = bindings[key].code;
}
}
}
}
for (const key in json.slots) {
componentOptions[key] = json.slots[key].map((node) => (0, exports.blockToBuilder)(node, options));
}
const hasCss = !!((_l = bindings.css) === null || _l === void 0 ? void 0 : _l.code);
let responsiveStyles = {
large: {},
};
if (hasCss) {
const cssRules = json5_1.default.parse((_m = bindings.css) === null || _m === void 0 ? void 0 : _m.code);
const cssRuleKeys = Object.keys(cssRules);
for (const ruleKey of cssRuleKeys) {
const mediaQueryMatch = ruleKey.match(media_sizes_1.mediaQueryRegex);
if (mediaQueryMatch) {
const [fullmatch, pixelSize] = mediaQueryMatch;
const sizeForWidth = media_sizes_1.sizes.getSizeForWidth(Number(pixelSize));
const currentSizeStyles = responsiveStyles[sizeForWidth] || {};
responsiveStyles[sizeForWidth] = {
...currentSizeStyles,
...cssRules[ruleKey],
};
}
else {
responsiveStyles.large = {
...responsiveStyles.large,
[ruleKey]: cssRules[ruleKey],
};
}
}
delete json.bindings.css;
}
const element = el({
tagName: thisIsComponent ? undefined : json.name,
...(hasCss && {
responsiveStyles,
}),
layerName: json.properties.$name,
...(thisIsComponent && {
component: {
name: mapComponentName(json.name),
options: componentOptions,
},
}),
code: {
bindings: builderBindings,
actions,
},
properties: thisIsComponent ? undefined : omitMetaProperties(json.properties),
bindings: thisIsComponent
? builderBindings
: (0, lodash_1.omit)((0, lodash_1.mapValues)(bindings, (value) => value === null || value === void 0 ? void 0 : value.code), 'css'),
actions,
children: json.children
.filter(filter_empty_text_nodes_1.filterEmptyTextNodes)
.map((child) => (0, exports.blockToBuilder)(child, options)),
}, options);
return processLocalizedValues(element, json);
};
exports.blockToBuilder = blockToBuilder;
const componentToBuilder = (options = {}) => ({ component }) => {
var _a, _b;
const hasState = (0, state_1.checkHasState)(component);
const result = (0, fast_clone_1.fastClone)({
data: {
httpRequests: (_b = (_a = component === null || component === void 0 ? void 0 : component.meta) === null || _a === void 0 ? void 0 : _a.useMetadata) === null || _b === void 0 ? void 0 : _b.httpRequests,
jsCode: tryFormat((0, dedent_1.dedent) `
${!(0, has_props_1.hasProps)(component) ? '' : `var props = state;`}
${!hasState ? '' : `Object.assign(state, ${(0, get_state_object_string_1.getStateObjectStringFromComponent)(component)});`}
${(0, on_mount_1.stringifySingleScopeOnMount)(component)}
`),
tsCode: tryFormat((0, dedent_1.dedent) `
${!(0, has_props_1.hasProps)(component) ? '' : `var props = state;`}
${!hasState ? '' : `useStore(${(0, get_state_object_string_1.getStateObjectStringFromComponent)(component)});`}
${!component.hooks.onMount.length
? ''
: `onMount(() => {
${(0, on_mount_1.stringifySingleScopeOnMount)(component)}
})`}
`),
cssCode: component === null || component === void 0 ? void 0 : component.style,
blocks: component.children
.filter(filter_empty_text_nodes_1.filterEmptyTextNodes)
.map((child) => (0, exports.blockToBuilder)(child, options)),
},
});
const subComponentMap = {};
for (const subComponent of component.subComponents) {
const name = subComponent.name;
subComponentMap[name] = (0, exports.componentToBuilder)(options)({
component: subComponent,
});
}
(0, legacy_1.default)([result, subComponentMap]).forEach(function (el) {
var _a;
if ((0, builder_1.isBuilderElement)(el)) {
const value = subComponentMap[(_a = el.component) === null || _a === void 0 ? void 0 : _a.name];
if (value) {
(0, lodash_1.set)(el, 'component.options.symbol.content', value);
}
if (el.bindings) {
for (const [key, value] of Object.entries(el.bindings)) {
if (value.match(/\n|;/)) {
if (!el.code) {
el.code = {};
}
if (!el.code.bindings) {
el.code.bindings = {};
}
el.code.bindings[key] = value;
el.bindings[key] = ` return ${value}`;
}
}
}
}
});
return result;
};
exports.componentToBuilder = componentToBuilder;
;