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
676 lines (582 loc) • 19.5 kB
JavaScript
/**
* JSXPatternConverter - Convert complex React/JSX patterns to Flutter
*
* Handles:
* - Higher-Order Components (HOCs) → Mixins
* - Render Props → Builder pattern
* - React.memo → const constructors
* - React.forwardRef → GlobalKey
* - React.Fragment → multiple children
* - Complex conditional rendering
* - List rendering with keys
*/
const { createModuleLogger } = require('../utils/logger');
const logger = createModuleLogger('JSXPatternConverter');
/**
* Pattern types
*/
const PatternType = {
HOC: 'HigherOrderComponent',
RENDER_PROPS: 'RenderProps',
MEMO: 'React.memo',
FORWARD_REF: 'React.forwardRef',
FRAGMENT: 'React.Fragment',
CONDITIONAL: 'ConditionalRendering',
LIST: 'ListRendering',
PORTAL: 'React.Portal',
SUSPENSE: 'React.Suspense',
ERROR_BOUNDARY: 'ErrorBoundary',
};
/**
* JSXPatternConverter class
*/
class JSXPatternConverter {
/**
* Detect and convert JSX patterns
* @param {Object} ast - AST from React parser
* @param {Object} context - Conversion context
* @returns {Object} Conversion result
*/
convertPatterns(ast, context = {}) {
logger.debug('Analyzing JSX patterns');
const result = {
hocs: [], // Higher-Order Components
renderProps: [], // Render Props patterns
memoized: [], // React.memo components
forwardRefs: [], // forwardRef components
fragments: [], // Fragment usages
conditionals: [], // Complex conditionals
lists: [], // List renderings
mixins: [], // Generated mixins
builders: [], // Builder patterns
imports: new Set(), // Required imports
};
// Detect patterns in AST
this._detectPatterns(ast, result, context);
logger.success(`Detected ${this._getTotalPatterns(result)} JSX patterns`);
return result;
}
/**
* Detect patterns in AST
* @param {Object} ast - AST node
* @param {Object} result - Result accumulator
* @param {Object} context - Context
* @private
*/
_detectPatterns(ast, result, context) {
const t = require('@babel/types');
const traverse = require('@babel/traverse').default;
traverse(ast, {
// Detect all CallExpression patterns (HOCs, React.memo, forwardRef, list rendering)
CallExpression: (path) => {
if (this._isHOC(path.node)) {
this._convertHOC(path.node, result);
} else if (this._isReactMemo(path.node)) {
this._convertMemo(path.node, result);
} else if (this._isForwardRef(path.node)) {
this._convertForwardRef(path.node, result);
} else if (this._isListRendering(path.node)) {
this._convertListRendering(path.node, result);
}
},
// Detect Render Props
JSXElement: (path) => {
if (this._isRenderProps(path.node)) {
this._convertRenderProps(path.node, result);
}
},
// Detect Fragment
JSXFragment: (path) => {
this._convertFragment(path.node, result);
},
// Detect complex conditionals
ConditionalExpression: (path) => {
if (path.parent.type === 'JSXExpressionContainer') {
this._convertConditional(path.node, result);
}
},
});
}
/**
* Check if node is a Higher-Order Component
* @param {Object} node - AST node
* @returns {boolean}
* @private
*/
_isHOC(node) {
const t = require('@babel/types');
// Pattern: withSomething(Component)
if (t.isCallExpression(node) &&
t.isIdentifier(node.callee) &&
node.callee.name.startsWith('with')) {
return true;
}
return false;
}
/**
* Convert HOC to mixin
* @param {Object} node - HOC call expression
* @param {Object} result - Result accumulator
* @private
*/
_convertHOC(node, result) {
const t = require('@babel/types');
const hocName = node.callee.name; // e.g., 'withAuth'
const componentArg = node.arguments[0];
let componentName = 'Component';
if (t.isIdentifier(componentArg)) {
componentName = componentArg.name;
}
const mixinName = `${hocName.replace('with', '')}Mixin`;
result.hocs.push({
hocName,
componentName,
mixinName,
pattern: 'mixin',
});
logger.debug('Detected HOC', { hocName, componentName, mixinName });
}
/**
* Check if node is React.memo
* @param {Object} node - AST node
* @returns {boolean}
* @private
*/
_isReactMemo(node) {
const t = require('@babel/types');
// Pattern: React.memo(Component) or memo(Component)
if (t.isCallExpression(node)) {
if (t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.object, { name: 'React' }) &&
t.isIdentifier(node.callee.property, { name: 'memo' })) {
return true;
}
if (t.isIdentifier(node.callee, { name: 'memo' })) {
return true;
}
}
return false;
}
/**
* Convert React.memo to const constructor
* @param {Object} node - React.memo call
* @param {Object} result - Result accumulator
* @private
*/
_convertMemo(node, result) {
const t = require('@babel/types');
const componentArg = node.arguments[0];
let componentName = 'Component';
if (t.isIdentifier(componentArg)) {
componentName = componentArg.name;
} else if (t.isArrowFunctionExpression(componentArg) ||
t.isFunctionExpression(componentArg)) {
componentName = 'MemoizedComponent';
}
result.memoized.push({
componentName,
pattern: 'const_constructor',
flutterPattern: 'Use const constructor for immutable widgets',
});
logger.debug('Detected React.memo', { componentName });
}
/**
* Check if node is React.forwardRef
* @param {Object} node - AST node
* @returns {boolean}
* @private
*/
_isForwardRef(node) {
const t = require('@babel/types');
// Pattern: React.forwardRef((props, ref) => ...)
if (t.isCallExpression(node)) {
if (t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.object, { name: 'React' }) &&
t.isIdentifier(node.callee.property, { name: 'forwardRef' })) {
return true;
}
if (t.isIdentifier(node.callee, { name: 'forwardRef' })) {
return true;
}
}
return false;
}
/**
* Convert React.forwardRef to GlobalKey
* @param {Object} node - forwardRef call
* @param {Object} result - Result accumulator
* @private
*/
_convertForwardRef(node, result) {
result.forwardRefs.push({
pattern: 'GlobalKey',
flutterPattern: 'Use GlobalKey to access widget from parent',
implementation: 'final key = GlobalKey<MyWidgetState>();',
});
logger.debug('Detected React.forwardRef');
}
/**
* Check if JSX element uses render props pattern
* @param {Object} node - JSX element
* @returns {boolean}
* @private
*/
_isRenderProps(node) {
const t = require('@babel/types');
// Check if any prop is a function (render prop)
if (node.openingElement && node.openingElement.attributes) {
for (const attr of node.openingElement.attributes) {
if (t.isJSXAttribute(attr) &&
t.isJSXExpressionContainer(attr.value)) {
const expr = attr.value.expression;
if (t.isArrowFunctionExpression(expr) ||
t.isFunctionExpression(expr)) {
// Common render prop names
const propName = attr.name.name;
if (propName === 'render' ||
propName === 'children' ||
propName.startsWith('render')) {
return true;
}
}
}
}
}
// Check if children is a function (render prop pattern)
if (node.children && node.children.length > 0) {
for (const child of node.children) {
if (t.isJSXExpressionContainer(child)) {
const expr = child.expression;
if (t.isArrowFunctionExpression(expr) ||
t.isFunctionExpression(expr)) {
return true;
}
}
}
}
return false;
}
/**
* Convert render props to Builder pattern
* @param {Object} node - JSX element with render props
* @param {Object} result - Result accumulator
* @private
*/
_convertRenderProps(node, result) {
const t = require('@babel/types');
const elementName = node.openingElement.name.name || 'Component';
// Find the render prop - first check attributes
let renderPropName = 'render';
if (node.openingElement.attributes) {
for (const attr of node.openingElement.attributes) {
if (t.isJSXAttribute(attr) &&
t.isJSXExpressionContainer(attr.value)) {
const expr = attr.value.expression;
if (t.isArrowFunctionExpression(expr) ||
t.isFunctionExpression(expr)) {
const propName = attr.name.name;
if (propName.startsWith('render') || propName === 'children') {
renderPropName = propName;
break;
}
}
}
}
}
// If no render prop in attributes, check if children is a function
if (renderPropName === 'render' && node.children) {
for (const child of node.children) {
if (t.isJSXExpressionContainer(child)) {
const expr = child.expression;
if (t.isArrowFunctionExpression(expr) ||
t.isFunctionExpression(expr)) {
renderPropName = 'children';
break;
}
}
}
}
result.renderProps.push({
componentName: elementName,
renderPropName,
pattern: 'Builder',
flutterPattern: 'Use Builder widget with builder callback',
});
logger.debug('Detected Render Props', { componentName: elementName, renderPropName });
}
/**
* Convert Fragment
* @param {Object} node - Fragment node
* @param {Object} result - Result accumulator
* @private
*/
_convertFragment(node, result) {
const t = require('@babel/types');
// Count only JSX elements (not text nodes or whitespace)
const jsxChildCount = node.children.filter(child =>
t.isJSXElement(child) || t.isJSXFragment(child)
).length;
result.fragments.push({
pattern: 'multiple_children',
flutterPattern: 'Return array of widgets or use Column/Row',
childCount: jsxChildCount,
});
logger.debug('Detected Fragment', { childCount: jsxChildCount });
}
/**
* Convert conditional rendering
* @param {Object} node - Conditional expression
* @param {Object} result - Result accumulator
* @private
*/
_convertConditional(node, result) {
result.conditionals.push({
pattern: 'ternary',
flutterPattern: 'condition ? Widget1() : Widget2()',
hasAlternate: !!node.alternate,
});
logger.debug('Detected conditional rendering');
}
/**
* Check if node is list rendering
* @param {Object} node - AST node
* @returns {boolean}
* @private
*/
_isListRendering(node) {
const t = require('@babel/types');
// Pattern: array.map(item => <Component key={item.id} />)
if (t.isCallExpression(node) &&
t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.property, { name: 'map' })) {
return true;
}
return false;
}
/**
* Convert list rendering
* @param {Object} node - Map call expression
* @param {Object} result - Result accumulator
* @private
*/
_convertListRendering(node, result) {
const t = require('@babel/types');
const arrayName = t.isMemberExpression(node.callee)
? this._expressionToString(node.callee.object)
: 'items';
result.lists.push({
arrayName,
pattern: 'ListView.builder',
flutterPattern: 'Use ListView.builder with itemCount and itemBuilder',
});
logger.debug('Detected list rendering', { arrayName });
}
/**
* Generate Flutter code from patterns
* @param {Object} patterns - Detected patterns
* @returns {Object} Generated code
*/
generateFlutterCode(patterns) {
const code = {
mixins: this._generateMixins(patterns.hocs),
builders: this._generateBuilders(patterns.renderProps),
memoizedWidgets: this._generateMemoizedWidgets(patterns.memoized),
forwardRefWidgets: this._generateForwardRefWidgets(patterns.forwardRefs),
listBuilders: this._generateListBuilders(patterns.lists),
comments: this._generateComments(patterns),
};
return code;
}
/**
* Generate mixins from HOCs
* @param {Array} hocs - HOC patterns
* @returns {string} Mixin code
* @private
*/
_generateMixins(hocs) {
if (hocs.length === 0) return '';
const mixins = hocs.map(hoc => {
return `
// Mixin converted from HOC: ${hoc.hocName}
mixin ${hoc.mixinName}<T extends StatefulWidget> on State<T> {
// TODO: Implement ${hoc.hocName} logic here
// This was originally a Higher-Order Component wrapping ${hoc.componentName}
}
// Usage: class MyWidgetState extends State<MyWidget> with ${hoc.mixinName} { }
`;
}).join('\n');
return mixins;
}
/**
* Generate builders from render props
* @param {Array} renderProps - Render props patterns
* @returns {string} Builder code
* @private
*/
_generateBuilders(renderProps) {
if (renderProps.length === 0) return '';
const builders = renderProps.map(rp => {
return `
// Builder pattern converted from render prop: ${rp.componentName}.${rp.renderPropName}
Builder(
builder: (BuildContext context) {
// TODO: Implement builder logic
// Original render prop: ${rp.renderPropName}
return Container();
},
)
`;
}).join('\n');
return builders;
}
/**
* Generate memoized widgets
* @param {Array} memoized - Memoized patterns
* @returns {string} Memoized widget code
* @private
*/
_generateMemoizedWidgets(memoized) {
if (memoized.length === 0) return '';
const widgets = memoized.map(memo => {
return `
// Memoized widget (converted from React.memo)
class ${memo.componentName} extends StatelessWidget {
const ${memo.componentName}({Key? key}) : super(key: key); // Use const constructor
Widget build(BuildContext context) {
// Widget is automatically memoized when using const constructor
return Container();
}
}
`;
}).join('\n');
return widgets;
}
/**
* Generate forwardRef widgets
* @param {Array} forwardRefs - ForwardRef patterns
* @returns {string} ForwardRef code
* @private
*/
_generateForwardRefWidgets(forwardRefs) {
if (forwardRefs.length === 0) return '';
return `
// ForwardRef pattern using GlobalKey
final myWidgetKey = GlobalKey<MyWidgetState>();
// Access widget state from parent:
// myWidgetKey.currentState?.someMethod();
`;
}
/**
* Generate list builders
* @param {Array} lists - List patterns
* @returns {string} List builder code
* @private
*/
_generateListBuilders(lists) {
if (lists.length === 0) return '';
const builders = lists.map(list => {
return `
// List rendering (converted from ${list.arrayName}.map)
ListView.builder(
itemCount: ${list.arrayName}.length,
itemBuilder: (context, index) {
final item = ${list.arrayName}[index];
return ItemWidget(item: item);
},
)
`;
}).join('\n');
return builders;
}
/**
* Generate helpful comments
* @param {Object} patterns - All patterns
* @returns {string} Comments
* @private
*/
_generateComments(patterns) {
const comments = [];
if (patterns.fragments.length > 0) {
comments.push('// Fragments: In Flutter, return array of widgets or wrap in Column/Row');
}
if (patterns.conditionals.length > 0) {
comments.push('// Conditionals: Use ternary (condition ? Widget1() : Widget2()) or if statements');
}
return comments.join('\n');
}
/**
* Convert expression to string
* @param {Object} expression - AST expression
* @returns {string}
* @private
*/
_expressionToString(expression) {
const generate = require('@babel/generator').default;
try {
return generate(expression, { concise: true }).code;
} catch (error) {
logger.warn('Failed to convert expression to string', { error: error.message });
return 'expression';
}
}
/**
* Get total pattern count
* @param {Object} result - Pattern result
* @returns {number}
* @private
*/
_getTotalPatterns(result) {
return result.hocs.length +
result.renderProps.length +
result.memoized.length +
result.forwardRefs.length +
result.fragments.length +
result.conditionals.length +
result.lists.length;
}
/**
* Generate comprehensive conversion guide
* @param {Object} patterns - Detected patterns
* @returns {string} Markdown guide
*/
generateConversionGuide(patterns) {
let guide = '# JSX Pattern Conversion Guide\n\n';
if (patterns.hocs.length > 0) {
guide += '## Higher-Order Components (HOCs)\n\n';
patterns.hocs.forEach(hoc => {
guide += `### ${hoc.hocName}\n`;
guide += `- **Pattern**: Mixin\n`;
guide += `- **Flutter**: \`${hoc.mixinName}\`\n`;
guide += `- **Usage**: \`class MyWidgetState extends State<MyWidget> with ${hoc.mixinName} {}\`\n\n`;
});
}
if (patterns.renderProps.length > 0) {
guide += '## Render Props\n\n';
patterns.renderProps.forEach(rp => {
guide += `### ${rp.componentName}.${rp.renderPropName}\n`;
guide += `- **Pattern**: Builder\n`;
guide += `- **Flutter**: Use \`Builder\` widget with \`builder\` callback\n\n`;
});
}
if (patterns.memoized.length > 0) {
guide += '## React.memo\n\n';
guide += '- **Flutter**: Use \`const\` constructors for immutable widgets\n';
guide += '- **Auto-optimization**: Flutter automatically optimizes const widgets\n\n';
}
if (patterns.forwardRefs.length > 0) {
guide += '## React.forwardRef\n\n';
guide += '- **Flutter**: Use \`GlobalKey\` to access widget/state from parent\n';
guide += '- **Example**: \`final key = GlobalKey<MyWidgetState>();\`\n\n';
}
if (patterns.lists.length > 0) {
guide += '## List Rendering\n\n';
guide += '- **Flutter**: Use \`ListView.builder\` for efficient rendering\n';
guide += '- **Keys**: Flutter handles keys automatically in lists\n\n';
}
return guide;
}
}
module.exports = new JSXPatternConverter();
module.exports.JSXPatternConverter = JSXPatternConverter;
module.exports.PatternType = PatternType;