@builder.io/mitosis
Version:
Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io
419 lines (418 loc) • 19.5 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.jsxElementToJson = void 0;
const babel = __importStar(require("@babel/core"));
const generator_1 = __importDefault(require("@babel/generator"));
const function_1 = require("fp-ts/lib/function");
const json5_1 = __importDefault(require("json5"));
const bindings_1 = require("../../helpers/bindings");
const create_mitosis_node_1 = require("../../helpers/create-mitosis-node");
const nullable_1 = require("../../helpers/nullable");
const helpers_1 = require("./helpers");
const { types } = babel;
const getBodyExpression = (node) => {
if (types.isArrowFunctionExpression(node) || types.isFunctionExpression(node)) {
const callback = node.body;
if (callback.type === 'BlockStatement') {
for (const statement of callback.body) {
if (statement.type === 'ReturnStatement') {
return statement.argument;
}
}
}
else {
return callback;
}
}
return undefined;
};
const getForArguments = (params) => {
const [forName, indexName, collectionName] = params
.filter((param) => types.isIdentifier(param))
.map((param) => param.name)
.filter(nullable_1.checkIsDefined);
return {
forName,
collectionName,
indexName,
};
};
/**
* Parses a JSX element into a MitosisNode.
*/
const jsxElementToJson = (node, options) => {
var _a;
if (types.isJSXText(node)) {
const value = typeof ((_a = node.extra) === null || _a === void 0 ? void 0 : _a.raw) === 'string' ? node.extra.raw : node.value;
return (0, create_mitosis_node_1.createMitosisNode)({
properties: {
_text: value,
},
});
}
if (types.isJSXEmptyExpression(node)) {
return null;
}
if (types.isJSXExpressionContainer(node)) {
return (0, exports.jsxElementToJson)(node.expression, options);
}
if ((types.isCallExpression(node) || types.isOptionalCallExpression(node)) &&
(node.callee.type === 'MemberExpression' || node.callee.type === 'OptionalMemberExpression')) {
const isMap = node.callee.property.type === 'Identifier' && node.callee.property.name === 'map';
const isArrayFrom = node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'from' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'Array';
if (isMap) {
const callback = node.arguments[0];
const bodyExpression = getBodyExpression(callback);
if (bodyExpression) {
const forArguments = getForArguments(callback.params);
return (0, create_mitosis_node_1.createMitosisNode)({
name: 'For',
bindings: {
each: (0, bindings_1.createSingleBinding)({
code: (0, generator_1.default)(node.callee.object, {
compact: true,
}).code,
}),
},
scope: forArguments,
children: [(0, exports.jsxElementToJson)(bodyExpression, options)].filter(nullable_1.checkIsDefined),
});
}
}
else if (isArrayFrom) {
// Array.from
const each = node.arguments[0];
const callback = node.arguments[1];
const bodyExpression = getBodyExpression(callback);
if (bodyExpression) {
const forArguments = getForArguments(callback.params);
return (0, create_mitosis_node_1.createMitosisNode)({
name: 'For',
bindings: {
each: (0, bindings_1.createSingleBinding)({
code: (0, generator_1.default)({
...node,
arguments: [each],
}, {
compact: true,
}).code,
}),
},
scope: forArguments,
children: [(0, exports.jsxElementToJson)(bodyExpression, options)],
});
}
}
}
else if (types.isLogicalExpression(node)) {
// {foo && <div />} -> <Show when={foo}>...</Show>
if (node.operator === '&&') {
return (0, create_mitosis_node_1.createMitosisNode)({
name: 'Show',
bindings: {
when: (0, bindings_1.createSingleBinding)({
code: (0, generator_1.default)(node.left, {
compact: true,
}).code,
}),
},
children: [(0, exports.jsxElementToJson)(node.right, options)].filter(nullable_1.checkIsDefined),
});
}
else {
// TODO: good warning system for unsupported operators
}
}
else if (types.isConditionalExpression(node)) {
// {foo ? <div /> : <span />} -> <Show when={foo} else={<span />}>...</Show>
const child = (0, exports.jsxElementToJson)(node.consequent, options);
const elseCase = (0, exports.jsxElementToJson)(node.alternate, options);
return (0, create_mitosis_node_1.createMitosisNode)({
name: 'Show',
meta: {
...((0, nullable_1.checkIsDefined)(elseCase) ? { else: elseCase } : undefined),
},
bindings: {
when: (0, bindings_1.createSingleBinding)({ code: (0, generator_1.default)(node.test, { compact: true }).code }),
},
children: child === null ? [] : [child],
});
}
else if (types.isJSXFragment(node)) {
return (0, create_mitosis_node_1.createMitosisNode)({
name: 'Fragment',
children: node.children
.map((child) => (0, exports.jsxElementToJson)(child, options))
.filter(nullable_1.checkIsDefined),
});
}
else if (types.isJSXSpreadChild(node)) {
// TODO: support spread attributes
return null;
}
else if (types.isNullLiteral(node) || types.isBooleanLiteral(node)) {
return null;
}
else if (types.isNumericLiteral(node)) {
return (0, create_mitosis_node_1.createMitosisNode)({
properties: {
_text: String(node.value),
},
});
}
else if (types.isStringLiteral(node)) {
return (0, create_mitosis_node_1.createMitosisNode)({
properties: {
_text: node.value,
},
});
}
if (!types.isJSXElement(node)) {
return (0, create_mitosis_node_1.createMitosisNode)({
bindings: {
_text: (0, bindings_1.createSingleBinding)({ code: (0, generator_1.default)(node, { compact: true }).code }),
},
});
}
const nodeName = (0, generator_1.default)(node.openingElement.name).code;
if (nodeName === 'Show') {
const whenAttr = node.openingElement.attributes.find((item) => types.isJSXAttribute(item) && item.name.name === 'when');
const elseAttr = node.openingElement.attributes.find((item) => types.isJSXAttribute(item) && item.name.name === 'else');
const whenValue = whenAttr && types.isJSXExpressionContainer(whenAttr.value)
? (0, generator_1.default)(whenAttr.value.expression, { compact: true }).code
: undefined;
const elseValue = elseAttr &&
types.isJSXExpressionContainer(elseAttr.value) &&
(0, exports.jsxElementToJson)(elseAttr.value.expression, options);
return (0, create_mitosis_node_1.createMitosisNode)({
name: 'Show',
meta: {
else: elseValue || undefined,
},
bindings: {
...(whenValue ? { when: (0, bindings_1.createSingleBinding)({ code: whenValue }) } : {}),
},
children: node.children
.map((child) => (0, exports.jsxElementToJson)(child, options))
.filter(nullable_1.checkIsDefined),
});
}
// <For ...> control flow component
if (nodeName === 'For') {
const child = node.children.find((item) => types.isJSXExpressionContainer(item));
if ((0, nullable_1.checkIsDefined)(child)) {
const childExpression = child.expression;
if (types.isArrowFunctionExpression(childExpression)) {
const forArguments = getForArguments(childExpression === null || childExpression === void 0 ? void 0 : childExpression.params);
const forCode = (0, function_1.pipe)(node.openingElement.attributes[0], (attr) => {
if (types.isJSXAttribute(attr) && types.isJSXExpressionContainer(attr.value)) {
return (0, generator_1.default)(attr.value.expression, { compact: true }).code;
}
else {
// TO-DO: is an empty string valid here?
return '';
}
});
return (0, create_mitosis_node_1.createMitosisNode)({
name: 'For',
bindings: {
each: (0, bindings_1.createSingleBinding)({
code: forCode,
}),
},
scope: forArguments,
children: [(0, exports.jsxElementToJson)(childExpression.body, options)],
});
}
}
}
// const properties: MitosisNode['properties'] = {}
// const bindings: MitosisNode['bindings'] = {}
// const slots: MitosisNode['slots'] = {}
const { bindings, properties, slots, blocksSlots } = node.openingElement.attributes.reduce((memo, item) => {
if (types.isJSXAttribute(item)) {
const key = (0, helpers_1.transformAttributeName)(item.name.name);
const value = item.value;
// <Foo myProp />
if (value === null) {
memo.bindings[key] = (0, bindings_1.createSingleBinding)({ code: 'true' });
return memo;
}
// <Foo myProp="hello" />
if (types.isStringLiteral(value)) {
memo.properties[key] = value.value;
return memo;
}
if (!types.isJSXExpressionContainer(value))
return memo;
const { expression } = value;
if (types.isStringLiteral(expression)) {
// <Foo myProp={"hello"} />
memo.properties[key] = expression.value;
}
else if (key.startsWith('on') && types.isArrowFunctionExpression(expression)) {
// <Foo myProp={() => {}} />
const args = expression.params.map((node) => node === null || node === void 0 ? void 0 : node.name);
memo.bindings[key] = (0, bindings_1.createSingleBinding)({
code: (0, generator_1.default)(expression.body, { compact: true }).code,
async: expression.async === true ? true : undefined,
arguments: args.length ? args : undefined,
bindingType: 'function',
});
}
else if (/^on[A-Z]/.test(key) && types.isExpression(expression)) {
// regex ignores props that happen to start with "on" but are not handlers
// <Foo onClick={state.handler} />
const call = types.callExpression(expression, []);
memo.bindings[key] = (0, bindings_1.createSingleBinding)({
code: (0, generator_1.default)(call, { compact: true }).code,
bindingType: 'function',
});
}
else if (types.isJSXElement(expression) || types.isJSXFragment(expression)) {
// <Foo myProp={<MoreMitosisNode><div /></MoreMitosisNode>} />
// <Foo myProp={<><Node /><Node /></>} />
const slotNode = (0, exports.jsxElementToJson)(expression, options);
if (!slotNode)
return memo;
memo.slots[key] = [slotNode];
// Temporarily keep the slot as a binding until we migrate generators to use the slots.
memo.bindings[key] = (0, bindings_1.createSingleBinding)({
code: (0, generator_1.default)(expression, { compact: true }).code,
});
}
else {
if (options.enableBlocksSlots &&
(types.isArrayExpression(expression) || types.isObjectExpression(expression))) {
/**
* Find any deeply nested JSX Elements, convert them to Mitosis nodes
* then store them in "replacements" to later do a string substitution
* to swap out the stringified JSX with stringified Mitosis nodes.
*
* Object expressions need to wrapped in an expression statement (e.g. `({... })`)
* otherwise Babel generate will fail.
*/
const code = types.isObjectExpression(expression)
? (0, generator_1.default)(types.expressionStatement(expression)).code
: (0, generator_1.default)(expression).code;
const replacements = [];
(0, helpers_1.babelDefaultTransform)(code, {
JSXElement(path) {
const { start, end } = path.node;
if (start == null || end == null) {
return;
}
const node = (0, exports.jsxElementToJson)(path.node, options);
if (!node)
return;
/**
* Perform replacements in the reverse order in which we saw them
* otherwise start/end indices will quickly become incorrect.
*/
replacements.unshift({
start,
end,
node,
});
/**
* babelTransform will keep iterating into deeper nodes. However,
* the "jsxElementToJson" call above will handle deeper nodes.
* Replace the path will null so we do not accidentally process
* child nodes multiple times.
*/
path.replaceWith(types.nullLiteral());
},
});
// If no replacements then this is just a regular binding
if (replacements.length > 0) {
// Replace stringified JSX (e.g. <Foo></Foo>) with stringified Mitosis JSON
let replacedCode = code;
replacements.forEach(({ start, end, node }) => {
replacedCode =
replacedCode.substring(0, start) +
JSON.stringify(node) +
replacedCode.substring(end);
});
let finalCode = replacedCode;
if (types.isObjectExpression(expression)) {
/**
* Remove the ( and ); surrounding the expression because we just want
* a valid JS object instead.
*/
const match = replacedCode.match(/\(([\s\S]*?)\);/);
if (match) {
finalCode = match[1];
}
}
/**
* The result should be a valid array of objects. Use json5 to parse
* as not every key will be wrapped in quotes, so a normal JSON.parse
* will fail.
*/
memo.blocksSlots[key] = json5_1.default.parse(finalCode);
return memo;
}
}
memo.bindings[key] = (0, bindings_1.createSingleBinding)({
code: (0, generator_1.default)(expression, { compact: true }).code,
});
}
return memo;
}
else if (types.isJSXSpreadAttribute(item)) {
// TODO: potentially like Vue store bindings and properties as array of key value pairs
// too so can do this accurately when order matters. Also tempting to not support spread,
// as some frameworks do not support it (e.g. Angular) tho Angular may be the only one
const { code: key } = (0, generator_1.default)(item.argument, { compact: true });
memo.bindings[key] = {
code: types.stringLiteral((0, generator_1.default)(item.argument, { compact: true }).code).value,
type: 'spread',
spreadType: 'normal',
};
}
return memo;
}, {
bindings: {},
properties: {},
slots: {},
blocksSlots: {},
});
return (0, create_mitosis_node_1.createMitosisNode)({
name: nodeName,
properties,
bindings,
children: node.children.map((child) => (0, exports.jsxElementToJson)(child, options)).filter(nullable_1.checkIsDefined),
slots: Object.keys(slots).length > 0 ? slots : undefined,
blocksSlots: Object.keys(blocksSlots).length > 0 ? blocksSlots : undefined,
});
};
exports.jsxElementToJson = jsxElementToJson;