@builder.io/mitosis
Version:
Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io
228 lines (227 loc) • 10.7 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.parseJsx = void 0;
const hooks_1 = require("../../constants/hooks");
const create_mitosis_component_1 = require("../../helpers/create-mitosis-component");
const filter_empty_text_nodes_1 = require("../../helpers/filter-empty-text-nodes");
const json_1 = require("../../helpers/json");
const replace_new_lines_in_strings_1 = require("../../helpers/replace-new-lines-in-strings");
const signals_1 = require("../../helpers/signals");
const traverse_nodes_1 = require("../../helpers/traverse-nodes");
const babel = __importStar(require("@babel/core"));
const generator_1 = __importDefault(require("@babel/generator"));
const preset_typescript_1 = __importDefault(require("@babel/preset-typescript"));
const function_1 = require("fp-ts/lib/function");
const ast_1 = require("./ast");
const component_types_1 = require("./component-types");
const context_1 = require("./context");
const element_parser_1 = require("./element-parser");
const exports_1 = require("./exports");
const function_parser_1 = require("./function-parser");
const helpers_1 = require("./helpers");
const hooks_2 = require("./hooks");
const use_target_1 = require("./hooks/use-target");
const imports_1 = require("./imports");
const props_1 = require("./props");
const props_types_1 = require("./props-types");
const signals_2 = require("./signals");
const state_1 = require("./state");
const { types } = babel;
const typescriptBabelPreset = [preset_typescript_1.default, { isTSX: true, allExtensions: true }];
const beforeParse = (path) => {
path.traverse({
FunctionDeclaration(path) {
(0, props_1.undoPropsDestructure)(path);
},
});
};
/**
* This function takes the raw string from a Mitosis component, and converts it into a JSON that can be processed by
* each generator function.
*
* @param jsx string representation of the Mitosis component
* @returns A JSON representation of the Mitosis component
*/
function parseJsx(jsx, _options = {}) {
let subComponentFunctions = [];
const options = {
typescript: false,
..._options,
};
const stateToScope = [];
const jsxToUse = (0, helpers_1.babelStripTypes)(jsx, !options.typescript);
const output = (0, helpers_1.babelDefaultTransform)(jsxToUse, {
JSXExpressionContainer(path, context) {
if (types.isJSXEmptyExpression(path.node.expression)) {
path.remove();
}
},
Program(path, context) {
if (context.builder) {
return;
}
beforeParse(path);
context.builder = {
component: (0, create_mitosis_component_1.createMitosisComponent)(),
};
const keepStatements = path.node.body.filter((statement) => (0, helpers_1.isImportOrDefaultExport)(statement) || (0, component_types_1.isTypeOrInterface)(statement));
context.builder.component.exports = (0, exports_1.generateExports)(path);
subComponentFunctions = path.node.body
.filter((node) => !types.isExportDefaultDeclaration(node) && types.isFunctionDeclaration(node))
.map((node) => `export default ${(0, generator_1.default)(node).code}`);
const preComponentCode = (0, function_1.pipe)(path, (0, hooks_2.collectModuleScopeHooks)(context, options), types.program, generator_1.default, (generatorResult) => generatorResult.code);
// TODO: support multiple? e.g. for others to add imports?
context.builder.component.hooks.preComponent = { code: preComponentCode };
path.replaceWith(types.program(keepStatements));
},
FunctionDeclaration(path, context) {
const { node } = path;
if (types.isIdentifier(node.id)) {
const name = node.id.name;
if (name[0].toUpperCase() === name[0]) {
path.traverse({
/**
* Plugin to find all `useTarget()` assignment calls inside of the component function body
* and replace them with a magic string.
*/
CallExpression(path) {
if (!types.isCallExpression(path.node))
return;
if (!types.isIdentifier(path.node.callee))
return;
if (path.node.callee.name !== hooks_1.HOOKS.TARGET)
return;
const targetBlock = (0, use_target_1.getUseTargetStatements)(path);
if (!targetBlock)
return;
const blockId = (0, use_target_1.getTargetId)(context.builder.component);
// replace the useTarget() call with a magic string
path.replaceWith(types.stringLiteral((0, use_target_1.getMagicString)(blockId)));
// store the target block in the component
context.builder.component.targetBlocks = {
...context.builder.component.targetBlocks,
[blockId]: targetBlock,
};
},
});
path.replaceWith((0, ast_1.jsonToAst)((0, function_parser_1.componentFunctionToJson)(node, context, stateToScope)));
}
}
},
ImportDeclaration(path, context) {
(0, imports_1.handleImportDeclaration)({ options, path, context });
},
ExportDefaultDeclaration(path) {
path.replaceWith(path.node.declaration);
},
JSXElement(path) {
const { node } = path;
path.replaceWith((0, ast_1.jsonToAst)((0, element_parser_1.jsxElementToJson)(node)));
},
ExportNamedDeclaration(path, context) {
const { node } = path;
if (babel.types.isTSInterfaceDeclaration(node.declaration) ||
babel.types.isTSTypeAliasDeclaration(node.declaration)) {
(0, component_types_1.collectTypes)(path, context);
}
},
TSTypeAliasDeclaration(path, context) {
(0, component_types_1.collectTypes)(path, context);
},
TSInterfaceDeclaration(path, context) {
(0, component_types_1.collectTypes)(path, context);
},
});
if (!output || !output.code) {
throw new Error('Could not parse JSX');
}
const stringifiedMitosisComponent = (0, replace_new_lines_in_strings_1.stripNewlinesInStrings)(output.code
.trim()
// Occasional issues where comments get kicked to the top. Full fix should strip these sooner
.replace(/^\/\*[\s\S]*?\*\/\s*/, '')
// Weird bug with adding a newline in a normal at end of a normal string that can't have one
// If not one-off find full solve and cause
.replace(/\n"/g, '"')
.replace(/^\({/, '{')
.replace(/}\);$/, '}'));
const mitosisComponent = (0, json_1.tryParseJson)(stringifiedMitosisComponent);
(0, state_1.mapStateIdentifiers)(mitosisComponent, stateToScope);
(0, context_1.extractContextComponents)(mitosisComponent);
mitosisComponent.subComponents = subComponentFunctions.map((item) => parseJsx(item, options));
const signalTypeImportName = (0, signals_1.getSignalImportName)(jsxToUse);
if (signalTypeImportName) {
mitosisComponent.signals = { signalTypeImportName };
}
if (options.tsProject && options.filePath) {
// identify optional props.
const optionalProps = (0, props_types_1.findOptionalProps)({
project: options.tsProject.project,
filePath: options.filePath,
});
optionalProps.forEach((prop) => {
var _a;
mitosisComponent.props = {
...mitosisComponent.props,
[prop]: {
...(_a = mitosisComponent.props) === null || _a === void 0 ? void 0 : _a[prop],
optional: true,
},
};
});
const reactiveValues = (0, signals_2.findSignals)({
filePath: options.filePath,
project: options.tsProject.project,
});
reactiveValues.props.forEach((prop) => {
var _a;
mitosisComponent.props = {
...mitosisComponent.props,
[prop]: {
...(_a = mitosisComponent.props) === null || _a === void 0 ? void 0 : _a[prop],
propertyType: 'reactive',
},
};
});
reactiveValues.state.forEach((state) => {
if (!mitosisComponent.state[state])
return;
mitosisComponent.state[state].propertyType = 'reactive';
});
reactiveValues.context.forEach((context) => {
if (!mitosisComponent.context.get[context])
return;
mitosisComponent.context.get[context].type = 'reactive';
});
}
(0, traverse_nodes_1.traverseNodes)(mitosisComponent, (node) => {
node.children = node.children.filter(filter_empty_text_nodes_1.filterEmptyTextNodes);
});
return mitosisComponent;
}
exports.parseJsx = parseJsx;
;