@builder.io/mitosis
Version:
Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io
333 lines (332 loc) • 14.9 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.blockToReact = void 0;
const event_handlers_1 = require("../../helpers/event-handlers");
const filter_empty_text_nodes_1 = require("../../helpers/filter-empty-text-nodes");
const is_children_1 = __importDefault(require("../../helpers/is-children"));
const is_root_text_node_1 = require("../../helpers/is-root-text-node");
const is_valid_attribute_name_1 = require("../../helpers/is-valid-attribute-name");
const for_1 = require("../../helpers/nodes/for");
const slots_1 = require("../../helpers/slots");
const mitosis_node_1 = require("../../types/mitosis-node");
const html_tags_1 = require("../../constants/html_tags");
const helpers_1 = require("./helpers");
const state_1 = require("./helpers/state");
const NODE_MAPPERS = {
Slot(json, options, component, _insideJsx, parentSlots) {
var _a, _b;
const slotName = ((_a = json.bindings.name) === null || _a === void 0 ? void 0 : _a.code) || json.properties.name;
const hasChildren = json.children.length;
const renderChildren = () => {
var _a;
const childrenStr = (_a = json.children) === null || _a === void 0 ? void 0 : _a.map((item) => (0, exports.blockToReact)(item, options, component, true)).join('\n').trim();
/**
* Ad-hoc way of figuring out if the children defaultProp is:
* - a JSX element, e.g. `<div>foo</div>`
* - a JS expression, e.g. `true`, `false`
* - a string, e.g. `'Default text'`
*
* and correctly wrapping it in quotes when appropriate.
*/
if (childrenStr.startsWith(`<`) && childrenStr.endsWith(`>`)) {
return childrenStr;
}
else if (['false', 'true', 'null', 'undefined'].includes(childrenStr)) {
return childrenStr;
}
else {
return `"${childrenStr}"`;
}
};
if (!slotName) {
// TODO: update MitosisNode for simple code
const key = Object.keys(json.bindings).find(Boolean);
if (key && parentSlots) {
parentSlots.push({ key, value: (_b = json.bindings[key]) === null || _b === void 0 ? void 0 : _b.code });
return '';
}
const children = (0, helpers_1.processBinding)('props.children', options);
return `<>{${children} ${hasChildren ? `|| (${renderChildren()})` : ''}}</>`;
}
let slotProp = (0, helpers_1.processBinding)(slotName, options).replace('name=', '');
if (!slotProp.startsWith('props.')) {
slotProp = `props.${slotProp}`;
}
return `<>{${slotProp} ${hasChildren ? `|| (${renderChildren()})` : ''}}</>`;
},
Fragment(json, options, component) {
const wrap = (0, helpers_1.isFragmentWithKey)(json) || (0, helpers_1.wrapInFragment)(json) || (0, is_root_text_node_1.isRootTextNode)(json);
return `${wrap ? (0, helpers_1.getFragment)('open', options, json) : ''}${json.children
.map((item) => (0, exports.blockToReact)(item, options, component, wrap))
.join('\n')}${wrap ? (0, helpers_1.getFragment)('close', options, json) : ''}`;
},
For(_json, options, component, insideJsx) {
var _a;
const json = _json;
const wrap = (0, helpers_1.wrapInFragment)(json);
const forArguments = (0, for_1.getForArguments)(json).join(', ');
const expression = `${(0, helpers_1.processBinding)((_a = json.bindings.each) === null || _a === void 0 ? void 0 : _a.code, options)}?.map((${forArguments}) => (
${wrap ? (0, helpers_1.openFrag)(options) : ''}${json.children
.filter(filter_empty_text_nodes_1.filterEmptyTextNodes)
.map((item) => (0, exports.blockToReact)(item, options, component, wrap))
.join('\n')}${wrap ? (0, helpers_1.closeFrag)(options) : ''}
))`;
if (insideJsx) {
return `{${expression}}`;
}
else {
return expression;
}
},
Show(json, options, component, insideJsx) {
var _a;
const wrap = (0, helpers_1.wrapInFragment)(json) ||
(0, is_root_text_node_1.isRootTextNode)(json) ||
component.children[0] === json ||
// when `<Show><For>...</For></Show>`, we need to wrap the For generated code in a fragment
// since it's a `.map()` call
(json.children.length === 1 && ['For', 'Show'].includes(json.children[0].name));
const wrapElse = !!(json.meta.else &&
((0, helpers_1.wrapInFragment)(json.meta.else) || (0, mitosis_node_1.checkIsForNode)(json.meta.else)));
const expression = `${(0, helpers_1.processBinding)((_a = json.bindings.when) === null || _a === void 0 ? void 0 : _a.code, options)} ? (
${wrap ? (0, helpers_1.openFrag)(options) : ''}${json.children
.filter(filter_empty_text_nodes_1.filterEmptyTextNodes)
.map((item) => (0, exports.blockToReact)(item, options, component, wrap))
.join('\n')}${wrap ? (0, helpers_1.closeFrag)(options) : ''}
) : ${!json.meta.else
? 'null'
: (wrapElse ? (0, helpers_1.openFrag)(options) : '') +
(0, exports.blockToReact)(json.meta.else, options, component, wrapElse) +
(wrapElse ? (0, helpers_1.closeFrag)(options) : '')}`;
if (insideJsx) {
return `{${expression}}`;
}
else {
return expression;
}
},
};
const ATTTRIBUTE_MAPPERS = {
spellcheck: 'spellCheck',
autocapitalize: 'autoCapitalize',
autocomplete: 'autoComplete',
for: 'htmlFor',
};
// TODO: Maybe in the future allow defining `string | function` as values
const BINDING_MAPPERS = {
ref(ref, value, options) {
if (options === null || options === void 0 ? void 0 : options.preact) {
return [ref, value];
}
const regexp = /(.+)?props\.(.+)( |\)|;|\()?$/m;
if (regexp.test(value)) {
const match = regexp.exec(value);
const prop = match === null || match === void 0 ? void 0 : match[2];
if (prop) {
return [ref, prop];
}
}
return [ref, value];
},
innerHTML(_key, value, _options, isProperty) {
const wrapChar = isProperty ? '"' : '';
let useValue = value.replace(/\s+/g, ' ');
if (isProperty) {
useValue = value.replace(/"/g, '\\"');
}
return ['dangerouslySetInnerHTML', `{__html: ${wrapChar}${useValue}${wrapChar}}`];
},
...ATTTRIBUTE_MAPPERS,
};
const NATIVE_EVENT_MAPPER = {
onClick: 'onPress',
};
const blockToReact = (json, options, component, insideJsx, parentSlots = []) => {
var _a, _b, _c, _d, _e;
const needsToRenderSlots = [];
if (NODE_MAPPERS[json.name]) {
return NODE_MAPPERS[json.name](json, options, component, insideJsx, parentSlots);
}
if (json.properties._text) {
const text = json.properties._text;
if (options.type === 'native' && text.trim().length) {
return `<Text>${text}</Text>`;
}
return text;
}
if ((_a = json.bindings._text) === null || _a === void 0 ? void 0 : _a.code) {
const processed = (0, helpers_1.processBinding)(json.bindings._text.code, options);
if (options.type === 'native' &&
!(0, is_children_1.default)({ node: json }) &&
!(0, slots_1.isSlotProperty)(json.bindings._text.code.split('.')[1] || '')) {
return `<Text>{${processed}}</Text>`;
}
return `{${processed}}`;
}
let str = '';
str += `<${json.name} `;
for (const key in json.properties) {
const value = (json.properties[key] || '').replace(/"/g, '"').replace(/\n/g, '\\n');
// Handle src for Image
// Handle src for Image
if (json.name === 'Image' && key === 'src') {
let src;
const imageSource = json.properties.src;
if (imageSource) {
const isUrl = /^(http|https):\/\/[^ "]+$/.test(imageSource);
src = isUrl ? `{ uri: '${imageSource}' }` : `require('${imageSource}')`;
str += `source={${src}} `;
continue; // Skip further processing for 'src' in Image
}
}
// Handle href for TouchableOpacity
if (json.name === 'TouchableOpacity' && key === 'href') {
const hrefValue = (0, helpers_1.processBinding)(value, options);
if (hrefValue) {
const onPress = `() => Linking.openURL(${JSON.stringify(hrefValue)})`;
str += ` onPress={${onPress}} `;
}
continue; // Skip further processing for 'href' in TouchableOpacity
}
// Ignore target for TouchableOpacity
if (json.name === 'TouchableOpacity' && key === 'target') {
continue; // Skip further processing for 'target' in TouchableOpacity
}
if (key === 'class') {
str = `${str.trim()} className="${value}" `;
}
else if (BINDING_MAPPERS[key]) {
const mapper = BINDING_MAPPERS[key];
if (typeof mapper === 'function') {
const [newKey, newValue] = mapper(key, value, options, true);
str += ` ${newKey}={${newValue}} `;
}
else {
str += ` ${BINDING_MAPPERS[key]}="${value}" `;
}
}
else {
if ((0, is_valid_attribute_name_1.isValidAttributeName)(key)) {
str += ` ${key}="${value.replace(/"/g, '"')}" `;
}
}
}
for (const key in json.bindings) {
// ignore duplicate slot attribute
if ((_b = json.slots) === null || _b === void 0 ? void 0 : _b[key]) {
continue;
}
const value = String((_c = json.bindings[key]) === null || _c === void 0 ? void 0 : _c.code);
if (key === 'css' && value.trim() === '{}') {
continue;
}
const useBindingValue = (0, helpers_1.processBinding)(value, options);
if (json.name === 'Image' && key === 'src') {
let src;
const imageSource = (0, helpers_1.processBinding)(value, options);
if (imageSource) {
src = `{ uri: ${imageSource} }`;
str += `source={${src}} `;
continue; // Skip further processing for 'src' in Image
}
}
// Handle href for TouchableOpacity
if (json.name === 'TouchableOpacity' && key === 'href') {
const hrefValue = (0, helpers_1.processBinding)(value, options);
if (hrefValue) {
const onPress = `() => Linking.openURL(${hrefValue})`;
str += ` onPress={${onPress}} `;
continue; // Skip further processing for 'href' in TouchableOpacity
}
}
// Ignore target for TouchableOpacity
if (json.name === 'TouchableOpacity' && key === 'target') {
continue; // Skip further processing for 'target' in TouchableOpacity
}
if (((_d = json.bindings[key]) === null || _d === void 0 ? void 0 : _d.type) === 'spread') {
str += ` {...(${value})} `;
}
else if ((0, event_handlers_1.checkIsEvent)(key)) {
const asyncKeyword = ((_e = json.bindings[key]) === null || _e === void 0 ? void 0 : _e.async) ? 'async ' : '';
const { arguments: cusArgs = ['event'] } = json.bindings[key];
const eventName = options.type === 'native' ? NATIVE_EVENT_MAPPER[key] || key : key;
str += ` ${eventName}={${asyncKeyword}(${cusArgs.join(',')}) => ${(0, state_1.updateStateSettersInCode)(useBindingValue, options)} } `;
}
else if (key.startsWith('slot')) {
// <Component slotProjected={<AnotherComponent />} />
str += ` ${key}={${value}} `;
}
else if (key === 'class') {
str += ` className={${useBindingValue}} `;
}
else if (BINDING_MAPPERS[key]) {
const mapper = BINDING_MAPPERS[key];
if (typeof mapper === 'function') {
const [newKey, newValue] = mapper(key, useBindingValue, options);
str += ` ${newKey}={${newValue}} `;
}
else {
if (useBindingValue === 'true') {
str += ` ${BINDING_MAPPERS[key]} `;
}
else {
str += ` ${BINDING_MAPPERS[key]}={${useBindingValue}} `;
}
}
}
else if (key === 'style' && options.type === 'native' && json.name === 'ScrollView') {
// React Native's ScrollView has a different prop for styles: `contentContainerStyle`
str += ` contentContainerStyle={${useBindingValue}} `;
}
else {
if ((0, is_valid_attribute_name_1.isValidAttributeName)(key)) {
if (useBindingValue === 'true') {
str += ` ${key} `;
}
else {
str += ` ${key}={${useBindingValue}} `;
}
}
}
}
if (json.slots) {
for (const key in json.slots) {
const value = json.slots[key];
if (!(value === null || value === void 0 ? void 0 : value.length)) {
continue;
}
const reactComponents = value.map((node) => (0, exports.blockToReact)(node, options, component, true));
const slotStringValue = reactComponents.length === 1 ? reactComponents[0] : `<>${reactComponents.join('\n')}</>`;
str += ` ${key}={${slotStringValue}} `;
}
}
if (html_tags_1.SELF_CLOSING_HTML_TAGS.has(json.name)) {
return str + ' />';
}
// Self close by default if no children
if (!json.children.length) {
str += ' />';
return str;
}
// TODO: update MitosisNode for simple code
let childrenNodes = '';
if (json.children) {
childrenNodes = json.children
.map((item) => (0, exports.blockToReact)(item, options, component, true, needsToRenderSlots))
.join('');
}
if (needsToRenderSlots.length) {
needsToRenderSlots.forEach(({ key, value }) => {
str += ` ${key}={${value}} `;
});
}
str = str.trim() + '>';
if (json.children) {
str += childrenNodes;
}
return str + `</${json.name}>`;
};
exports.blockToReact = blockToReact;
;