Skip to content

Commit 8ed842c

Browse files
authored
Package xml improvements (#1872)
2 parents caef300 + cf3a936 commit 8ed842c

File tree

39 files changed

+282
-38
lines changed

39 files changed

+282
-38
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env ts-node
2+
3+
import { writeClientPackageXml, ClientPackageXML } from "../src/package-xml-v2";
4+
import { getWidgetInfo } from "../src/package-info";
5+
import { readPropertiesFile } from "../src/package-xml-v2/properties-xml";
6+
import path from "node:path";
7+
import { existsSync } from "node:fs";
8+
9+
async function generatePackageXml(): Promise<void> {
10+
const widgetDir = process.cwd();
11+
12+
// Read package.json info
13+
const packageInfo = await getWidgetInfo(widgetDir);
14+
15+
const srcDir = path.join(widgetDir, "src");
16+
17+
// Create src directory if it doesn't exist
18+
if (!existsSync(srcDir)) {
19+
throw new Error(`Src folder not found: ${srcDir}`);
20+
}
21+
22+
// Get properties file name from package.json (mxpackage.name + ".xml")
23+
const propertiesFileName = packageInfo.mxpackage.name + ".xml";
24+
const propertiesFilePath = path.join(srcDir, propertiesFileName);
25+
26+
// Properties file must exist
27+
if (!existsSync(propertiesFilePath)) {
28+
throw new Error(`Properties file not found: ${propertiesFilePath}`);
29+
}
30+
31+
// Read properties file and extract widget ID
32+
const propertiesXml = await readPropertiesFile(propertiesFilePath);
33+
const widgetId = propertiesXml.widget["@_id"];
34+
35+
// Generate ClientPackageXML structure
36+
const clientPackageXml: ClientPackageXML = {
37+
name: packageInfo.mxpackage.name,
38+
version: packageInfo.version,
39+
widgetFiles: [packageInfo.mxpackage.name + ".xml"],
40+
files: [widgetId.split(".").slice(0, -1).join("/") + "/"]
41+
};
42+
43+
// Write the generated package.xml
44+
const packageXmlPath = path.join(srcDir, "package.xml");
45+
await writeClientPackageXml(packageXmlPath, clientPackageXml);
46+
}
47+
48+
async function main() {
49+
try {
50+
await generatePackageXml();
51+
} catch (error) {
52+
console.error("Error generating package.xml:", error instanceof Error ? error.message : String(error));
53+
process.exit(1);
54+
}
55+
}
56+
57+
if (require.main === module) {
58+
main();
59+
}

automation/utils/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"rui-agent-rules": "bin/rui-agent-rules.ts",
99
"rui-create-gh-release": "bin/rui-create-gh-release.ts",
1010
"rui-create-translation": "bin/rui-create-translation.ts",
11+
"rui-generate-package-xml": "bin/rui-generate-package-xml.ts",
1112
"rui-prepare-release": "bin/rui-prepare-release.ts",
1213
"rui-publish-marketplace": "bin/rui-publish-marketplace.ts",
1314
"rui-update-changelog-module": "bin/rui-update-changelog-module.ts",

automation/utils/src/package-info.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export const PackageSchema = z.object({
8686
appNumber: true
8787
}),
8888
repository: RepositorySchema,
89-
testProject: TestProjectSchema
89+
testProject: TestProjectSchema.optional()
9090
});
9191

9292
export const PublishedPackageSchema = PackageSchema.extend({
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { XMLBuilder, XMLParser } from "fast-xml-parser";
2+
import { readFile, writeFile } from "fs/promises";
3+
import { Version, VersionString } from "../version";
4+
import { ClientModulePackageFile } from "./schema";
5+
6+
export function xmlTextToXmlJson(xmlText: string | Buffer): unknown {
7+
const parser = new XMLParser({ ignoreAttributes: false });
8+
return parser.parse(xmlText);
9+
}
10+
11+
export function xmlJsonToXmlText(xmlObject: any): string {
12+
const builder = new XMLBuilder({
13+
ignoreAttributes: false,
14+
format: true,
15+
indentBy: " ",
16+
suppressEmptyNode: true
17+
});
18+
return builder
19+
.build(xmlObject)
20+
.replaceAll(/(<[^>]*?)\/>/g, "$1 />") // Add space before /> in self-closing tags
21+
.replaceAll(/(<\?[^>]*?)\?>/g, "$1 ?>"); // Add space before ?> in XML declarations
22+
}
23+
24+
export interface ClientPackageXML {
25+
name: string;
26+
version: Version;
27+
widgetFiles: string[];
28+
files: string[];
29+
}
30+
31+
export async function readClientPackageXml(path: string): Promise<ClientPackageXML> {
32+
return parseClientPackageXml(ClientModulePackageFile.passthrough().parse(xmlTextToXmlJson(await readFile(path))));
33+
}
34+
35+
export async function writeClientPackageXml(path: string, data: ClientPackageXML): Promise<void> {
36+
await writeFile(path, xmlJsonToXmlText(buildClientPackageXml(data)));
37+
}
38+
39+
function parseClientPackageXml(xmlJson: ClientModulePackageFile): ClientPackageXML {
40+
const clientModule = xmlJson?.package?.clientModule ?? {};
41+
const widgetFilesNode = clientModule.widgetFiles !== "" ? clientModule.widgetFiles?.widgetFile : undefined;
42+
const filesNode = clientModule.files !== "" ? clientModule.files?.file : undefined;
43+
44+
const extractPaths = (node: any): string[] => {
45+
if (!node) return [];
46+
if (Array.isArray(node)) {
47+
return node.map((item: any) => item["@_path"]);
48+
}
49+
return [node["@_path"]];
50+
};
51+
52+
const name = clientModule["@_name"] ?? "";
53+
const versionString = clientModule["@_version"] ?? "1.0.0";
54+
55+
return {
56+
name,
57+
version: Version.fromString(versionString as VersionString),
58+
widgetFiles: extractPaths(widgetFilesNode),
59+
files: extractPaths(filesNode)
60+
};
61+
}
62+
63+
function buildClientPackageXml(clientPackage: ClientPackageXML): ClientModulePackageFile {
64+
const toXmlNode = <T extends "file" | "widgetFile">(arr: string[], tag: T) => {
65+
if (arr.length === 0) return "";
66+
if (arr.length === 1) {
67+
return { [tag]: { "@_path": arr[0] } };
68+
}
69+
return { [tag]: arr.map(path => ({ "@_path": path })) };
70+
};
71+
72+
return {
73+
"?xml": {
74+
"@_version": "1.0",
75+
"@_encoding": "utf-8"
76+
},
77+
package: {
78+
clientModule: {
79+
widgetFiles: toXmlNode(
80+
clientPackage.widgetFiles,
81+
"widgetFile"
82+
) as ClientModulePackageFile["package"]["clientModule"]["widgetFiles"],
83+
files: toXmlNode(
84+
clientPackage.files,
85+
"file"
86+
) as ClientModulePackageFile["package"]["clientModule"]["files"],
87+
"@_name": clientPackage.name,
88+
"@_version": clientPackage.version.format(),
89+
"@_xmlns": "http://www.mendix.com/clientModule/1.0/"
90+
},
91+
"@_xmlns": "http://www.mendix.com/package/1.0/"
92+
}
93+
};
94+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { z } from "zod";
2+
import { xmlTextToXmlJson } from "./index";
3+
import { readFile } from "node:fs/promises";
4+
5+
export const PropertiesXMLFile = z.object({
6+
"?xml": z.object({
7+
"@_version": z.literal("1.0"),
8+
"@_encoding": z.literal("utf-8")
9+
}),
10+
widget: z.object({
11+
"@_id": z.string(),
12+
"@_xmlns": z.literal("http://www.mendix.com/widget/1.0/"),
13+
"@_xmlns:xsi": z.literal("http://www.w3.org/2001/XMLSchema-instance")
14+
})
15+
});
16+
17+
type PropertiesXMLFile = z.infer<typeof PropertiesXMLFile>;
18+
19+
export async function readPropertiesFile(filePath: string): Promise<PropertiesXMLFile> {
20+
return PropertiesXMLFile.passthrough().parse(xmlTextToXmlJson(await readFile(filePath, "utf-8")));
21+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { z } from "zod";
2+
3+
const FileTag = z.object({
4+
"@_path": z.string()
5+
});
6+
7+
const FileNode = z.union([FileTag, FileTag.array()]);
8+
9+
export const ModelerProjectPackageFile = z.object({
10+
"?xml": z.object({
11+
"@_version": z.literal("1.0"),
12+
"@_encoding": z.literal("utf-8")
13+
}),
14+
package: z.object({
15+
"@_xmlns": z.literal("http://www.mendix.com/package/1.0/"),
16+
modelerProject: z.object({
17+
"@_xmlns": z.literal("http://www.mendix.com/modelerProject/1.0/"),
18+
module: z.object({
19+
"@_name": z.string()
20+
}),
21+
projectFile: z.object({
22+
"@_path": z.string()
23+
}),
24+
files: z.union([
25+
z.literal(""),
26+
z.object({
27+
file: FileNode
28+
})
29+
])
30+
})
31+
})
32+
});
33+
34+
export type ModelerProjectPackageFile = z.infer<typeof ModelerProjectPackageFile>;
35+
36+
export const ClientModulePackageFile = z.object({
37+
"?xml": z.object({
38+
"@_version": z.literal("1.0"),
39+
"@_encoding": z.literal("utf-8")
40+
}),
41+
package: z.object({
42+
"@_xmlns": z.literal("http://www.mendix.com/package/1.0/"),
43+
clientModule: z.object({
44+
"@_name": z.string({
45+
required_error: "name attribute is required"
46+
}),
47+
"@_version": z.string({
48+
required_error: "version attribute is required"
49+
}),
50+
"@_xmlns": z.literal("http://www.mendix.com/clientModule/1.0/"),
51+
52+
files: z.union([
53+
z.literal(""),
54+
z.object({
55+
file: FileNode
56+
})
57+
]),
58+
59+
widgetFiles: z.union([
60+
z.literal(""),
61+
z.object({
62+
widgetFile: FileNode
63+
})
64+
])
65+
})
66+
})
67+
});
68+
69+
export type ClientModulePackageFile = z.infer<typeof ClientModulePackageFile>;

automation/utils/src/steps.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ export async function cloneTestProject({ info, config }: CommonStepParams): Prom
5050
const clone = process.env.CI ? cloneRepoShallow : cloneRepo;
5151
rm("-rf", config.paths.targetProject);
5252
await clone({
53-
remoteUrl: testProject.githubUrl,
54-
branch: testProject.branchName,
53+
remoteUrl: testProject!.githubUrl,
54+
branch: testProject!.branchName,
5555
localFolder: config.paths.targetProject
5656
});
5757
}

packages/pluggableWidgets/accessibility-helper-web/src/package.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<widgetFile path="AccessibilityHelper.xml" />
66
</widgetFiles>
77
<files>
8-
<file path="com/mendix/widget/web/accessibilityhelper" />
8+
<file path="com/mendix/widget/web/accessibilityhelper/" />
99
</files>
1010
</clientModule>
1111
</package>

packages/pluggableWidgets/accordion-web/src/package.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<widgetFile path="Accordion.xml" />
66
</widgetFiles>
77
<files>
8-
<file path="com/mendix/widget/web/accordion" />
8+
<file path="com/mendix/widget/web/accordion/" />
99
</files>
1010
</clientModule>
1111
</package>

packages/pluggableWidgets/badge-web/src/package.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<widgetFile path="Badge.xml" />
66
</widgetFiles>
77
<files>
8-
<file path="com/mendix/widget/custom/badge" />
8+
<file path="com/mendix/widget/custom/badge/" />
99
</files>
1010
</clientModule>
1111
</package>

0 commit comments

Comments
 (0)