@builder.io/mitosis
Version:
Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io
413 lines (412 loc) • 16.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertJsFunctionToSwift = exports.extractFunctionSignature = exports.getBindingType = exports.camelToSnakeCase = exports.getForEachParams = exports.isForEachBlock = exports.needsScrollView = exports.getEventHandlerName = exports.getStatePropertyTypeAnnotation = exports.cssToSwiftUIModifiers = exports.jsxElementToSwiftUIView = exports.getSwiftType = exports.stripStateAndProps = exports.ensureSwiftStringFormat = exports.convertConsoleLogToPrint = void 0;
const capitalize_1 = require("../../helpers/capitalize");
const strip_state_and_props_refs_1 = require("../../helpers/strip-state-and-props-refs");
// TODO(kyle): use babel here to do ast
const convertConsoleLogToPrint = (code) => {
if (!code)
return code;
// Match console.log statements with various argument patterns
return code.replace(/console\.log\s*\(\s*(.*?)\s*\)/g, (match, args) => {
// Handle empty console.log()
if (!args.trim()) {
return 'print()';
}
// Simple handling for basic console.log calls
// For complex cases, we'd need a more sophisticated parser
return `print(${(0, exports.ensureSwiftStringFormat)(args)})`;
});
};
exports.convertConsoleLogToPrint = convertConsoleLogToPrint;
// Helper function to ensure Swift strings use double quotes
const ensureSwiftStringFormat = (code) => {
if (!code)
return code;
// We need a more reliable approach to handle nested quotes
// This uses a state machine approach to track whether we're inside double quotes
let result = '';
let insideDoubleQuotes = false;
for (let i = 0; i < code.length; i++) {
const char = code[i];
const prevChar = i > 0 ? code[i - 1] : '';
// Handle quote state tracking
if (char === '"' && prevChar !== '\\') {
insideDoubleQuotes = !insideDoubleQuotes;
result += char;
}
// Only replace single quotes when not inside double quotes
else if (char === "'" && prevChar !== '\\' && !insideDoubleQuotes) {
// Start of a single-quoted string
result += '"';
// Find the end of the single-quoted string, accounting for escaped quotes
let j = i + 1;
while (j < code.length) {
if (code[j] === "'" && code[j - 1] !== '\\') {
break;
}
j++;
}
// Add the string content
result += code.substring(i + 1, j);
// Add closing double quote if we found the end
if (j < code.length) {
result += '"';
i = j; // Skip to the end of the single-quoted string
}
else {
// If no closing quote was found, just add the single quote as is
result = result.substring(0, result.length - 1) + "'";
}
}
else {
result += char;
}
}
return result;
};
exports.ensureSwiftStringFormat = ensureSwiftStringFormat;
const stripStateAndProps = ({ json, options, }) => {
return (code) => {
// Convert console.log statements to Swift print
code = (0, exports.convertConsoleLogToPrint)(code);
// Ensure Swift strings use double quotes
code = (0, exports.ensureSwiftStringFormat)(code);
// In Swift, we use self.propertyName for accessing properties
return (0, strip_state_and_props_refs_1.stripStateAndPropsRefs)(code, {
includeState: true,
includeProps: true,
replaceWith: (name) => {
// In Swift, we access properties with self.propertyName
return `self.${name}`;
},
});
};
};
exports.stripStateAndProps = stripStateAndProps;
const getSwiftType = (type) => {
if (!type)
return 'Any';
// Handle array types with proper Swift syntax
if (type.includes('Array<') || type.includes('[]') || type.toLowerCase().startsWith('array')) {
// Extract the element type from Array<ElementType>
let elementType = 'Any';
// Match different array type patterns
const arrayMatch = type.match(/Array<([^>]+)>/i) ||
type.match(/([^[\]]+)\[\]/i) ||
type.match(/array\s*<([^>]+)>/i);
if (arrayMatch && arrayMatch[1]) {
elementType = (0, exports.getSwiftType)(arrayMatch[1].trim());
}
// Return Swift array type: [ElementType]
return `[${elementType}]`;
}
// Handle primitive types
switch (type.toLowerCase()) {
case 'string':
return 'String';
case 'number':
return 'Double';
case 'boolean':
case 'bool':
return 'Bool';
case 'any':
return 'Any';
case 'void':
return 'Void';
case 'object':
return '[String: Any]';
case 'null':
case 'undefined':
return 'Optional<Any>';
default:
// For complex types, return as is with first letter capitalized
return type.charAt(0).toUpperCase() + type.slice(1);
}
};
exports.getSwiftType = getSwiftType;
const jsxElementToSwiftUIView = (tagName) => {
// Map JSX/HTML elements to SwiftUI components
switch (tagName.toLowerCase()) {
case 'div':
return 'VStack';
case 'span':
case 'p':
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return 'Text';
case 'img':
return 'Image';
case 'input':
return 'TextField';
case 'button':
return 'Button';
case 'a':
return 'Link';
case 'ul':
return 'List';
case 'li':
return 'Text'; // Will be wrapped in List
case 'form':
return 'Form';
case 'select':
return 'Picker';
case 'option':
return 'Text'; // Options in SwiftUI are part of the Picker content
default:
// For custom components or unrecognized tags
return (0, capitalize_1.capitalize)(tagName);
}
};
exports.jsxElementToSwiftUIView = jsxElementToSwiftUIView;
const cssToSwiftUIModifiers = (style) => {
const modifiers = [];
// Map CSS properties to SwiftUI modifiers
Object.entries(style).forEach(([key, value]) => {
switch (key) {
case 'backgroundColor':
modifiers.push(`.background(Color("${value}"))`);
break;
case 'color':
modifiers.push(`.foregroundColor(Color("${value}"))`);
break;
case 'fontSize':
const fontSize = parseInt(value);
if (!isNaN(fontSize)) {
modifiers.push(`.font(.system(size: ${fontSize}))`);
}
break;
case 'fontWeight':
modifiers.push(`.fontWeight(.${value})`);
break;
case 'padding':
modifiers.push(`.padding(${value})`);
break;
case 'margin':
// Swift doesn't have direct margin equivalent, we'll use padding
modifiers.push(`// Note: 'margin' converted to padding: ${value}`);
modifiers.push(`.padding(${value})`);
break;
case 'width':
modifiers.push(`.frame(width: ${value})`);
break;
case 'height':
modifiers.push(`.frame(height: ${value})`);
break;
// Add more CSS to SwiftUI modifier mappings as needed
default:
modifiers.push(`// Unmapped style: ${key}: ${value}`);
}
});
return modifiers;
};
exports.cssToSwiftUIModifiers = cssToSwiftUIModifiers;
const getStatePropertyTypeAnnotation = (propertyType, type) => {
// Use appropriate SwiftUI property wrappers
switch (propertyType) {
case 'reactive':
// For reactive state, use @State for simple values
// @Observable would be used for classes but requires Swift 5.9+/iOS 17+
return `@State private var`;
case 'normal':
// For normal state, use @State for simple values
return `@State private var`;
default:
// For non-reactive values, use a regular property
return `var`;
}
};
exports.getStatePropertyTypeAnnotation = getStatePropertyTypeAnnotation;
const getEventHandlerName = (eventName) => {
switch (eventName) {
case 'onClick':
return 'onTapGesture';
case 'onChange':
return 'onChange';
case 'onInput':
return 'onEditingChanged';
case 'onBlur':
return 'onSubmit';
case 'onFocus':
return 'onEditingChanged';
default:
return eventName;
}
};
exports.getEventHandlerName = getEventHandlerName;
const needsScrollView = (json) => {
// Check if overflow property indicates scrolling
if (json.properties.style) {
try {
const styleObj = JSON.parse(json.properties.style);
return (styleObj.overflow === 'auto' ||
styleObj.overflow === 'scroll' ||
styleObj.overflowY === 'auto' ||
styleObj.overflowY === 'scroll' ||
styleObj.overflowX === 'auto' ||
styleObj.overflowX === 'scroll');
}
catch (e) {
// If style can't be parsed, check for overflow directly in the style string
const styleStr = json.properties.style;
return (styleStr.includes('overflow:auto') ||
styleStr.includes('overflow:scroll') ||
styleStr.includes('overflow-y:auto') ||
styleStr.includes('overflow-y:scroll') ||
styleStr.includes('overflow-x:auto') ||
styleStr.includes('overflow-x:scroll'));
}
}
return false;
};
exports.needsScrollView = needsScrollView;
const isForEachBlock = (json) => {
var _a;
// Check if this is a ForEach binding using the bindings.each pattern
return !!((_a = json.bindings.each) === null || _a === void 0 ? void 0 : _a.code);
};
exports.isForEachBlock = isForEachBlock;
const getForEachParams = (json, processCode) => {
var _a, _b;
if (!((_a = json.bindings.each) === null || _a === void 0 ? void 0 : _a.code)) {
return { collection: '', itemName: 'item', indexName: null };
}
const eachCode = json.bindings.each.code;
let itemName = 'item';
let indexName = null;
// Extract collection, item name, and index name from each binding
try {
// Parse expressions like: items.map(item => ...)
// or items.map((item, index) => ...)
const match = eachCode.match(/(\w+)\.map\(\s*(?:\()?([^,)]+)(?:,\s*([^)]+))?\)?/);
if (match) {
const collection = processCode(match[1]);
itemName = match[2].trim();
indexName = ((_b = match[3]) === null || _b === void 0 ? void 0 : _b.trim()) || null;
return { collection, itemName, indexName };
}
// Fallback to the whole code as collection if pattern doesn't match
return {
collection: processCode(eachCode),
itemName,
indexName,
};
}
catch (e) {
console.warn('Failed to parse each binding:', eachCode);
return {
collection: processCode(eachCode),
itemName,
indexName,
};
}
};
exports.getForEachParams = getForEachParams;
const camelToSnakeCase = (str) => {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
};
exports.camelToSnakeCase = camelToSnakeCase;
const getBindingType = (key) => {
if (key.startsWith('bind:')) {
return key.substring(5);
}
return key;
};
exports.getBindingType = getBindingType;
/**
* Extract function signature information from JavaScript function code
*/
const extractFunctionSignature = (code) => {
// Default values
let name = '';
let params = [];
let returnType = 'Void';
let body = '';
// Extract function name, parameters, and body
const funcMatch = code.match(/(?:function\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)?\s*\(([^)]*)\)\s*(?:=>)?\s*(?:{([\s\S]*)}|(.*))/);
if (funcMatch) {
name = funcMatch[1] || '';
// Extract parameters
const paramsStr = funcMatch[2].trim();
if (paramsStr) {
params = paramsStr.split(',').map((param) => {
// Handle TypeScript-style parameter types if present
const paramParts = param.trim().split(':');
const paramName = paramParts[0].trim();
const paramType = paramParts.length > 1 ? (0, exports.getSwiftType)(paramParts[1].trim()) : 'Any';
return { name: paramName, type: paramType };
});
}
// Extract function body
body = funcMatch[3] || funcMatch[4] || '';
// Try to determine return type from TypeScript annotations or infer from return statements
const returnTypeMatch = code.match(/\)\s*:\s*([^{]+)/);
if (returnTypeMatch) {
returnType = (0, exports.getSwiftType)(returnTypeMatch[1].trim());
}
else if (body.includes('return')) {
// Try to infer from return statements
const returnValueMatch = body.match(/return\s+(["'].*["']|true|false|\d+|\d+\.\d+|\[.*\])/);
if (returnValueMatch) {
const returnValue = returnValueMatch[1];
if (returnValue.startsWith('"') || returnValue.startsWith("'")) {
returnType = 'String';
}
else if (returnValue === 'true' || returnValue === 'false') {
returnType = 'Bool';
}
else if (returnValue.match(/^\d+$/)) {
returnType = 'Int';
}
else if (returnValue.match(/^\d+\.\d+$/)) {
returnType = 'Double';
}
else if (returnValue.startsWith('[')) {
returnType = '[Any]';
}
}
}
}
return { name, params, returnType, body };
};
exports.extractFunctionSignature = extractFunctionSignature;
/**
* Convert JavaScript function code to Swift function syntax
*/
const convertJsFunctionToSwift = (code, functionName) => {
// Extract the function signature
const { name, params, returnType, body } = (0, exports.extractFunctionSignature)(code);
// Use provided name or extracted name
const finalName = functionName || name || 'function';
// Convert function body to Swift
let swiftBody = body
// Convert variable declarations
.replace(/\bvar\s+(\w+)/g, 'var $1')
.replace(/\blet\s+(\w+)/g, 'let $1')
.replace(/\bconst\s+(\w+)/g, 'let $1')
// Convert common array methods
.replace(/\.push\(/g, '.append(')
.replace(/\.map\(/g, '.map(')
.replace(/\.filter\(/g, '.filter(')
.replace(/\.includes\(/g, '.contains(')
.replace(/\.indexOf\(/g, '.firstIndex(of: ')
// Convert null/undefined checks
.replace(/=== null/g, '== nil')
.replace(/!== null/g, '!= nil')
.replace(/=== undefined/g, '== nil')
.replace(/!== undefined/g, '!= nil')
// Convert console.log
.replace(/console\.log\((.+?)\)/g, 'print($1)');
// Create parameter list with Swift types
const paramList = params.map((p) => `${p.name}: ${p.type}`).join(', ');
// Build the Swift function signature
const signature = `func ${finalName}(${paramList}) -> ${returnType}`;
// Build the complete Swift function
const swiftCode = `${signature} {\n ${swiftBody}\n}`;
return { swiftCode, signature };
};
exports.convertJsFunctionToSwift = convertJsFunctionToSwift;