prprompts-flutter-generator
Version:
AI-powered Flutter development with full automation + official extension support - Generate 32 security-audited guides & auto-implement in 2-3 hours. NEW v5.1: Official Claude Code plugin with hooks, Gemini TOML commands, Qwen MCP settings. Features: Comp
1,067 lines (904 loc) • 32.6 kB
JavaScript
/**
* WidgetGenerator.js
* Converts React ComponentModel to Flutter WidgetModel
*
* Transforms React components into Flutter widgets with proper structure,
* state management, and property mappings.
*/
const { WidgetModel, WidgetProperty, WidgetChild, WidgetImport, WidgetType } = require('../models/WidgetModel');
const { ComponentType } = require('../models/ComponentModel');
const { createModuleLogger } = require('../utils/logger');
const { mapWidget, mapProp, isStyleProp, isEventHandler, getChildProperty, getSpecialHandling } = require('./utils/widgetMapper');
const { toFlutterWidgetName, toFlutterPageName, toCamelCase, toPascalCase } = require('./utils/namingConventions');
const styleConverter = require('./utils/styleConverter');
const logger = createModuleLogger('WidgetGenerator');
/**
* WidgetGenerator class
* Converts ComponentModel to WidgetModel
*/
class WidgetGenerator {
/**
* @param {Object} options
* @param {Object} [options.config] - Configuration object
*/
constructor(options = {}) {
this.config = options.config || {};
}
/**
* Generate Flutter widget from React component
* @param {ComponentModel} component - React component model
* @returns {WidgetModel}
*/
generate(component) {
logger.info(`Generating Flutter widget for: ${component.name}`);
try {
// Determine widget type (StatelessWidget or StatefulWidget)
const widgetType = this._determineWidgetType(component);
// Determine widget name
const widgetName = this._generateWidgetName(component);
// Convert props to widget properties
const properties = this._convertProps(component.props);
// Add state variables as private fields (for StatefulWidget)
if (widgetType === WidgetType.STATEFUL) {
// State will be handled in the State class
}
// Generate imports
const imports = this._generateImports(component);
// Determine state management
const stateManagement = component.recommendedFlutterStateManagement();
// Generate widget model
const widget = new WidgetModel({
name: widgetName,
type: widgetType,
properties,
imports,
stateManagement,
description: component.description,
isResponsive: component.isResponsive,
metadata: {
sourceComponent: component.name,
sourceFile: component.filePath,
hasAsyncOps: component.hasAsyncOperations(),
complexityScore: component.getComplexityScore(),
},
});
logger.success(`Generated widget: ${widgetName} (${widgetType})`);
return widget;
} catch (error) {
logger.error(`Failed to generate widget for ${component.name}`, {
error: error.message,
});
throw error;
}
}
/**
* Determine if widget should be Stateless or Stateful
* @param {ComponentModel} component
* @returns {string}
* @private
*/
_determineWidgetType(component) {
// Has state or lifecycle methods → StatefulWidget
if (component.state.length > 0) {
return WidgetType.STATEFUL;
}
// Has async operations (API calls) → StatefulWidget
if (component.hasAsyncOperations()) {
return WidgetType.STATEFUL;
}
// Has hooks that imply state (useState, useReducer, etc.)
const statefulHooks = ['useState', 'useReducer', 'useEffect'];
if (component.hooks.some(hook => statefulHooks.includes(hook))) {
return WidgetType.STATEFUL;
}
// Default to StatelessWidget
return WidgetType.STATELESS;
}
/**
* Generate widget name from component
* @param {ComponentModel} component
* @returns {string}
* @private
*/
_generateWidgetName(component) {
if (component.type === ComponentType.PAGE) {
return toFlutterPageName(component.name);
} else {
return toFlutterWidgetName(component.name);
}
}
/**
* Convert React props to Flutter widget properties
* @param {ComponentProp[]} props
* @returns {WidgetProperty[]}
* @private
*/
_convertProps(props) {
return props
.filter(prop => !isStyleProp(prop.name)) // Style props handled separately
.map(prop => {
const flutterPropName = this._convertPropName(prop.name);
const flutterType = this._convertPropType(prop.type);
return new WidgetProperty({
name: flutterPropName,
type: flutterType,
isRequired: prop.isRequired,
isFinal: true, // Flutter widget properties are typically final
defaultValue: this._convertDefaultValue(prop.defaultValue, flutterType),
description: `Converted from React prop: ${prop.name}`,
});
});
}
/**
* Convert React prop name to Flutter property name
* @param {string} propName
* @returns {string}
* @private
*/
_convertPropName(propName) {
const mapped = mapProp(propName);
if (mapped === null) {
// Prop should be excluded
return null;
}
return mapped || toCamelCase(propName);
}
/**
* Convert React prop type to Dart type
* @param {string} reactType
* @returns {string}
* @private
*/
_convertPropType(reactType) {
const typeMappings = this.config.getTypeMappings?.() || {
string: 'String',
number: 'num',
boolean: 'bool',
any: 'dynamic',
void: 'void',
null: 'Null',
undefined: 'Null',
Date: 'DateTime',
Array: 'List',
Map: 'Map',
Promise: 'Future',
function: 'VoidCallback',
};
// Handle array types
if (reactType.endsWith('[]')) {
const elementType = reactType.slice(0, -2);
const dartElementType = this._convertPropType(elementType);
return `List<${dartElementType}>`;
}
// Handle generic types (e.g., Array<string>)
const genericMatch = reactType.match(/^(\w+)<(.+)>$/);
if (genericMatch) {
const [, container, inner] = genericMatch;
const dartContainer = typeMappings[container] || container;
const dartInner = this._convertPropType(inner);
return `${dartContainer}<${dartInner}>`;
}
return typeMappings[reactType] || 'dynamic';
}
/**
* Convert default value to Dart syntax
* @param {string} defaultValue
* @param {string} dartType
* @returns {string|null}
* @private
*/
_convertDefaultValue(defaultValue, dartType) {
if (!defaultValue) return null;
// String values
if (dartType === 'String') {
return `'${defaultValue.replace(/'/g, "\\'")}'`;
}
// Boolean values
if (dartType === 'bool') {
return defaultValue === 'true' || defaultValue === true ? 'true' : 'false';
}
// Number values
if (dartType === 'num' || dartType === 'int' || dartType === 'double') {
return defaultValue.toString();
}
// Arrays
if (defaultValue === '[]') {
return 'const []';
}
// Objects
if (defaultValue === '{}') {
return 'const {}';
}
// Functions
if (defaultValue === '() => {}') {
return 'null';
}
return defaultValue;
}
/**
* Generate imports for widget
* @param {ComponentModel} component
* @returns {WidgetImport[]}
* @private
*/
_generateImports(component) {
const imports = [];
// Always import material
imports.push(new WidgetImport({
package: 'flutter/material.dart',
}));
// Import for async operations
if (component.hasAsyncOperations()) {
imports.push(new WidgetImport({
package: 'dart:async',
}));
}
// Import for state management
const stateManagement = component.recommendedFlutterStateManagement();
if (stateManagement === 'bloc') {
imports.push(new WidgetImport({
package: 'flutter_bloc/flutter_bloc.dart',
}));
}
return imports;
}
/**
* Generate widget tree from JSX children
* @param {Object} jsxElement - JSX element from React AST
* @param {Object} context - Context for variable resolution
* @returns {WidgetChild|WidgetChild[]|null}
*/
generateWidgetTree(jsxElement, context = {}) {
if (!jsxElement) return null;
const t = require('@babel/types');
logger.debug('Generating widget tree from JSX', { type: jsxElement.type });
// Handle JSX Element
if (t.isJSXElement(jsxElement)) {
return this._handleJSXElement(jsxElement, context);
}
// Handle JSX Fragment
if (t.isJSXFragment(jsxElement)) {
return this._handleJSXFragment(jsxElement, context);
}
// Handle JSX Text
if (t.isJSXText(jsxElement)) {
return this._handleJSXText(jsxElement, context);
}
// Handle JSX Expression Container
if (t.isJSXExpressionContainer(jsxElement)) {
return this._handleJSXExpression(jsxElement.expression, context);
}
logger.warn('Unsupported JSX type', { type: jsxElement.type });
return null;
}
/**
* Handle JSX Element conversion
* @param {Object} jsxElement - JSX element node
* @param {Object} context - Context for variable resolution
* @returns {WidgetChild}
* @private
*/
_handleJSXElement(jsxElement, context) {
const t = require('@babel/types');
// Extract element name
const elementName = this._getJSXElementName(jsxElement.openingElement.name);
// Map to Flutter widget
const flutterWidget = mapWidget(elementName);
// Check for special handling (FlatList, etc.)
const specialHandling = getSpecialHandling(elementName);
if (specialHandling) {
return this._handleSpecialWidget(jsxElement, elementName, specialHandling, context);
}
// Extract props
const props = this._extractJSXProps(jsxElement.openingElement.attributes, context);
// Extract children
const children = this._extractJSXChildren(jsxElement.children, context);
// Convert props to Flutter properties
const flutterProperties = this._convertPropsToFlutter(props, flutterWidget, children.length > 0);
// Create WidgetChild
return new WidgetChild({
name: flutterWidget,
type: flutterWidget,
properties: flutterProperties,
children: children,
});
}
/**
* Handle JSX Fragment conversion
* @param {Object} jsxFragment - JSX fragment node
* @param {Object} context - Context for variable resolution
* @returns {WidgetChild[]}
* @private
*/
_handleJSXFragment(jsxFragment, context) {
// Fragments don't render in Flutter, just return children directly
return this._extractJSXChildren(jsxFragment.children, context);
}
/**
* Handle JSX Text conversion
* @param {Object} jsxText - JSX text node
* @param {Object} context - Context for variable resolution
* @returns {WidgetChild|null}
* @private
*/
_handleJSXText(jsxText, context) {
const text = jsxText.value.trim();
if (!text) return null;
return new WidgetChild({
name: 'Text',
type: 'Text',
properties: {
data: `'${text.replace(/'/g, "\\'")}'`
},
children: [],
});
}
/**
* Handle JSX Expression (conditionals, lists, variables)
* @param {Object} expression - JSX expression node
* @param {Object} context - Context for variable resolution
* @returns {WidgetChild|WidgetChild[]|null}
* @private
*/
_handleJSXExpression(expression, context) {
const t = require('@babel/types');
// Conditional expression: condition ? A : B
if (t.isConditionalExpression(expression)) {
return this._handleConditionalExpression(expression, context);
}
// Logical expression: condition && Widget
if (t.isLogicalExpression(expression)) {
return this._handleLogicalExpression(expression, context);
}
// Call expression: items.map(...)
if (t.isCallExpression(expression)) {
return this._handleCallExpression(expression, context);
}
// Identifier or member expression (variable reference)
if (t.isIdentifier(expression) || t.isMemberExpression(expression)) {
const varName = this._expressionToString(expression);
logger.debug('Variable reference in JSX', { varName });
// Return a placeholder widget with variable reference
return new WidgetChild({
name: 'Builder',
type: 'Builder',
properties: {
builder: `(context) => ${varName}`,
},
children: [],
});
}
logger.warn('Unsupported JSX expression', { type: expression.type });
return null;
}
/**
* Handle conditional expression (ternary)
* @param {Object} expression - Conditional expression node
* @param {Object} context - Context for variable resolution
* @returns {WidgetChild}
* @private
*/
_handleConditionalExpression(expression, context) {
const condition = this._expressionToString(expression.test);
const consequent = this.generateWidgetTree(expression.consequent, context);
const alternate = this.generateWidgetTree(expression.alternate, context);
// Create a Builder widget with conditional logic
return new WidgetChild({
name: 'Builder',
type: 'Builder',
properties: {
builder: `(context) => ${condition} ? ${this._widgetToString(consequent)} : ${this._widgetToString(alternate)}`,
},
children: [],
});
}
/**
* Handle logical expression (condition && Widget)
* @param {Object} expression - Logical expression node
* @param {Object} context - Context for variable resolution
* @returns {WidgetChild}
* @private
*/
_handleLogicalExpression(expression, context) {
const t = require('@babel/types');
if (expression.operator === '&&') {
const condition = this._expressionToString(expression.left);
const widget = this.generateWidgetTree(expression.right, context);
// if (condition) Widget() else SizedBox.shrink()
return new WidgetChild({
name: 'Builder',
type: 'Builder',
properties: {
builder: `(context) => ${condition} ? ${this._widgetToString(widget)} : const SizedBox.shrink()`,
},
children: [],
});
}
logger.warn('Unsupported logical operator', { operator: expression.operator });
return null;
}
/**
* Handle call expression (mainly .map for lists)
* @param {Object} expression - Call expression node
* @param {Object} context - Context for variable resolution
* @returns {WidgetChild}
* @private
*/
_handleCallExpression(expression, context) {
const t = require('@babel/types');
// Check if this is a .map() call
if (t.isMemberExpression(expression.callee) &&
t.isIdentifier(expression.callee.property) &&
expression.callee.property.name === 'map') {
const arrayName = this._expressionToString(expression.callee.object);
const mapCallback = expression.arguments[0];
if (t.isArrowFunctionExpression(mapCallback) || t.isFunctionExpression(mapCallback)) {
const itemParam = mapCallback.params[0] ? mapCallback.params[0].name : 'item';
const indexParam = mapCallback.params[1] ? mapCallback.params[1].name : 'index';
// Extract the widget returned by map callback
const mapBody = mapCallback.body;
let itemWidget;
if (t.isJSXElement(mapBody) || t.isJSXFragment(mapBody)) {
itemWidget = this.generateWidgetTree(mapBody, { ...context, [itemParam]: 'dynamic' });
} else if (t.isBlockStatement(mapBody)) {
// Find return statement
const returnStmt = mapBody.body.find(stmt => t.isReturnStatement(stmt));
if (returnStmt && returnStmt.argument) {
itemWidget = this.generateWidgetTree(returnStmt.argument, { ...context, [itemParam]: 'dynamic' });
}
}
// Convert to ListView.builder
return new WidgetChild({
name: 'ListView.builder',
type: 'ListView.builder',
properties: {
itemCount: `${arrayName}.length`,
itemBuilder: `(context, ${indexParam}) { final ${itemParam} = ${arrayName}[${indexParam}]; return ${this._widgetToString(itemWidget)}; }`,
},
children: [],
});
}
}
logger.warn('Unsupported call expression', { type: expression.type });
return null;
}
/**
* Handle special widgets (FlatList, SectionList, etc.)
* @param {Object} jsxElement - JSX element node
* @param {string} elementName - React element name
* @param {Object} specialHandling - Special handling config
* @param {Object} context - Context for variable resolution
* @returns {WidgetChild}
* @private
*/
_handleSpecialWidget(jsxElement, elementName, specialHandling, context) {
const props = this._extractJSXProps(jsxElement.openingElement.attributes, context);
if (elementName === 'FlatList' || elementName === 'SectionList') {
// FlatList/SectionList → ListView.builder
const dataSource = props.data || '[]';
const renderItem = props.renderItem || '({ item }) => Container()';
return new WidgetChild({
name: 'ListView.builder',
type: 'ListView.builder',
properties: {
itemCount: `${dataSource}.length`,
itemBuilder: `(context, index) { final item = ${dataSource}[index]; return ${renderItem}; }`,
},
children: [],
});
}
return null;
}
/**
* Get JSX element name from JSX identifier
* @param {Object} jsxName - JSX identifier/member expression
* @returns {string}
* @private
*/
_getJSXElementName(jsxName) {
const t = require('@babel/types');
if (t.isJSXIdentifier(jsxName)) {
return jsxName.name;
}
if (t.isJSXMemberExpression(jsxName)) {
// Handle namespaced components like React.Fragment
return `${this._getJSXElementName(jsxName.object)}.${jsxName.property.name}`;
}
return 'Container';
}
/**
* Extract props from JSX attributes
* @param {Array} attributes - JSX attributes
* @param {Object} context - Context for variable resolution
* @returns {Object}
* @private
*/
_extractJSXProps(attributes, context) {
const t = require('@babel/types');
const props = {};
for (const attr of attributes) {
if (t.isJSXAttribute(attr)) {
const propName = attr.name.name;
let propValue;
if (!attr.value) {
// Boolean prop: <Button disabled />
propValue = 'true';
} else if (t.isStringLiteral(attr.value)) {
// String prop: <Text title="Hello" />
propValue = `'${attr.value.value}'`;
} else if (t.isJSXExpressionContainer(attr.value)) {
// Expression prop: <Button onPress={handlePress} />
propValue = this._expressionToString(attr.value.expression);
} else {
propValue = 'null';
}
props[propName] = propValue;
} else if (t.isJSXSpreadAttribute(attr)) {
// Spread attributes: <Component {...props} />
logger.debug('Spread attribute found (not fully supported)', {
argument: this._expressionToString(attr.argument)
});
}
}
return props;
}
/**
* Extract children from JSX
* @param {Array} jsxChildren - JSX children nodes
* @param {Object} context - Context for variable resolution
* @returns {WidgetChild[]}
* @private
*/
_extractJSXChildren(jsxChildren, context) {
const children = [];
for (const child of jsxChildren) {
const widget = this.generateWidgetTree(child, context);
if (widget) {
if (Array.isArray(widget)) {
children.push(...widget);
} else {
children.push(widget);
}
}
}
return children;
}
/**
* Convert React props to Flutter properties
* @param {Object} props - React props
* @param {string} flutterWidget - Flutter widget name
* @param {boolean} hasChildren - Whether widget has children
* @returns {Object}
* @private
*/
_convertPropsToFlutter(props, flutterWidget, hasChildren) {
const flutterProps = {};
for (const [propName, propValue] of Object.entries(props)) {
// Skip style props (handled separately)
if (isStyleProp(propName)) {
const styleProps = this._convertStyleProp(propValue, flutterWidget);
Object.assign(flutterProps, styleProps);
continue;
}
// Map event handlers
if (isEventHandler(propName)) {
const mappedName = mapProp(propName);
if (mappedName) {
flutterProps[mappedName] = propValue;
}
continue;
}
// Map regular props
const mappedPropName = mapProp(propName);
if (mappedPropName === null) {
// Prop should be excluded
continue;
}
// Special handling for specific props
if (propName === 'value' && flutterWidget === 'TextField') {
// TextField value needs TextEditingController
flutterProps['controller'] = `TextEditingController(text: ${propValue})`;
continue;
}
if (propName === 'src' || propName === 'source') {
// Image source conversion
if (flutterWidget === 'Image') {
flutterProps['image'] = this._convertImageSource(propValue);
continue;
}
}
flutterProps[mappedPropName || propName] = propValue;
}
return flutterProps;
}
/**
* Convert style prop to Flutter properties
* @param {string} styleValue - Style object string or expression
* @param {string} widgetType - Flutter widget type for context
* @returns {Object}
* @private
*/
_convertStyleProp(styleValue, widgetType = 'Container') {
const flutterProps = {};
try {
// Handle object literal style: {backgroundColor: 'red', padding: 10}
if (styleValue.trim().startsWith('{')) {
// Parse the style object safely
const styleObject = this._parseStyleObject(styleValue);
if (styleObject) {
// Use styleConverter to convert CSS/React styles to Flutter
const convertedStyle = styleConverter.convertStyleObject(styleObject, widgetType);
// Map converted properties to Flutter widget properties
Object.assign(flutterProps, convertedStyle);
logger.debug('Converted style object', {
from: styleValue,
to: Object.keys(convertedStyle)
});
}
} else if (styleValue.includes('[') && styleValue.includes(',')) {
// Handle array of styles: [styles.base, styles.active]
logger.debug('Array style reference detected (requires manual review)', { styleValue });
// For style arrays, we'd need to merge them - defer to developer
flutterProps._styleComment = `// TODO: Convert style array: ${styleValue}`;
} else {
// Handle style reference: styles.container or this.props.style
logger.debug('Style reference detected (requires manual review)', { styleValue });
// For variable references, we can't statically convert
flutterProps._styleComment = `// TODO: Convert style reference: ${styleValue}`;
}
} catch (error) {
logger.warn('Failed to convert style prop', {
styleValue,
error: error.message
});
flutterProps._styleComment = `// TODO: Review style conversion: ${styleValue}`;
}
return flutterProps;
}
/**
* Parse style object string into JavaScript object
* @param {string} styleString - Style object as string
* @returns {Object|null}
* @private
*/
_parseStyleObject(styleString) {
try {
// Remove outer braces and parse key-value pairs
const cleaned = styleString.trim().replace(/^{/, '').replace(/}$/, '');
if (!cleaned) return {};
const styleObject = {};
// Split by comma (but not inside strings or nested objects)
const parts = this._smartSplit(cleaned, ',');
for (const part of parts) {
const colonIndex = part.indexOf(':');
if (colonIndex === -1) continue;
let key = part.substring(0, colonIndex).trim();
let value = part.substring(colonIndex + 1).trim();
// Remove quotes from property names (handles both 'border-radius' and "border-radius")
if ((key.startsWith("'") && key.endsWith("'")) ||
(key.startsWith('"') && key.endsWith('"'))) {
key = key.slice(1, -1);
}
// Remove quotes from string values
if ((value.startsWith("'") && value.endsWith("'")) ||
(value.startsWith('"') && value.endsWith('"'))) {
value = value.slice(1, -1);
}
// Convert numeric strings to numbers (but not if they have units like 'px', 'rem', '%')
if (/^-?\d+(\.\d+)?$/.test(value)) {
value = parseFloat(value);
}
// Keep string values with units as strings (will be converted by styleConverter)
// This includes: '100px', '2rem', '50%', etc.
styleObject[key] = value;
}
return styleObject;
} catch (error) {
logger.warn('Failed to parse style object', {
styleString,
error: error.message
});
return null;
}
}
/**
* Smart split that respects nested structures
* @param {string} str - String to split
* @param {string} delimiter - Delimiter character
* @returns {string[]}
* @private
*/
_smartSplit(str, delimiter) {
const parts = [];
let current = '';
let depth = 0;
let inString = false;
let stringChar = null;
for (let i = 0; i < str.length; i++) {
const char = str[i];
// Track string boundaries
if ((char === '"' || char === "'") && (i === 0 || str[i - 1] !== '\\')) {
if (!inString) {
inString = true;
stringChar = char;
} else if (char === stringChar) {
inString = false;
stringChar = null;
}
}
// Track nesting depth (objects, arrays)
if (!inString) {
if (char === '{' || char === '[') depth++;
if (char === '}' || char === ']') depth--;
// Split at delimiter only when not nested
if (char === delimiter && depth === 0) {
parts.push(current);
current = '';
continue;
}
}
current += char;
}
if (current) {
parts.push(current);
}
return parts;
}
/**
* Convert image source to Flutter Image provider
* @param {string} sourceValue - Image source value
* @returns {string}
* @private
*/
_convertImageSource(sourceValue) {
// Remove quotes if present
const cleanValue = sourceValue.replace(/['"]/g, '');
// Network image
if (cleanValue.startsWith('http://') || cleanValue.startsWith('https://')) {
return `NetworkImage(${sourceValue})`;
}
// Asset image
if (cleanValue.startsWith('./') || cleanValue.startsWith('../') || !cleanValue.includes('/')) {
return `AssetImage(${sourceValue})`;
}
// Default to asset
return `AssetImage(${sourceValue})`;
}
/**
* Convert AST expression to string
* @param {Object} expression - AST expression node
* @returns {string}
* @private
*/
_expressionToString(expression) {
const t = require('@babel/types');
const generate = require('@babel/generator').default;
if (!expression) return 'null';
// Use babel generator to convert AST to code
try {
const result = generate(expression, { concise: true });
return result.code;
} catch (error) {
logger.warn('Failed to convert expression to string', { error: error.message });
return 'null';
}
}
/**
* Convert widget to string representation
* @param {WidgetChild|WidgetChild[]|null} widget - Widget to convert
* @returns {string}
* @private
*/
_widgetToString(widget) {
if (!widget) return 'const SizedBox.shrink()';
if (Array.isArray(widget)) {
// Multiple widgets - wrap in Column
const widgetStrings = widget.map(w => this._widgetToString(w)).join(', ');
return `Column(children: [${widgetStrings}])`;
}
// Single widget - generate constructor call
const props = Object.entries(widget.properties)
.map(([key, value]) => `${key}: ${value}`)
.join(', ');
const childProp = getChildProperty(widget.name);
let childrenStr = '';
if (widget.children.length > 0) {
if (childProp === 'children') {
const childrenContent = widget.children.map(c => this._widgetToString(c)).join(', ');
childrenStr = `, children: [${childrenContent}]`;
} else {
childrenStr = `, child: ${this._widgetToString(widget.children[0])}`;
}
}
return `${widget.name}(${props}${childrenStr})`;
}
/**
* Convert conditional rendering (ternary) to Flutter
* @param {Object} conditionalExpression
* @returns {string}
*/
convertConditionalRendering(conditionalExpression) {
// condition ? Widget1() : Widget2()
// Stays similar in Flutter
const { test, consequent, alternate } = conditionalExpression;
// Would generate Dart ternary or if/else
return `${test} ? ${consequent} : ${alternate}`;
}
/**
* Convert list rendering (.map) to ListView.builder
* @param {Object} mapExpression
* @returns {Object}
*/
convertListRendering(mapExpression) {
// items.map(item => <Widget key={item.id} {...item} />)
// → ListView.builder(itemCount: items.length, itemBuilder: (context, index) => Widget(...))
logger.debug('Converting list rendering to ListView.builder');
return {
widget: 'ListView.builder',
properties: {
itemCount: 'items.length',
itemBuilder: '(context, index) { /* widget */ }',
},
};
}
/**
* Convert event handlers to Flutter callbacks
* @param {string} eventName
* @param {string} handler
* @returns {Object}
*/
convertEventHandler(eventName, handler) {
const flutterEventName = mapProp(eventName);
// onClick → onTap
// onChange → onChanged
// etc.
return {
name: flutterEventName,
handler: this._convertHandlerFunction(handler),
};
}
/**
* Convert React handler function to Dart
* @param {string} handler
* @returns {string}
* @private
*/
_convertHandlerFunction(handler) {
// () => { } → () { }
// (e) => { } → () { } (event param often not needed in Flutter)
return handler
.replace(/=>/g, '')
.replace(/function\s*\(/g, '(');
}
/**
* Generate multiple widgets from multiple components
* @param {ComponentModel[]} components
* @returns {WidgetModel[]}
*/
generateMultiple(components) {
logger.info(`Generating ${components.length} Flutter widgets`);
const widgets = [];
for (const component of components) {
try {
const widget = this.generate(component);
widgets.push(widget);
} catch (error) {
logger.error(`Skipping ${component.name}: ${error.message}`);
}
}
logger.success(`Generated ${widgets.length}/${components.length} widgets`);
return widgets;
}
}
/**
* Create a widget generator instance
* @param {Object} options
* @returns {WidgetGenerator}
*/
function createWidgetGenerator(options) {
return new WidgetGenerator(options);
}
module.exports = {
WidgetGenerator,
createWidgetGenerator,
};