UNPKG

@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
"use strict"; 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('"', '&quot;'); } }, 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;