Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/pluggable-widgets-tools/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Changed

- We changed the order of imports in generated widget prop types to match that of the eslint sort-imports rule.

## [11.8.1] - 2026-03-16

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion packages/pluggable-widgets-tools/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mendix/pluggable-widgets-tools",
"version": "11.8.1",
"version": "11.8.2",
"description": "Mendix Pluggable Widgets Tools",
"engines": {
"node": ">=20"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { ImportableModule, generateImports, ImportStatement } from "../generateImports";

describe("generateImports", () => {

describe("with no modules", () => {
it("returns no import statements for empty code", () => {
const result = generateImports([], "");
expect(result).toEqual([]);
});

it("returns no import statements", () => {
const result = generateImports([], "const x = 5; const y = 10;");
expect(result).toEqual([]);
});
});

describe("with modules", () => {
const foobarModule = new ImportableModule("foobar", ["foo", "bar", "baz"]);
const numbersModule = new ImportableModule("numbers", ["pi", "e", "infinity"]);
const animalsModule = new ImportableModule("animals", ["cat", "dog", "capibara"]);

describe("with no matching members in code", () => {

it("returns no import statements", () => {
const code = "x y z";
const result = generateImports([foobarModule, numbersModule, animalsModule], code);

expect(result).toEqual([]);
});

});

describe("with matching members in code", () => {

it("returns import statements for each module with matching members", () => {
const code = " foo cat ";
const result = generateImports([foobarModule, animalsModule], code);

expect(result).toHaveLength(2);
expect(result[0].from).toBe("animals");
expect(result[1].from).toBe("foobar");
});

it("orders import statements alphabetically by module name", () => {
const code = " foo pi cat ";
const result = generateImports([foobarModule, numbersModule, animalsModule], code);

expect(result.map(s => s.from)).toEqual(["animals", "foobar", "numbers"]);
});

describe("with mixed import types", () => {
const code = " foo bar pi e cat ";
const result = generateImports([numbersModule, animalsModule, foobarModule], code);

it("groups import statements by type (multiple, single)", () => {
// Multiple-member imports should come first
expect(result[0].members).toHaveLength(2);
expect(result[1].members).toHaveLength(2);

// Single-member imports should come last
expect(result[2].members).toHaveLength(1);
});

it("sorts grouped imports alphabetically", () => {
expect(result[0].from).toBe("foobar")
expect(result[1].from).toBe("numbers")
});
})

});

});

});

describe("ImportableModule", () => {

describe("generateImportStatement()", () => {
const module = new ImportableModule("test-module", ["TypeA", "TypeB", "helper"]);

it("finds no members for empty code", () => {
const statement = module.generateImportStatement("");
expect(statement.members).toEqual([]);
expect(statement.from).toBe("test-module");
});

it("finds no members for code with names containing members", () => {
// Should NOT match partial names like "TypeAbc" or "myhelper"
const code = "TypeAbc myhelper";
const statement = module.generateImportStatement(code);
expect(statement.members).toEqual([]);
});

it.each([" TypeA ", "(TypeA)", "{TypeA}", "<TypeA>", "'TypeA'", '"TypeA"', " TypeA.foo", " TypeA:"])(
"matches member TypeA in code `%s`", (code) => {
const statement = module.generateImportStatement(code);
expect(statement.members).toContain("TypeA");
});
});
});

describe("ImportStatement", () => {

describe("type property", () => {

it("returns 'none' for statement with no members", () => {
const statement = new ImportStatement("test-module", []);
expect(statement.type).toBe("none");
});

it("returns 'single' for statement with 1 member", () => {
const statement = new ImportStatement("test-module", ["TypeA"]);
expect(statement.type).toBe("single");
});

it("returns 'multiple' for statement with multiple members", () => {
const statement = new ImportStatement("test-module", ["TypeA", "TypeB"]);
expect(statement.type).toBe("multiple");
});
});

describe("toString() formats string properly", () => {

it("for statement with 1 member", () => {
const statement = new ImportStatement("test-module", ["TypeA"]);
expect(statement.toString()).toBe('import { TypeA } from "test-module";');
});

it("for statement with multiple members", () => {
const statement = new ImportStatement("test-module", ["TypeA", "TypeB", "helper"]);
expect(statement.toString()).toBe('import { TypeA, TypeB, helper } from "test-module";');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ export const associationWebOutput = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { ListAttributeValue, ListValue, ReferenceSetValue, ReferenceValue } from "mendix";
import { CSSProperties } from "react";
import { ListValue, ListAttributeValue, ReferenceValue, ReferenceSetValue } from "mendix";

export interface MyWidgetContainerProps {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ export const containmentWebOutput = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { ComponentType, CSSProperties, ReactNode } from "react";
import { ActionValue, EditableValue } from "mendix";
import { ComponentType, CSSProperties, ReactNode } from "react";

export interface MyWidgetContainerProps {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ export const datasourceWebOutput = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { ActionValue, EditableValue, ListActionValue, ListAttributeValue, ListExpressionValue, ListValue, ListWidgetValue } from "mendix";
import { ComponentType, ReactNode } from "react";
import { ActionValue, EditableValue, ListValue, ListActionValue, ListAttributeValue, ListExpressionValue, ListWidgetValue } from "mendix";
import { Big } from "big.js";

export interface DatasourcePropertiesType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ export const expressionWebOutput = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { CSSProperties } from "react";
import { DynamicValue, EditableValue, ListValue, ListExpressionValue } from "mendix";
import { DynamicValue, EditableValue, ListExpressionValue, ListValue } from "mendix";
import { Big } from "big.js";
import { CSSProperties } from "react";

export interface MyWidgetContainerProps {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const fileWebOutput = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { ActionValue, DynamicValue, EditableValue, EditableFileValue, FileValue } from "mendix";
import { ActionValue, DynamicValue, EditableFileValue, EditableValue, FileValue } from "mendix";

export interface MyWidgetContainerProps {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const imageWebOutput = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { ActionValue, DynamicValue, EditableValue, EditableImageValue, WebImage } from "mendix";
import { ActionValue, DynamicValue, EditableImageValue, EditableValue, WebImage } from "mendix";

export interface MyWidgetContainerProps {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ export const nativeResult = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { CSSProperties } from "react";
import { ActionValue, DynamicValue, EditableValue, FileValue, NativeImage } from "mendix";
import { Big } from "big.js";
import { CSSProperties } from "react";

export type BootstrapStyleEnum = "default" | "primary" | "success" | "info" | "inverse" | "warning" | "danger";

Expand Down Expand Up @@ -75,9 +75,9 @@ export const webResult = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { CSSProperties } from "react";
import { ActionValue, DynamicValue, EditableValue, FileValue, WebImage } from "mendix";
import { Big } from "big.js";
import { CSSProperties } from "react";

export type BootstrapStyleEnum = "default" | "primary" | "success" | "info" | "inverse" | "warning" | "danger";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ export const listAssociationWebOutput = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { ListAttributeValue, ListReferenceSetValue, ListReferenceValue, ListValue } from "mendix";
import { CSSProperties } from "react";
import { ListValue, ListAttributeValue, ListReferenceValue, ListReferenceSetValue } from "mendix";

export interface MyWidgetContainerProps {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ export const listAttributeWebOutput = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { ListAttributeListValue, ListAttributeValue, ListValue } from "mendix";
import { CSSProperties } from "react";
import { ListValue, ListAttributeValue, ListAttributeListValue } from "mendix";

export interface MyWidgetContainerProps {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ export const associationMetaDataWebOutput = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { CSSProperties } from "react";
import { AssociationMetaData, ListValue } from "mendix";
import { CSSProperties } from "react";

export interface MyWidgetContainerProps {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ export const attributeMetaDataWebOutput = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { CSSProperties } from "react";
import { AttributeMetaData, ListValue } from "mendix";
import { Big } from "big.js";
import { CSSProperties } from "react";

export interface MyWidgetContainerProps {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ export const nonLinkedListAttributeWebOutput = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { EditableListValue, EditableValue } from "mendix";
import { CSSProperties } from "react";
import { EditableValue, EditableListValue } from "mendix";

export interface MyWidgetContainerProps {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ export const selectionWebOutput = `/**
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { ListValue, SelectionMultiValue, SelectionSingleValue } from "mendix";
import { CSSProperties } from "react";
import { ListValue, SelectionSingleValue, SelectionMultiValue } from "mendix";

export interface MyWidgetContainerProps {
name: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
import { generateClientTypes } from "./generateClientTypes";
import { generateImports, ImportableModule } from "./generateImports";
import { generatePreviewTypes } from "./generatePreviewTypes";
import { extractProperties, extractSystemProperties } from "./helpers";
import { WidgetXml } from "./WidgetXml";

const mxExports = [
"ActionValue",
"AssociationMetaData",
"AttributeMetaData",
"DynamicValue",
"EditableValue",
"EditableListValue",
"EditableFileValue",
"EditableImageValue",
"FileValue",
"ListValue",
"NativeIcon",
"NativeImage",
"Option",
"ListActionValue",
"ListAttributeValue",
"ListAttributeListValue",
"ListExpressionValue",
"ListReferenceValue",
"ListReferenceSetValue",
"ListWidgetValue",
"ReferenceValue",
"ReferenceSetValue",
"SelectionSingleValue",
"SelectionMultiValue",
"WebIcon",
"WebImage"
const importableModules = [
new ImportableModule("mendix", [
"ActionValue",
"AssociationMetaData",
"AttributeMetaData",
"DynamicValue",
"EditableFileValue",
"EditableImageValue",
"EditableListValue",
"EditableValue",
"FileValue",
"ListActionValue",
"ListAttributeListValue",
"ListAttributeValue",
"ListExpressionValue",
"ListReferenceSetValue",
"ListReferenceValue",
"ListValue",
"ListWidgetValue",
"NativeIcon",
"NativeImage",
"Option",
"ReferenceSetValue",
"ReferenceValue",
"SelectionMultiValue",
"SelectionSingleValue",
"WebIcon",
"WebImage"
]),
new ImportableModule("react", ["ComponentType", "CSSProperties", "ReactNode"]),
new ImportableModule("big.js", ["Big"])
];

export function generateForWidget(widgetXml: WidgetXml, widgetName: string) {
Expand All @@ -55,24 +60,15 @@ export function generateForWidget(widgetXml: WidgetXml, widgetName: string) {
.concat([clientTypes[clientTypes.length - 1], modelerTypes[modelerTypes.length - 1]])
.join("\n\n");

const imports = [
generateImport("react", generatedTypesCode, ["ComponentType", "CSSProperties", "ReactNode"]),
generateImport("mendix", generatedTypesCode, mxExports),
generateImport("big.js", generatedTypesCode, ["Big"])
]
.filter(line => line)
.join("\n");
const imports = generateImports(importableModules, generatedTypesCode);

return `/**
* This file was generated from ${widgetName}.xml
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
${imports.length ? imports + "\n\n" : ""}${generatedTypesCode}
${imports.length ? imports.join("\n") + "\n\n" : ""}${generatedTypesCode}
`;
}

function generateImport(from: string, code: string, availableNames: string[]) {
const usedNames = availableNames.filter(type => new RegExp(`\\W${type}\\W`).test(code));
return usedNames.length ? `import { ${usedNames.join(", ")} } from "${from}";` : "";
}

Loading
Loading