@builder.io/mitosis
Version:
Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io
548 lines (547 loc) • 28.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getCodeProcessorPlugins = void 0;
const get_computed_1 = require("../../../../generators/angular/signals/helpers/get-computed");
const babel_transform_1 = require("../../../../helpers/babel-transform");
const class_components_1 = require("../../../../helpers/class-components");
const event_handlers_1 = require("../../../../helpers/event-handlers");
const process_code_1 = require("../../../../helpers/plugins/process-code");
const symbol_processor_1 = require("../../../../symbols/symbol-processor");
const mitosis_node_1 = require("../../../../types/mitosis-node");
const types_1 = require("@babel/types");
// Helper functions for handling nested state updates
const getBaseObject = (node) => {
if (!node)
return null;
if (!(0, types_1.isMemberExpression)(node))
return node;
return getBaseObject(node.object);
};
const getPropertyFromStateChain = (node) => {
// Start at the leftmost object and traverse up to find the first property after 'state'
let current = node;
while ((0, types_1.isMemberExpression)(current)) {
if ((0, types_1.isMemberExpression)(current.object) &&
(0, types_1.isIdentifier)(current.object.object) &&
current.object.object.name === 'state' &&
(0, types_1.isIdentifier)(current.object.property)) {
return current.object.property.name;
}
current = current.object;
}
return null;
};
const getNestedPath = (node, topLevelProp) => {
const path = [];
let current = node;
// Collect all property names starting after the top-level property
let foundTopLevel = false;
while ((0, types_1.isMemberExpression)(current)) {
if ((0, types_1.isIdentifier)(current.property)) {
path.unshift(current.property.name);
}
if ((0, types_1.isMemberExpression)(current.object) &&
(0, types_1.isIdentifier)(current.object.object) &&
current.object.object.name === 'state' &&
(0, types_1.isIdentifier)(current.object.property) &&
current.object.property.name === topLevelProp) {
foundTopLevel = true;
break;
}
current = current.object;
}
return foundTopLevel ? path : [];
};
const buildPathAccess = (baseParam, propertyPath) => {
return propertyPath.reduce((acc, prop) => {
return (0, types_1.memberExpression)(acc, (0, types_1.identifier)(prop));
}, baseParam);
};
const isStateOrPropsExpression = (path) => {
return ((0, types_1.isMemberExpression)(path.node) &&
(0, types_1.isIdentifier)(path.node.object) &&
(0, types_1.isIdentifier)(path.node.property) &&
(path.node.object.name === 'props' || path.node.object.name === 'state'));
};
const isAFunctionOrMethod = (json, path) => {
var _a, _b, _c;
return (json &&
(0, types_1.isIdentifier)(path.node.object) &&
(0, types_1.isIdentifier)(path.node.property) &&
path.node.object.name === 'state' &&
json.state &&
typeof path.node.property.name === 'string' &&
json.state[path.node.property.name] &&
((_a = json.state[path.node.property.name]) === null || _a === void 0 ? void 0 : _a.type) &&
(((_b = json.state[path.node.property.name]) === null || _b === void 0 ? void 0 : _b.type) === 'method' ||
((_c = json.state[path.node.property.name]) === null || _c === void 0 ? void 0 : _c.type) === 'function'));
};
const handleAssignmentExpression = (path) => {
if ((0, types_1.isMemberExpression)(path.node.left) &&
(0, types_1.isIdentifier)(path.node.left.object) &&
(0, types_1.isIdentifier)(path.node.left.property) &&
path.node.left.object.name === 'state') {
const root = (0, types_1.memberExpression)(path.node.left, (0, types_1.identifier)('set'));
root.extra = { ...root.extra, updateExpression: true };
const call = (0, types_1.callExpression)(root, [path.node.right]);
path.replaceWith(call);
}
else if ((0, types_1.isMemberExpression)(path.node.left) &&
path.node.left.computed &&
(((0, types_1.isMemberExpression)(path.node.left.object) &&
(0, types_1.isIdentifier)(path.node.left.object.object) &&
path.node.left.object.object.name === 'state') ||
((0, types_1.isIdentifier)(path.node.left.object) && path.node.left.object.name === 'state'))) {
/**
* Handle array element assignments like: state.arr[0] = '123'
* Example:
* Input: state.arr[0] = '123'
* Output: state.arr.update(arr => {
* arr[0] = '123';
* return arr;
* })
*/
let stateProp;
let baseObject;
if ((0, types_1.isIdentifier)(path.node.left.object) && path.node.left.object.name === 'state') {
stateProp = path.node.left.property;
baseObject = path.node.left.object;
}
else {
stateProp = getPropertyFromStateChain(path.node.left);
if (!stateProp)
return;
baseObject = (0, types_1.memberExpression)((0, types_1.identifier)('state'), (0, types_1.identifier)(stateProp));
}
const root = (0, types_1.memberExpression)(baseObject, (0, types_1.identifier)('update'));
root.extra = { ...root.extra, updateExpression: true };
const paramName = typeof stateProp === 'string' ? stateProp : (0, types_1.isIdentifier)(stateProp) ? stateProp.name : 'item';
const param = (0, types_1.identifier)(paramName);
let assignTarget;
if ((0, types_1.isIdentifier)(path.node.left.object) && path.node.left.object.name === 'state') {
// Direct state array: state[index] = value
assignTarget = (0, types_1.memberExpression)(param, path.node.left.property, true);
}
else {
// Property array: state.arr[index] = value
assignTarget = (0, types_1.memberExpression)(param, path.node.left.property, true);
}
const block = (0, types_1.blockStatement)([
(0, types_1.expressionStatement)((0, types_1.assignmentExpression)('=', assignTarget, path.node.right)),
(0, types_1.returnStatement)(param),
]);
const arrowFunction = (0, types_1.arrowFunctionExpression)([param], block);
const call = (0, types_1.callExpression)(root, [arrowFunction]);
path.replaceWith(call);
}
else if ((0, types_1.isMemberExpression)(path.node.left) &&
(0, types_1.isMemberExpression)(path.node.left.object) &&
(0, types_1.isIdentifier)(getBaseObject(path.node.left)) &&
getBaseObject(path.node.left).name === 'state') {
/**
* Handle any level of nested updates like state.store.something.nested = newVal
* Example:
* Input: state.store.something.nested = newVal
* Output: state.store.update(obj => ({
* ...obj,
* store: {
* ...obj.store,
* something: {
* ...obj.store.something,
* nested: newVal
* }
* }
* }))
*/
const stateProp = getPropertyFromStateChain(path.node.left);
if (!stateProp)
return;
const topLevelProp = (0, types_1.memberExpression)((0, types_1.identifier)('state'), (0, types_1.identifier)(stateProp));
const nestedPaths = getNestedPath(path.node.left, stateProp);
const root = (0, types_1.memberExpression)(topLevelProp, (0, types_1.identifier)('update'));
root.extra = { ...root.extra, updateExpression: true };
const paramName = stateProp;
const param = (0, types_1.identifier)(paramName);
let innerValue = path.node.right;
for (let i = nestedPaths.length - 1; i >= 0; i--) {
const spreadTarget = i === 0 ? param : buildPathAccess(param, nestedPaths.slice(0, i));
innerValue = (0, types_1.objectExpression)([
(0, types_1.spreadElement)(spreadTarget),
(0, types_1.objectProperty)((0, types_1.identifier)(nestedPaths[i]), innerValue, false, false),
]);
}
const arrowFunction = (0, types_1.arrowFunctionExpression)([param], innerValue, false);
const call = (0, types_1.callExpression)(root, [arrowFunction]);
path.replaceWith(call);
}
};
const handleMemberExpression = (path, json) => {
var _a, _b, _c, _d;
if (((_a = path.node.extra) === null || _a === void 0 ? void 0 : _a.makeCallExpressionDone) || ((_c = (_b = path.parentPath) === null || _b === void 0 ? void 0 : _b.node.extra) === null || _c === void 0 ? void 0 : _c.updateExpression)) {
// Don't add a function if we've done it already
return;
}
if ((0, types_1.isCallExpression)(path.parent) &&
(0, types_1.isMemberExpression)(path.parent.callee) &&
(0, types_1.isIdentifier)(path.parent.callee.object) &&
(path.parent.callee.object.name === 'props' || path.parent.callee.object.name === 'state') &&
!((_d = path.parent.callee.extra) === null || _d === void 0 ? void 0 : _d.updateExpression)) {
// Don't add a function if it is already
return;
}
if (isStateOrPropsExpression(path)) {
// Check if the state property is a method or function type, and if so, bind it to 'this'
if (isAFunctionOrMethod(json, path)) {
const bindExpr = `${path.toString()}.bind(this)`;
path.replaceWith((0, types_1.identifier)(bindExpr));
return;
}
path.node.extra = { ...path.node.extra, makeCallExpressionDone: true };
path.replaceWith((0, types_1.callExpression)(path.node, []));
}
};
const handleHookAndStateOnEvents = (path, isHookDepArray) => {
if ((0, types_1.isIdentifier)(path.node.property) && (0, event_handlers_1.checkIsEvent)(path.node.property.name)) {
if ((0, types_1.isIfStatement)(path.parent)) {
// We don't do anything if the event is in an IfStatement
path.node.extra = { ...path.node.extra, updateExpression: true };
return true;
}
else if ((0, types_1.isCallExpression)(path.parent) &&
(0, types_1.isIdentifier)(path.node.object) &&
(0, types_1.isMemberExpression)(path.parent.callee)) {
// We add "emit" to events
const root = (0, types_1.memberExpression)(path.node, (0, types_1.identifier)('emit'));
root.extra = { ...root.extra, updateExpression: true };
path.replaceWith(root);
}
else if (isHookDepArray && (0, types_1.isIdentifier)(path.node.object)) {
const iden = (0, types_1.identifier)(`// "${path.node.object.name}.${path.node.property.name}" is an event skip it.`);
path.replaceWith(iden);
return true;
}
}
return false;
};
const handleTemplateLiteral = (path, json, context) => {
var _a;
const fnName = `templateStr_${(0, symbol_processor_1.hashCodeAsString)(path.toString())}`;
const extraParams = new Set();
// Collect loop variables from context
let currentContext = context;
while (currentContext === null || currentContext === void 0 ? void 0 : currentContext.parent) {
if (((_a = currentContext.parent.node) === null || _a === void 0 ? void 0 : _a.name) === mitosis_node_1.ForNodeName) {
const forNode = currentContext.parent.node;
if (forNode.scope.forName)
extraParams.add(forNode.scope.forName);
if (forNode.scope.indexName)
extraParams.add(forNode.scope.indexName);
}
currentContext = currentContext.parent;
}
const processedExpressions = path.node.expressions.map((expr) => {
let exprCode = '';
try {
const { code } = require('@babel/generator').default(expr);
exprCode = code;
}
catch (e) {
exprCode = expr.toString();
}
// Replace state.x with this.x() for signals
return exprCode
.replace(/\bstate\.(\w+)(?!\()/g, 'this.$1()')
.replace(/\bprops\.(\w+)(?!\()/g, 'this.$1()');
});
// Convert Set to Array for final usage
const paramsList = Array.from(extraParams);
json.state[fnName] = {
code: `${fnName}(${paramsList.join(', ')}) {
return \`${path.node.quasis
.map((quasi, i) => {
const escapedRaw = quasi.value.raw.replace(/\\/g, '\\\\').replace(/\$/g, '\\$');
return (escapedRaw +
(i < processedExpressions.length ? '${' + processedExpressions[i] + '}' : ''));
})
.join('')}\`;
}`,
type: 'method',
};
// Return the function call with any needed parameters
return `${fnName}(${paramsList.join(', ')})`;
};
const handleCallExpressionArgument = (json, arg) => {
var _a;
if ((0, types_1.isMemberExpression)(arg) &&
(0, types_1.isIdentifier)(arg.object) &&
(0, types_1.isIdentifier)(arg.property) &&
(arg.object.name === 'state' || arg.object.name === 'props') &&
!((_a = arg.extra) === null || _a === void 0 ? void 0 : _a.makeCallExpressionDone)) {
if (arg.object.name === 'state' && json) {
const argPath = { node: arg };
if (isAFunctionOrMethod(json, argPath)) {
const argStr = arg.object.name + '.' + arg.property.name;
return (0, types_1.identifier)(`${argStr}.bind(this)`);
}
}
const newArg = (0, types_1.callExpression)(arg, []);
newArg.extra = { makeCallExpressionDone: true };
return newArg;
}
return arg;
};
const transformHooksAndState = (code, isHookDepArray, json) => {
return (0, babel_transform_1.babelTransformExpression)(code, {
AssignmentExpression(path) {
handleAssignmentExpression(path);
},
UpdateExpression(path) {
/*
* If we have a function like this:
* `state._counter++;`
*
* We need to convert it and use the "update" example from https://angular.dev/guide/signals#writable-signals:
* `state._counter.update(_counter=>_counter++)`
*
*/
if ((0, types_1.isMemberExpression)(path.node.argument) &&
(0, types_1.isIdentifier)(path.node.argument.object) &&
path.node.argument.object.name === 'state' &&
(0, types_1.isIdentifier)(path.node.argument.property)) {
const root = (0, types_1.memberExpression)(path.node.argument, (0, types_1.identifier)('update'));
root.extra = { ...root.extra, updateExpression: true };
const argument = path.node.argument.property;
const block = (0, types_1.blockStatement)([
(0, types_1.expressionStatement)((0, types_1.updateExpression)(path.node.operator, argument)),
(0, types_1.returnStatement)(argument),
]);
const arrowFunction = (0, types_1.arrowFunctionExpression)([argument], block);
const call = (0, types_1.callExpression)(root, [arrowFunction]);
path.replaceWith(call);
}
else if ((0, types_1.isMemberExpression)(path.node.argument) &&
(0, types_1.isMemberExpression)(path.node.argument.object) &&
(0, types_1.isIdentifier)(getBaseObject(path.node.argument)) &&
getBaseObject(path.node.argument).name === 'state') {
// Handle nested update expressions like: state.obj.counter++
// Example:
// Input: state.obj.counter++
// Output: state.obj.update(obj => {
// Object.assign(obj, {
// counter: obj.counter + 1
// });
// return obj;
// });
//
const stateProp = getPropertyFromStateChain(path.node.argument);
if (!stateProp)
return;
const topLevelProp = (0, types_1.memberExpression)((0, types_1.identifier)('state'), (0, types_1.identifier)(stateProp));
const nestedPaths = getNestedPath(path.node.argument, stateProp);
const root = (0, types_1.memberExpression)(topLevelProp, (0, types_1.identifier)('update'));
root.extra = { ...root.extra, updateExpression: true };
const paramName = stateProp;
const param = (0, types_1.identifier)(paramName);
const lastPropName = nestedPaths[nestedPaths.length - 1];
const innerParamName = lastPropName + '_value';
const nestedPathAccess = buildPathAccess(param, nestedPaths.slice(0, -1));
let innerValue = (0, types_1.objectExpression)([
(0, types_1.spreadElement)(nestedPathAccess),
(0, types_1.objectProperty)((0, types_1.identifier)(lastPropName), (0, types_1.updateExpression)(path.node.operator, (0, types_1.identifier)(innerParamName), path.node.prefix), false, false),
]);
for (let i = nestedPaths.length - 2; i >= 0; i--) {
const spreadTarget = i === 0 ? param : buildPathAccess(param, nestedPaths.slice(0, i));
innerValue = (0, types_1.objectExpression)([
(0, types_1.spreadElement)(spreadTarget),
(0, types_1.objectProperty)((0, types_1.identifier)(nestedPaths[i]), innerValue, false, false),
]);
}
const block = (0, types_1.blockStatement)([
(0, types_1.expressionStatement)((0, types_1.callExpression)((0, types_1.memberExpression)((0, types_1.identifier)('Object'), (0, types_1.identifier)('assign')), [
param,
innerValue,
])),
(0, types_1.returnStatement)(param),
]);
const arrowFunction = (0, types_1.arrowFunctionExpression)([param], block);
const call = (0, types_1.callExpression)(root, [arrowFunction]);
path.replaceWith(call);
}
},
MemberExpression(path) {
const skip = handleHookAndStateOnEvents(path, isHookDepArray);
if (skip) {
return;
}
handleMemberExpression(path, json);
},
CallExpression(path) {
// if args has a state.x or props.x, we need to add this.x() to the args
if (path.node.arguments.length > 0) {
const newArgs = path.node.arguments.map((arg) => handleCallExpressionArgument(json, arg));
// Only replace arguments if we made any changes
if (newArgs.some((arg, i) => arg !== path.node.arguments[i])) {
path.node.arguments = newArgs;
}
}
},
});
};
const addToImportCall = (json, importName) => {
const importInstance = json.imports.find((imp) => imp.imports[importName]);
// Check if this is a type import - if it is, don't add it to importCalls
if ((importInstance === null || importInstance === void 0 ? void 0 : importInstance.importKind) === 'type') {
return;
}
const isImportCall = !!importInstance;
const isExportCall = json.exports ? !!json.exports[importName] : false;
if (isImportCall || isExportCall) {
json.compileContext.angular.extra.importCalls.push(importName);
}
};
const transformBindings = (json, code, key, context) => {
return (0, babel_transform_1.babelTransformExpression)(code, {
BlockStatement() {
console.error(`
Component ${json.name} has a BlockStatement inside JSX'.
This will cause an error in Angular.
Please create and call a new function instead with this code:
${code}`);
},
CallExpression(path) {
// If we call a function from an import we need to add it to the Component as well
if ((0, types_1.isIdentifier)(path.node.callee)) {
addToImportCall(json, path.node.callee.name);
}
if (path.node.arguments.length > 0) {
const newArgs = path.node.arguments.map((arg) => handleCallExpressionArgument(json, arg));
// Only replace arguments if we made any changes
if (newArgs.some((arg, i) => arg !== path.node.arguments[i])) {
path.node.arguments = newArgs;
}
}
},
Identifier(path) {
// If we use a constant from an import we need to add it to the Component as well
if ((0, types_1.isIdentifier)(path.node)) {
addToImportCall(json, path.node.name);
}
},
StringLiteral(path) {
var _a;
// We cannot use " for string literal in template
if ((_a = path.node.extra) === null || _a === void 0 ? void 0 : _a.raw) {
path.node.extra.raw = path.node.extra.raw.replaceAll('"', '"');
}
},
AssignmentExpression(path) {
handleAssignmentExpression(path);
},
MemberExpression(path) {
handleMemberExpression(path, json);
},
TemplateLiteral(path) {
var _a, _b, _c;
// they are already created as trackBy functions
if (key === 'key') {
return;
}
// skip template literals in spread bindings
if (key && ((_c = (_b = (_a = context === null || context === void 0 ? void 0 : context.node) === null || _a === void 0 ? void 0 : _a.bindings) === null || _b === void 0 ? void 0 : _b[key]) === null || _c === void 0 ? void 0 : _c.code.includes('...'))) {
return;
}
// When we encounter a template literal, convert it to a function
const fnCall = handleTemplateLiteral(path, json, context);
path.replaceWith((0, types_1.identifier)(fnCall));
},
});
};
const getCodeProcessorPlugins = (json, options, processBindingOptions) => {
return [
...(options.plugins || []),
(0, process_code_1.CODE_PROCESSOR_PLUGIN)((codeType) => {
switch (codeType) {
case 'bindings':
return (code, key, context) => {
var _a, _b, _c, _d, _e, _f;
// Handle object spread expressions or TypeScript type assertions
if ((code.startsWith('{') && code.includes('...')) || code.includes(' as ')) {
if (((_a = context === null || context === void 0 ? void 0 : context.node) === null || _a === void 0 ? void 0 : _a.bindings) && key) {
const binding = context.node.bindings[key];
let isForContext = false;
let forName = '';
let indexName = '';
let currentContext = context;
while (currentContext === null || currentContext === void 0 ? void 0 : currentContext.parent) {
if (((_b = currentContext.parent.node) === null || _b === void 0 ? void 0 : _b.name) === mitosis_node_1.ForNodeName) {
isForContext = true;
const forNode = currentContext.parent.node;
forName = forNode.scope.forName || '_';
indexName = forNode.scope.indexName || '_';
break;
}
currentContext = currentContext.parent;
}
const computedName = (0, get_computed_1.createObjectSpreadComputed)(json, binding, key, isForContext, forName, indexName);
if (isForContext) {
const params = [];
if (forName)
params.push(forName);
if (indexName)
params.push(indexName);
if (params.length > 0) {
return (0, class_components_1.processClassComponentBinding)(json, `${computedName}(${params.join(', ')})`, {
...processBindingOptions,
replaceWith: 'this.',
});
}
}
return (0, class_components_1.processClassComponentBinding)(json, `${computedName}()`, {
...processBindingOptions,
replaceWith: 'this.',
});
}
}
const needsToReplaceWithThis = (code.startsWith('{') && code.includes('...')) ||
code.includes(' as ') ||
(context === null || context === void 0 ? void 0 : context.node.name.includes('.')) ||
((_c = context === null || context === void 0 ? void 0 : context.node.bindings[key]) === null || _c === void 0 ? void 0 : _c.type) === 'spread';
let replaceWith = '';
if (key === 'key') {
/**
* If we have a key attribute we need to check if it is inside a @for loop.
* We will create a new function for the key attribute.
* Therefore, we need to add "this" to state and props.
*/
const isForParent = ((_f = (_e = (_d = context === null || context === void 0 ? void 0 : context.parent) === null || _d === void 0 ? void 0 : _d.parent) === null || _e === void 0 ? void 0 : _e.node) === null || _f === void 0 ? void 0 : _f.name) === mitosis_node_1.ForNodeName;
if (isForParent) {
replaceWith = 'this.';
}
}
if (needsToReplaceWithThis) {
replaceWith = 'this.';
}
return (0, class_components_1.processClassComponentBinding)(json, transformBindings(json, code, key, context), {
...processBindingOptions,
replaceWith,
});
};
case 'hooks-deps-array':
case 'hooks':
case 'state':
return (code) => {
return (0, class_components_1.processClassComponentBinding)(json, transformHooksAndState(code, codeType === 'hooks-deps-array', json), processBindingOptions);
};
case 'properties':
case 'hooks-deps':
case 'context-set':
case 'dynamic-jsx-elements':
case 'types':
return (code) => {
return (0, class_components_1.processClassComponentBinding)(json, code, processBindingOptions);
};
}
}),
];
};
exports.getCodeProcessorPlugins = getCodeProcessorPlugins;