Automating Mendix Widget Creation

A conceptual guide and script for automating the process of converting React components into Mendix pluggable widgets.

The "Why": Benefits of Automation

Manually creating Mendix widgets from React components is repetitive and error-prone. An automation script provides several key benefits: - **Consistency:** Ensures every widget is built and structured the same way. - **Speed:** Drastically reduces the time to create and update widgets. - **Reliability:** Eliminates manual errors in XML configuration and file packaging. - **Scalability:** Makes it feasible to manage a large library of custom widgets.

The "How": A Conceptual Overview

The process can be broken down into a few logical steps, which can be orchestrated by a Node.js script. 1. **Component Scaffolding:** Find the source React component files. 2. **XML Generation:** Introspect the component's props to auto-generate the Mendix widget XML. 3. **Bundling:** Use a tool like Webpack or Rollup to compile the code and styles. 4. **Packaging:** Zip the build artifacts into an `.mpk` file.

The Automation Script (Proof of Concept)

Below is a conceptual Node.js script that demonstrates how this automation could work. Note: This is a template and requires a real project structure and further development (e.g., more robust prop parsing, error handling) to be fully functional. It uses the `fs-extra` and `archiver` libraries.

// To use this script, first install dependencies:
// npm install fs-extra archiver commander

const fs = require('fs-extra');
const path = require('path');
const archiver = require('archiver');
const { program } = require('commander');

// A simplified mapping from TypeScript types to Mendix property types
const typeMapping = {
    string: 'string',
    boolean: 'boolean',
    // Add more complex types like 'action', 'datasource', etc. as needed
};

// Generates the <properties> part of the widget XML
function generateXmlProperties(propsInterface) {
    let propertiesXml = '<properties>\n';
    for (const prop of propsInterface) {
        const mendixType = typeMapping[prop.type] || 'string'; // Default to string
        propertiesXml += `    <property key="${prop.name}" type="${mendixType}" required="${prop.required}">
        <caption>${prop.name}</caption>
        <description>Generated property for ${prop.name}</description>
    </property>\n`;
    }
    propertiesXml += '</properties>';
    return propertiesXml;
}

// A mock function to parse props from a TSX file
// In a real script, this would use a proper TypeScript parser like ts-morph
function parseComponentProps(filePath) {
    console.log(`Parsing props from: ${filePath}`);
    // This is a simplified mock. A real implementation is much more complex.
    return [
        { name: 'triggerText', type: 'string', required: 'false' },
        { name: 'titleText', type: 'string', required: 'false' },
        { name: 'descriptionText', type: 'string', required: 'false' },
    ];
}


async function createWidget(componentName, options) {
    const widgetName = options.widgetName || `${componentName}Widget`;
    console.log(`Generating ${widgetName} from ${componentName} component...`);

    const widgetDir = path.join(process.cwd(), 'widgets', widgetName);
    const componentPath = path.join(process.cwd(), 'packages/shadcn-ui-components/src/components/ui', `${componentName}.tsx`);

    if (!fs.existsSync(componentPath)) {
        console.error(`Component source file not found at: ${componentPath}`);
        return;
    }
    
    // 1. Generate XML
    const props = parseComponentProps(componentPath);
    const propertiesXml = generateXmlProperties(props);
    const xmlTemplate = `<?xml version="1.0" encoding="utf-8"?>
<widget id="com.example.${widgetName.toLowerCase()}" pluginWidget="true" supportedPlatform="Web" xmlns="http://www.mendix.com/widget/1.0/">
    <name>${widgetName}</name>
    <description>A widget generated from the ${componentName} component.</description>
    ${propertiesXml}
</widget>`;

    // Here you would scaffold the full widget directory structure
    // For this example, we'll assume a 'dist' folder is created by a build step
    const distDir = path.join(widgetDir, 'dist');
    await fs.ensureDir(distDir);
    fs.writeFileSync(path.join(widgetDir, `${widgetName}.xml`), xmlTemplate);
    console.log('Generated widget XML.');


    // 2. Mock build step (in reality you would run webpack/rollup here)
    console.log('Running mock build step...');
    const jsContent = `// Bundled JS for ${widgetName}`;
    const cssContent = `/* Bundled CSS for ${widgetName} */`;
    fs.writeFileSync(path.join(distDir, `${widgetName}.js`), jsContent);
    fs.writeFileSync(path.join(distDir, `${widgetName}.css`), cssContent);


    // 3. Create the .mpk package
    const mpkPath = path.join(distDir, `${widgetName}.mpk`);
    const output = fs.createWriteStream(mpkPath);
    const archive = archiver('zip');

    archive.on('error', err => { throw err; });
    archive.pipe(output);
    archive.directory(widgetDir, false);
    await archive.finalize();

    console.log(`Successfully created widget package at ${mpkPath}`);
}

program
    .command('generate <componentName>')
    .description('Generate a Mendix widget from a React component')
    .option('-w, --widgetName <name>', 'The name for the output widget')
    .action(createWidget);

program.parse(process.argv);
How to Use This Script

1. **Save:** Save the code above into a file, for example, `scripts/create-mendix-widget.js`. 2. **Install Dependencies:** Run `npm install fs-extra archiver commander` in your project root. 3. **Run from Terminal:** You could then execute it like this: `node scripts/create-mendix-widget.js generate dialog --widgetName MyDialogWidget`. 4. **Integrate with `package.json`:** For easier use, add a script to your `package.json`: `"generate-widget": "node scripts/create-mendix-widget.js"`. Then you can run `npm run generate-widget -- generate dialog`.

Next Steps and Real-World Improvements

This script is a starting point. A production-ready version would need: - **A real TypeScript parser:** Use libraries like `ts-morph` or the TypeScript Compiler API to accurately parse prop interfaces instead of mocking them. - **Real Build Integration:** Replace the mock build step with actual commands to run your bundler (e.g., `exec('npx webpack --config webpack.config.js')`). - **Error Handling:** Add robust error handling for file operations, parsing, and building. - **Template-based Scaffolding:** Use template files for the widget structure (`package.json`, `MyWidget.tsx`, etc.) and populate them with the correct names and imports.