UNPKG

@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
"use strict"; 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;