From 817207a9239d65c4ef450f94a53201069dc5dd99 Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Thu, 28 Aug 2025 11:41:18 +0900 Subject: [PATCH 01/10] feat: add fast-xml-parser dependency --- package.json | 1 + pnpm-lock.yaml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/package.json b/package.json index f714d3c..9a229e6 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@vitejs/plugin-react-swc": "4.0.1", "chalk": "5.6.0", "commander": "14.0.0", + "fast-xml-parser": "^5.2.5", "vite": "7.1.3", "zip-a-folder": "3.1.9" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52098b6..f730f81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: commander: specifier: 14.0.0 version: 14.0.0 + fast-xml-parser: + specifier: ^5.2.5 + version: 5.2.5 vite: specifier: 7.1.3 version: 7.1.3(@types/node@22.17.2)(jiti@2.5.1) @@ -697,6 +700,10 @@ packages: fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-xml-parser@5.2.5: + resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} + hasBin: true + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -928,6 +935,9 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strnum@2.1.1: + resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} + tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} @@ -1548,6 +1558,10 @@ snapshots: fast-fifo@1.3.2: {} + fast-xml-parser@5.2.5: + dependencies: + strnum: 2.1.1 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -1784,6 +1798,8 @@ snapshots: strip-bom@3.0.0: {} + strnum@2.1.1: {} + tar-stream@3.1.7: dependencies: b4a: 1.6.7 From b83adc97bdf3b9f68fefb527e4f0b421ff20b387 Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Thu, 28 Aug 2025 12:33:06 +0900 Subject: [PATCH 02/10] feat: add mendix on dependency --- package.json | 3 ++- pnpm-lock.yaml | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a229e6..0c4bfdf 100644 --- a/package.json +++ b/package.json @@ -38,13 +38,14 @@ "@rslib/core": "0.12.2", "@types/node": "22.17.2", "type-fest": "4.41.0", - "typescript": "^5.9.2" + "typescript": "5.9.2" }, "dependencies": { "@vitejs/plugin-react-swc": "4.0.1", "chalk": "5.6.0", "commander": "14.0.0", "fast-xml-parser": "^5.2.5", + "mendix": "10.24.77222", "vite": "7.1.3", "zip-a-folder": "3.1.9" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f730f81..aaa1af8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: fast-xml-parser: specifier: ^5.2.5 version: 5.2.5 + mendix: + specifier: ^10.24.77222 + version: 10.24.77222 vite: specifier: 7.1.3 version: 7.1.3(@types/node@22.17.2)(jiti@2.5.1) @@ -572,12 +575,24 @@ packages: '@tybys/wasm-util@0.10.0': resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@types/big.js@6.2.2': + resolution: {integrity: sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/node@22.17.2': resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react@18.0.38': + resolution: {integrity: sha512-ExsidLLSzYj4cvaQjGnQCk4HFfVT9+EZ9XZsQ8Hsrcn8QNgXtpZ3m9vSIC2MWtx7jHictK6wYhQgGh6ic58oOw==} + + '@types/scheduler@0.26.0': + resolution: {integrity: sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==} + '@vitejs/plugin-react-swc@4.0.1': resolution: {integrity: sha512-NQhPjysi5duItyrMd5JWZFf2vNOuSMyw+EoZyTBDzk+DkfYD8WNrsUs09sELV2cr1P15nufsN25hsUBt4CKF9Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -675,6 +690,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -795,6 +813,9 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + mendix@10.24.77222: + resolution: {integrity: sha512-sxDa9DrGFJY65bE1XbREozEUWmYKZUc+cmATxq61mK8qMGININaN0+qCat/2Qb6iIMlcid8JUxUL7szDgRonYQ==} + minimatch@10.0.3: resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} engines: {node: 20 || >=22} @@ -1412,12 +1433,24 @@ snapshots: tslib: 2.8.1 optional: true + '@types/big.js@6.2.2': {} + '@types/estree@1.0.8': {} '@types/node@22.17.2': dependencies: undici-types: 6.21.0 + '@types/prop-types@15.7.15': {} + + '@types/react@18.0.38': + dependencies: + '@types/prop-types': 15.7.15 + '@types/scheduler': 0.26.0 + csstype: 3.1.3 + + '@types/scheduler@0.26.0': {} + '@vitejs/plugin-react-swc@4.0.1(@swc/helpers@0.5.17)(vite@7.1.3(@types/node@22.17.2)(jiti@2.5.1))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.32 @@ -1517,6 +1550,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.1.3: {} + eastasianwidth@0.2.0: {} emoji-regex@8.0.0: {} @@ -1640,6 +1675,11 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + mendix@10.24.77222: + dependencies: + '@types/big.js': 6.2.2 + '@types/react': 18.0.38 + minimatch@10.0.3: dependencies: '@isaacs/brace-expansion': 5.0.0 From 06f236b18bf06c5451640d0c654965a3e58ec9b8 Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Thu, 28 Aug 2025 12:33:29 +0900 Subject: [PATCH 03/10] fix: remove caret on fast-xml-parser version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0c4bfdf..2832cb8 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@vitejs/plugin-react-swc": "4.0.1", "chalk": "5.6.0", "commander": "14.0.0", - "fast-xml-parser": "^5.2.5", + "fast-xml-parser": "5.2.5", "mendix": "10.24.77222", "vite": "7.1.3", "zip-a-folder": "3.1.9" From 5dbd07dbf2408107c7ce5f97069f861e97892c03 Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Thu, 28 Aug 2025 12:44:54 +0900 Subject: [PATCH 04/10] feat: add copy widget schema script --- .gitignore | 5 ++++- package.json | 3 ++- pnpm-lock.yaml | 6 +++--- tools/copy-widget-schema.js | 20 ++++++++++++++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 tools/copy-widget-schema.js diff --git a/.gitignore b/.gitignore index 5b9e049..0e50763 100644 --- a/.gitignore +++ b/.gitignore @@ -222,4 +222,7 @@ $RECYCLE.BIN/ # Temp directory /tmp -# End of https://www.toptal.com/developers/gitignore/api/windows,macos,node,visualstudiocode \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/windows,macos,node,visualstudiocode + +# Mendix Widget schema file +custom_widget.xsd \ No newline at end of file diff --git a/package.json b/package.json index 2832cb8..00c62ed 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "build": "rslib build", "watch": "rslib build --watch", "start": "pnpm build && node ./dist/cli.js", - "package": "pnpm build && pnpm pack" + "package": "pnpm build && pnpm pack", + "postinstall": "node ./tools/copy-widget-schema.js" }, "main": "dist/index.cjs", "module": "dist/index.mjs", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aaa1af8..19278cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,10 +18,10 @@ importers: specifier: 14.0.0 version: 14.0.0 fast-xml-parser: - specifier: ^5.2.5 + specifier: 5.2.5 version: 5.2.5 mendix: - specifier: ^10.24.77222 + specifier: 10.24.77222 version: 10.24.77222 vite: specifier: 7.1.3 @@ -40,7 +40,7 @@ importers: specifier: 4.41.0 version: 4.41.0 typescript: - specifier: ^5.9.2 + specifier: 5.9.2 version: 5.9.2 packages: diff --git a/tools/copy-widget-schema.js b/tools/copy-widget-schema.js new file mode 100644 index 0000000..9fafc2d --- /dev/null +++ b/tools/copy-widget-schema.js @@ -0,0 +1,20 @@ +const fs = require('fs'); +const path = require('path'); + +const sourcePath = path.join(__dirname, '..', 'node_modules', 'mendix', 'custom_widget.xsd'); +const targetPath = path.join(__dirname, '..', 'custom_widget.xsd'); + +try { + if (fs.existsSync(sourcePath)) { + fs.copyFileSync(sourcePath, targetPath); + + console.log('Successfully copied custom_widget.xsd to project root'); + } else { + console.warn('custom_widget.xsd not found in node_modules/mendix'); + console.warn('Make sure mendix package is installed'); + } +} catch (error) { + console.error('Failed to copy custom_widget.xsd:', error.message); + + process.exit(1); +} \ No newline at end of file From 5f636d47eb0ae61492a89deceb245332770093c5 Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Thu, 28 Aug 2025 12:45:18 +0900 Subject: [PATCH 05/10] feat: add custom_widget.xsd on files --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 00c62ed..7f6c31c 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "dist/**/*", "package.json", "LICENSE", - "src/configurations/hotReload/**/*" + "src/configurations/hotReload/**/*", + "custom_widget.xsd" ], "publishConfig": { "access": "public", From e385ed41d863b2ac4e73c4b5a4a2cff8a1058ff7 Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Thu, 28 Aug 2025 14:39:52 +0900 Subject: [PATCH 06/10] feat: add type generator --- src/type-generator/generator.ts | 146 ++++++++++++ src/type-generator/header.ts | 37 +++ src/type-generator/index.ts | 25 ++ src/type-generator/mendix-types.ts | 347 ++++++++++++++++++++++++++++ src/type-generator/parser.ts | 194 ++++++++++++++++ src/type-generator/preview-types.ts | 221 ++++++++++++++++++ src/type-generator/system-props.ts | 87 +++++++ src/type-generator/types.ts | 174 ++++++++++++++ src/type-generator/utils.ts | 88 +++++++ 9 files changed, 1319 insertions(+) create mode 100644 src/type-generator/generator.ts create mode 100644 src/type-generator/header.ts create mode 100644 src/type-generator/index.ts create mode 100644 src/type-generator/mendix-types.ts create mode 100644 src/type-generator/parser.ts create mode 100644 src/type-generator/preview-types.ts create mode 100644 src/type-generator/system-props.ts create mode 100644 src/type-generator/types.ts create mode 100644 src/type-generator/utils.ts diff --git a/src/type-generator/generator.ts b/src/type-generator/generator.ts new file mode 100644 index 0000000..6f42a7c --- /dev/null +++ b/src/type-generator/generator.ts @@ -0,0 +1,146 @@ +import type { + WidgetDefinition, + Property, + PropertyGroup, + SystemProperty, +} from './types'; +import type { GenerateTargetPlatform } from './mendix-types'; +import { + mapPropertyTypeToTS, + pascalCase, + sanitizePropertyKey, + formatDescription, +} from './utils'; +import { getMendixImports, generateMendixImports } from './mendix-types'; +import { + extractSystemProperties, + hasLabelProperty, + generateSystemProps, + getSystemPropsImports +} from './system-props'; +import { generateHeaderComment } from './header'; + +export function generateTypeDefinition(widget: WidgetDefinition, target: GenerateTargetPlatform): string { + const interfaceName = generateInterfaceName(widget.name); + const properties = extractAllProperties(widget.properties); + const systemProps = extractSystemProperties(widget.properties); + const hasLabel = hasLabelProperty(systemProps); + const widgetProperties = properties.filter(p => !isSystemProperty(p)) as Property[]; + + let output = ''; + + output += generateHeaderComment(); + + const imports = getMendixImports(widgetProperties, target); + const systemImports = getSystemPropsImports({ hasLabel, platform: target }); + const allImports = [...imports, ...systemImports]; + const importStatements = generateMendixImports(allImports); + + if (importStatements) { + output += importStatements + '\n'; + } + + output += generateJSDoc(widget); + output += `export interface ${interfaceName} {\n`; + + const systemPropsLines = generateSystemProps({ hasLabel, platform: target }); + + for (const line of systemPropsLines) { + output += ` ${line}\n`; + } + + if (systemPropsLines.length > 0 && widgetProperties.length > 0) { + output += `\n // Widget properties\n`; + } + + for (const property of widgetProperties) { + output += generatePropertyDefinition(property, target); + } + + output += '}\n'; + + return output; +} + +function generateInterfaceName(widgetName: string): string { + return `${pascalCase(widgetName)}Props`; +} + +export function extractAllProperties(properties: PropertyGroup[] | Property[]): (Property | SystemProperty)[] { + const result: (Property | SystemProperty)[] = []; + + for (const item of properties) { + if (isPropertyGroup(item)) { + result.push(...item.properties); + } else { + result.push(item); + } + } + + return result; +} + +function isPropertyGroup(item: PropertyGroup | Property | SystemProperty): item is PropertyGroup { + return 'caption' in item && 'properties' in item; +} + +function isSystemProperty(item: Property | SystemProperty): item is SystemProperty { + return !('type' in item) && 'key' in item; +} + +function generateJSDoc(widget: WidgetDefinition): string { + let jsDoc = '/**\n'; + jsDoc += ` * Props for ${widget.name}\n`; + + if (widget.description) { + jsDoc += ` * ${formatDescription(widget.description)}\n`; + } + + if (widget.needsEntityContext) { + jsDoc += ` * @needsEntityContext true\n`; + } + + if (widget.supportedPlatform && widget.supportedPlatform !== 'Web') { + jsDoc += ` * @platform ${widget.supportedPlatform}\n`; + } + + jsDoc += ' */\n'; + return jsDoc; +} + +function generatePropertyDefinition(property: Property, target: GenerateTargetPlatform): string { + const indent = ' '; + let output = ''; + + if (property.description) { + output += `${indent}/**\n`; + output += `${indent} * ${formatDescription(property.description)}\n`; + + if (property.caption && property.caption !== property.description) { + output += `${indent} * @caption ${property.caption}\n`; + } + + if (property.defaultValue !== undefined && property.defaultValue !== '') { + output += `${indent} * @default ${property.defaultValue}\n`; + } + + if (property.type === 'attribute' && property.attributeTypes) { + output += `${indent} * @attributeTypes ${property.attributeTypes.join(', ')}\n`; + } + + if (property.type === 'enumeration' && property.enumerationValues) { + const values = property.enumerationValues.map(ev => ev.key).join(', '); + output += `${indent} * @enum {${values}}\n`; + } + + output += `${indent} */\n`; + } + + const propertyKey = sanitizePropertyKey(property.key); + const optional = property.required === false ? '?' : ''; + const propertyType = mapPropertyTypeToTS(property, target); + + output += `${indent}${propertyKey}${optional}: ${propertyType};\n`; + + return output; +} \ No newline at end of file diff --git a/src/type-generator/header.ts b/src/type-generator/header.ts new file mode 100644 index 0000000..0ff29ba --- /dev/null +++ b/src/type-generator/header.ts @@ -0,0 +1,37 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; + +export function getPackageVersion(): string { + try { + const packageJsonPath = join(process.cwd(), 'node_modules', '@repixelcorp', 'hyper-pwt', 'package.json'); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + + return packageJson.version || 'unknown'; + } catch { + try { + const currentDir = process.cwd(); + const packageJsonPath = join(currentDir, 'package.json'); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + + if (packageJson.name === '@repixelcorp/hyper-pwt') { + return packageJson.version || 'unknown'; + } + } catch { + return 'unknown'; + } + } +} + +export function generateHeaderComment(): string { + const version = getPackageVersion(); + + return `/** + * This file was automatically generated by @repixelcorp/hyper-pwt v${version} + * DO NOT MODIFY THIS FILE DIRECTLY + * + * To regenerate this file, run the type generator with your widget XML file. + * Any manual changes to this file will be lost when the types are regenerated. + */ + +`; +} \ No newline at end of file diff --git a/src/type-generator/index.ts b/src/type-generator/index.ts new file mode 100644 index 0000000..d9cc96c --- /dev/null +++ b/src/type-generator/index.ts @@ -0,0 +1,25 @@ +import { readFile } from 'fs/promises'; +import { parseWidgetXML } from './parser'; +import { generateTypeDefinition } from './generator'; +import { generatePreviewTypeDefinition } from './preview-types'; +import type { GenerateTargetPlatform } from './mendix-types'; + +export { parseWidgetXML } from './parser'; +export { generateTypeDefinition } from './generator'; +export { generatePreviewTypeDefinition } from './preview-types'; +export type { WidgetDefinition, Property, PropertyGroup, PropertyType } from './types'; + +export function generateTypes(xmlContent: string, target: GenerateTargetPlatform): string { + const widget = parseWidgetXML(xmlContent); + let output = generateTypeDefinition(widget, target); + + output += '\n' + generatePreviewTypeDefinition(widget); + + return output; +} + +export async function generateTypesFromFile(filePath: string, target: GenerateTargetPlatform): Promise { + const xmlContent = await readFile(filePath, 'utf-8'); + + return generateTypes(xmlContent, target); +} \ No newline at end of file diff --git a/src/type-generator/mendix-types.ts b/src/type-generator/mendix-types.ts new file mode 100644 index 0000000..6202b37 --- /dev/null +++ b/src/type-generator/mendix-types.ts @@ -0,0 +1,347 @@ +import type { + Property, + AttributeType, +} from './types'; + +export interface MendixTypeMapping { + type: string; + imports: Set; +} + +export type GenerateTargetPlatform = 'web' | 'native'; + +export function getMendixImports(properties: Property[], target: GenerateTargetPlatform): string[] { + const imports = new Set(); + + for (const property of properties) { + const mapping = mapPropertyToMendixType(property, target); + + mapping.imports.forEach(imp => imports.add(imp)); + } + + return Array.from(imports).sort(); +} + +export function mapPropertyToMendixType(property: Property, platform: GenerateTargetPlatform = 'web'): MendixTypeMapping { + const imports = new Set(); + let type: string; + + switch (property.type) { + case 'string': + case 'translatableString': + type = 'string'; + break; + + case 'boolean': + type = 'boolean'; + break; + + case 'integer': + type = 'number'; + break; + + case 'decimal': + imports.add('Big'); + type = 'Big'; + break; + + case 'textTemplate': + imports.add('DynamicValue'); + if (property.dataSource) { + imports.add('ListExpressionValue'); + type = 'ListExpressionValue'; + } else { + type = 'DynamicValue'; + } + break; + + case 'action': + if (property.dataSource) { + imports.add('ListActionValue'); + type = 'ListActionValue'; + } else { + imports.add('ActionValue'); + type = 'ActionValue'; + } + break; + + case 'microflow': + case 'nanoflow': + imports.add('ActionValue'); + type = 'ActionValue'; + break; + + case 'attribute': + type = mapAttributeToMendixType(property, imports); + break; + + case 'expression': + type = mapExpressionToMendixType(property, imports); + break; + + case 'datasource': + imports.add('ListValue'); + type = 'ListValue'; + break; + + case 'icon': + imports.add('DynamicValue'); + if (platform === 'native') { + imports.add('NativeIcon'); + type = 'DynamicValue'; + } else if (platform === 'web') { + imports.add('WebIcon'); + type = 'DynamicValue'; + } else { + imports.add('WebIcon'); + imports.add('NativeIcon'); + type = 'DynamicValue'; + } + break; + + case 'image': + imports.add('DynamicValue'); + if (platform === 'native') { + imports.add('NativeImage'); + type = 'DynamicValue'; + } else if (platform === 'web') { + imports.add('WebImage'); + type = 'DynamicValue'; + } else { + imports.add('WebImage'); + imports.add('NativeImage'); + type = 'DynamicValue'; + } + break; + + case 'file': + imports.add('DynamicValue'); + imports.add('FileValue'); + type = 'DynamicValue'; + break; + + case 'widgets': + if (property.dataSource) { + imports.add('ListWidgetValue'); + type = 'ListWidgetValue'; + } else { + imports.add('ReactNode'); + type = 'ReactNode'; + } + break; + + case 'object': + if (property.properties && property.properties.length > 0) { + type = generateObjectInterface(property); + } else { + type = 'object'; + } + break; + + case 'entity': + imports.add('ObjectItem'); + type = 'ObjectItem'; + break; + + case 'entityConstraint': + type = 'string'; + break; + + case 'enumeration': + if (property.enumerationValues && property.enumerationValues.length > 0) { + type = property.enumerationValues.map(ev => `"${ev.key}"`).join(' | '); + } else { + type = 'string'; + } + break; + + case 'association': + type = mapAssociationToMendixType(property, imports); + break; + + case 'selection': + type = mapSelectionToMendixType(property, imports); + break; + + case 'form': + type = 'string'; + break; + + default: + type = 'any'; + } + + if (property.isList && !['datasource', 'widgets'].includes(property.type)) { + type = `${type}[]`; + } + + return { type, imports }; +} + +function mapAttributeToMendixType(property: Property, imports: Set): string { + const baseType = getAttributeBaseType(property.attributeTypes || []); + + if (property.dataSource) { + imports.add('ListAttributeValue'); + + return `ListAttributeValue<${baseType}>`; + } else { + imports.add('EditableValue'); + + return `EditableValue<${baseType}>`; + } +} + +function mapExpressionToMendixType(property: Property, imports: Set): string { + const baseType = property.returnType ? mapReturnTypeToTS(property.returnType.type) : 'string'; + + if (property.dataSource) { + imports.add('ListExpressionValue'); + + const typeStr = property.returnType?.isList ? `${baseType}[]` : baseType; + + return `ListExpressionValue<${typeStr}>`; + } else { + imports.add('DynamicValue'); + + const typeStr = property.returnType?.isList ? `${baseType}[]` : baseType; + + return `DynamicValue<${typeStr}>`; + } +} + +function mapAssociationToMendixType(property: Property, imports: Set): string { + if (!property.associationTypes || property.associationTypes.length === 0) { + imports.add('ObjectItem'); + + return 'ObjectItem'; + } + + const assocType = property.associationTypes[0]; + + if (assocType === 'Reference') { + if (property.dataSource) { + imports.add('ListReferenceValue'); + + return 'ListReferenceValue'; + } else { + imports.add('ReferenceValue'); + + return 'ReferenceValue'; + } + } else if (assocType === 'ReferenceSet') { + if (property.dataSource) { + imports.add('ListReferenceSetValue'); + + return 'ListReferenceSetValue'; + } else { + imports.add('ReferenceSetValue'); + + return 'ReferenceSetValue'; + } + } + + imports.add('ObjectItem'); + + return 'ObjectItem'; +} + +function mapSelectionToMendixType(property: Property, imports: Set): string { + if (!property.selectionTypes || property.selectionTypes.length === 0) { + imports.add('SelectionSingleValue'); + + return 'SelectionSingleValue'; + } + + const selectionType = property.selectionTypes[0]; + + if (selectionType === 'Multi') { + imports.add('SelectionMultiValue'); + + return 'SelectionMultiValue'; + } else { + imports.add('SelectionSingleValue'); + + return 'SelectionSingleValue'; + } +} + +function getAttributeBaseType(attributeTypes: AttributeType[]): string { + if (attributeTypes.length === 0) return 'any'; + + const types = attributeTypes.map(type => { + switch (type) { + case 'String': + case 'HashString': + case 'Enum': + return 'string'; + case 'Boolean': + return 'boolean'; + case 'Integer': + case 'Long': + case 'AutoNumber': + case 'Float': + case 'Currency': + return 'number'; + case 'Decimal': + return 'Big'; + case 'DateTime': + return 'Date'; + case 'Binary': + return 'string'; + default: + return 'any'; + } + }); + + const uniqueTypes = Array.from(new Set(types)); + return uniqueTypes.length === 1 ? uniqueTypes[0] : uniqueTypes.join(' | '); +} + +function mapReturnTypeToTS(returnType: string): string { + switch (returnType) { + case 'Void': + return 'void'; + case 'Boolean': + return 'boolean'; + case 'Integer': + case 'Float': + return 'number'; + case 'Decimal': + return 'Big'; + case 'DateTime': + return 'Date'; + case 'String': + return 'string'; + case 'Object': + return 'object'; + default: + return 'any'; + } +} + +function generateObjectInterface(property: Property): string { + return `${property.key}Type`; +} + +export function generateMendixImports(imports: string[]): string { + if (imports.length === 0) return ''; + + const mendixImports = imports.filter(imp => + !['ReactNode'].includes(imp) + ); + + const reactImports = imports.filter(imp => imp === 'ReactNode'); + + let output = ''; + + if (reactImports.length > 0) { + output += `import { ${reactImports.join(', ')} } from 'react';\n`; + } + + if (mendixImports.length > 0) { + output += `import { ${mendixImports.join(', ')} } from 'mendix';\n`; + } + + return output; +} \ No newline at end of file diff --git a/src/type-generator/parser.ts b/src/type-generator/parser.ts new file mode 100644 index 0000000..3b8bc77 --- /dev/null +++ b/src/type-generator/parser.ts @@ -0,0 +1,194 @@ +import { XMLParser } from 'fast-xml-parser'; +import type { + WidgetDefinition, + Property, + PropertyGroup, + SystemProperty, + AttributeType, + AssociationType, + SelectionType, + EnumerationValue, + ParsedXMLWidget, + ParsedXMLProperty, + ParsedXMLPropertyGroup, + ParsedXMLSystemProperty, + ParsedXMLAttributeType, + ParsedXMLAssociationType, + ParsedXMLSelectionType, + ParsedXMLEnumerationValue, +} from './types'; +import { ensureArray } from './utils'; + +const parserOptions = { + ignoreAttributes: false, + attributeNamePrefix: '', + textNodeName: '_', + parseAttributeValue: false, + trimValues: true, + parseTrueNumberOnly: false, + parseTagValue: false, + allowBooleanAttributes: true, +}; + +export function parseWidgetXML(xmlContent: string): WidgetDefinition { + const parser = new XMLParser(parserOptions); + const parsedXML = parser.parse(xmlContent) as ParsedXMLWidget; + + if (!parsedXML.widget) { + throw new Error('Invalid widget XML: missing widget element'); + } + + const widget = parsedXML.widget; + + const widgetDef: WidgetDefinition = { + id: widget.id, + name: widget.name, + description: widget.description, + needsEntityContext: widget.needsEntityContext === 'true', + pluginWidget: widget.pluginWidget === 'true', + offlineCapable: widget.offlineCapable === 'true', + supportedPlatform: (widget.supportedPlatform as 'All' | 'Native' | 'Web') || 'Web', + properties: [], + }; + + if (widget.properties) { + widgetDef.properties = parseProperties(widget.properties); + } + + return widgetDef; +} + +function parseProperties(props: any): PropertyGroup[] | Property[] { + if (props.propertyGroup) { + const groups = ensureArray(props.propertyGroup); + + return groups.map(group => parsePropertyGroup(group)); + } + + const properties: Property[] = []; + + if (props.property) { + const propsArray = ensureArray(props.property); + + for (const prop of propsArray) { + properties.push(parseProperty(prop)); + } + } + + return properties; +} + +function parsePropertyGroup(group: ParsedXMLPropertyGroup): PropertyGroup { + const properties: (Property | SystemProperty)[] = []; + + if (group.property) { + const props = ensureArray(group.property); + for (const prop of props) { + properties.push(parseProperty(prop)); + } + } + + if (group.systemProperty) { + const sysProps = ensureArray(group.systemProperty); + for (const sysProp of sysProps) { + properties.push(parseSystemProperty(sysProp)); + } + } + + return { + caption: group.caption, + properties, + }; +} + +function parseProperty(prop: ParsedXMLProperty): Property { + const property: Property = { + key: prop.key, + type: prop.type, + caption: prop.caption || '', + description: prop.description || '', + required: prop.required !== 'false', + isList: prop.isList === 'true', + }; + + if (prop.defaultValue !== undefined) { + property.defaultValue = prop.defaultValue; + } + + if (prop.onChange) { + property.onChange = prop.onChange; + } + + if (prop.dataSource) { + property.dataSource = prop.dataSource; + } + + if (prop.attributeTypes) { + property.attributeTypes = parseAttributeTypes(prop.attributeTypes); + } + + if (prop.associationTypes) { + property.associationTypes = parseAssociationTypes(prop.associationTypes); + } + + if (prop.selectionTypes) { + property.selectionTypes = parseSelectionTypes(prop.selectionTypes); + } + + if (prop.enumerationValues) { + property.enumerationValues = parseEnumerationValues(prop.enumerationValues); + } + + if (prop.properties) { + const parsedProps = parseProperties(prop.properties); + property.properties = parsedProps.filter(p => !('caption' in p && 'properties' in p)) as Property[]; + } + + if (prop.returnType) { + property.returnType = { + type: prop.returnType.type as any, + isList: prop.returnType.isList === 'true', + }; + } + + return property; +} + +function parseSystemProperty(sysProp: ParsedXMLSystemProperty): SystemProperty { + const systemProperty: SystemProperty = { + key: sysProp.key, + }; + + if (sysProp.category) { + systemProperty.category = sysProp.category; + } + + return systemProperty; +} + +function parseAttributeTypes(attributeTypes: { attributeType: ParsedXMLAttributeType | ParsedXMLAttributeType[] }): AttributeType[] { + const types = ensureArray(attributeTypes.attributeType); + + return types.map(type => type.name); +} + +function parseAssociationTypes(associationTypes: { associationType: ParsedXMLAssociationType | ParsedXMLAssociationType[] }): AssociationType[] { + const types = ensureArray(associationTypes.associationType); + + return types.map(type => type.name); +} + +function parseSelectionTypes(selectionTypes: { selectionType: ParsedXMLSelectionType | ParsedXMLSelectionType[] }): SelectionType[] { + const types = ensureArray(selectionTypes.selectionType); + + return types.map(type => type.name); +} + +function parseEnumerationValues(enumerationValues: { enumerationValue: ParsedXMLEnumerationValue | ParsedXMLEnumerationValue[] }): EnumerationValue[] { + const values = ensureArray(enumerationValues.enumerationValue); + + return values.map(value => ({ + key: value.key, + value: value._ || value.key, + })); +} \ No newline at end of file diff --git a/src/type-generator/preview-types.ts b/src/type-generator/preview-types.ts new file mode 100644 index 0000000..9d61759 --- /dev/null +++ b/src/type-generator/preview-types.ts @@ -0,0 +1,221 @@ +import type { + WidgetDefinition, + Property, + PropertyGroup, + SystemProperty +} from './types'; +import { + pascalCase, + sanitizePropertyKey, + formatDescription +} from './utils'; +import { + extractSystemProperties, + hasLabelProperty, + generatePreviewSystemProps +} from './system-props'; + +export function generatePreviewTypeDefinition( + widget: WidgetDefinition, +): string { + const interfaceName = `${pascalCase(widget.name)}PreviewProps`; + const properties = extractAllProperties(widget.properties); + const systemProps = extractSystemProperties(widget.properties); + const hasLabel = hasLabelProperty(systemProps); + const widgetProperties = properties.filter(p => !isSystemProperty(p)) as Property[]; + + let output = ''; + + output += generatePreviewImports(); + output += generatePreviewJSDoc(widget); + output += `export interface ${interfaceName} {\n`; + output += ' /**\n'; + output += ' * Whether the widget is in read-only mode\n'; + output += ' */\n'; + output += ' readOnly: boolean;\n'; + output += ' /**\n'; + output += ' * The render mode of the widget preview\n'; + output += ' */\n'; + output += ' renderMode?: "design" | "xray" | "structure";\n'; + + const systemPropsLines = generatePreviewSystemProps(hasLabel); + + for (const line of systemPropsLines) { + output += ' ' + line + '\n'; + } + + for (const property of widgetProperties) { + output += generatePreviewPropertyDefinition(property); + } + + output += '}\n'; + + return output; +} + +function generatePreviewImports(): string { + const imports: string[] = []; + + imports.push('CSSProperties'); + imports.push('PreviewValue'); + + let output = ''; + + if (imports.length > 0) { + output += `import type { ${imports.join(', ')} } from 'react';\n\n`; + } + + return output; +} + +function generatePreviewJSDoc(widget: WidgetDefinition): string { + let jsDoc = '/**\n'; + jsDoc += ` * Preview props for ${widget.name}\n`; + + if (widget.description) { + jsDoc += ` * ${formatDescription(widget.description)}\n`; + } + + jsDoc += ' * @preview This interface is used in design mode\n'; + jsDoc += ' */\n'; + return jsDoc; +} + +function generatePreviewPropertyDefinition( + property: Property, +): string { + const indent = ' '; + let output = ''; + + if (property.description) { + output += `${indent}/**\n`; + output += `${indent} * ${formatDescription(property.description)}\n`; + + if (property.caption && property.caption !== property.description) { + output += `${indent} * @caption ${property.caption}\n`; + } + + output += `${indent} */\n`; + } + + const propertyKey = sanitizePropertyKey(property.key); + const optional = property.required === false ? '?' : ''; + const propertyType = mapPropertyToPreviewType(property); + + output += `${indent}${propertyKey}${optional}: ${propertyType};\n`; + + return output; +} + +function mapPropertyToPreviewType( + property: Property, +): string { + const { type, isList, enumerationValues } = property; + + let baseType: string; + + switch (type) { + case 'string': + case 'translatableString': + baseType = 'string'; + break; + + case 'boolean': + baseType = 'boolean'; + break; + + case 'integer': + case 'decimal': + baseType = 'number'; + break; + + case 'action': + case 'microflow': + case 'nanoflow': + baseType = '{} | null'; + break; + + case 'attribute': + case 'expression': + case 'entityConstraint': + baseType = 'string'; + break; + + case 'textTemplate': + baseType = 'string'; + break; + + case 'datasource': + baseType = '{ type: string } | { caption: string } | {}'; + break; + + case 'icon': + case 'image': + case 'file': + baseType = '{ uri: string } | null'; + break; + + case 'widgets': + baseType = 'PreviewValue | null'; + break; + + case 'enumeration': + if (enumerationValues && enumerationValues.length > 0) { + baseType = enumerationValues.map(ev => `"${ev.key}"`).join(' | '); + } else { + baseType = 'string'; + } + break; + + case 'object': + if (property.properties && property.properties.length > 0) { + baseType = `${pascalCase(property.key)}PreviewType`; + } else { + baseType = 'object'; + } + break; + + case 'entity': + case 'association': + case 'selection': + baseType = 'string'; + break; + + case 'form': + baseType = 'string'; + break; + + default: + baseType = 'any'; + } + + return isList && type !== 'datasource' ? `${baseType}[]` : baseType; +} + +function extractAllProperties( + properties: PropertyGroup[] | Property[] +): (Property | SystemProperty)[] { + const result: (Property | SystemProperty)[] = []; + + for (const item of properties) { + if (isPropertyGroup(item)) { + result.push(...item.properties); + } else { + result.push(item); + } + } + + return result; +} + +function isPropertyGroup( + item: PropertyGroup | Property | SystemProperty +): item is PropertyGroup { + return 'caption' in item && 'properties' in item; +} + +function isSystemProperty( + item: Property | SystemProperty +): item is SystemProperty { + return !('type' in item) && 'key' in item; +} \ No newline at end of file diff --git a/src/type-generator/system-props.ts b/src/type-generator/system-props.ts new file mode 100644 index 0000000..8b20b02 --- /dev/null +++ b/src/type-generator/system-props.ts @@ -0,0 +1,87 @@ +import { GenerateTargetPlatform } from './mendix-types'; +import type { SystemProperty, Property, PropertyGroup } from './types'; + +export interface SystemPropsConfig { + hasLabel?: boolean; + platform?: GenerateTargetPlatform; +} + +export function extractSystemProperties( + properties: PropertyGroup[] | Property[] | (Property | SystemProperty)[] +): SystemProperty[] { + const systemProps: SystemProperty[] = []; + + for (const item of properties) { + if (isPropertyGroup(item)) { + for (const prop of item.properties) { + if (isSystemProperty(prop)) { + systemProps.push(prop); + } + } + } else if (isSystemProperty(item)) { + systemProps.push(item); + } + } + + return systemProps; +} + +export function hasLabelProperty(systemProperties: SystemProperty[]): boolean { + return systemProperties.some(prop => prop.key === 'Label'); +} + +export function generateSystemProps(config: SystemPropsConfig = {}): string[] { + const { hasLabel = false, platform = 'web' } = config; + const props: string[] = []; + + props.push('name?: string;'); + + if (platform !== 'native') { + if (!hasLabel) { + props.push('class?: string;'); + props.push('style?: CSSProperties;'); + } + props.push('tabIndex?: number;'); + } + + if (hasLabel) { + props.push('id?: string;'); + } + + return props; +} + +export function getSystemPropsImports(config: SystemPropsConfig = {}): string[] { + const { platform = 'web' } = config; + const imports: string[] = []; + + if (platform !== 'native') { + imports.push('CSSProperties'); + } + + return imports; +} + +export function generatePreviewSystemProps(hasLabel: boolean): string[] { + const props: string[] = []; + + if (!hasLabel) { + props.push('/**'); + props.push(' * @deprecated Use class property instead'); + props.push(' */'); + props.push('className: string;'); + props.push('class: string;'); + props.push('style: string;'); + props.push('styleObject?: CSSProperties;'); + } + + return props; +} + +function isPropertyGroup(item: PropertyGroup | Property | SystemProperty): item is PropertyGroup { + return 'caption' in item && 'properties' in item; +} + +function isSystemProperty(item: Property | SystemProperty): item is SystemProperty { + return !('type' in item) && 'key' in item; +} \ No newline at end of file diff --git a/src/type-generator/types.ts b/src/type-generator/types.ts new file mode 100644 index 0000000..0398013 --- /dev/null +++ b/src/type-generator/types.ts @@ -0,0 +1,174 @@ +export interface WidgetDefinition { + id: string; + name: string; + description: string; + needsEntityContext?: boolean; + pluginWidget?: boolean; + offlineCapable?: boolean; + supportedPlatform?: 'All' | 'Native' | 'Web'; + properties: PropertyGroup[] | Property[]; +} + +export interface PropertyGroup { + caption: string; + properties: (Property | SystemProperty)[]; +} + +export interface Property { + key: string; + type: PropertyType; + caption: string; + description: string; + required?: boolean; + isList?: boolean; + defaultValue?: string; + attributeTypes?: AttributeType[]; + associationTypes?: AssociationType[]; + selectionTypes?: SelectionType[]; + enumerationValues?: EnumerationValue[]; + properties?: Property[]; + returnType?: ReturnType; + onChange?: string; + dataSource?: string; +} + +export interface SystemProperty { + key: SystemPropertyKey; + category?: string; +} + +export type PropertyType = + | 'action' + | 'association' + | 'attribute' + | 'boolean' + | 'datasource' + | 'decimal' + | 'entity' + | 'entityConstraint' + | 'enumeration' + | 'expression' + | 'file' + | 'form' + | 'icon' + | 'image' + | 'integer' + | 'microflow' + | 'nanoflow' + | 'object' + | 'selection' + | 'string' + | 'translatableString' + | 'textTemplate' + | 'widgets'; + +export type AttributeType = + | 'AutoNumber' + | 'Binary' + | 'Boolean' + | 'Currency' + | 'DateTime' + | 'Enum' + | 'Float' + | 'HashString' + | 'Integer' + | 'Long' + | 'String' + | 'Decimal'; + +export type AssociationType = 'Reference' | 'ReferenceSet'; + +export type SelectionType = 'None' | 'Single' | 'Multi'; + +export type SystemPropertyKey = + | 'Label' + | 'Name' + | 'TabIndex' + | 'Editability' + | 'Visibility'; + +export interface EnumerationValue { + key: string; + value: string; +} + +export interface ReturnType { + type: 'Void' | 'Boolean' | 'Integer' | 'Float' | 'DateTime' | 'String' | 'Object' | 'Decimal'; + isList?: boolean; +} + +export interface ParsedXMLWidget { + widget: { + id: string; + pluginWidget?: string; + needsEntityContext?: string; + offlineCapable?: string; + supportedPlatform?: string; + name: string; + description: string; + properties: ParsedXMLProperties; + }; +} + +export interface ParsedXMLProperties { + property?: ParsedXMLProperty | ParsedXMLProperty[]; + propertyGroup?: ParsedXMLPropertyGroup | ParsedXMLPropertyGroup[]; + systemProperty?: ParsedXMLSystemProperty | ParsedXMLSystemProperty[]; +} + +export interface ParsedXMLPropertyGroup { + caption: string; + property?: ParsedXMLProperty | ParsedXMLProperty[]; + systemProperty?: ParsedXMLSystemProperty | ParsedXMLSystemProperty[]; +} + +export interface ParsedXMLProperty { + key: string; + type: PropertyType; + required?: string; + isList?: string; + defaultValue?: string; + onChange?: string; + dataSource?: string; + caption: string; + description: string; + attributeTypes?: { + attributeType: ParsedXMLAttributeType | ParsedXMLAttributeType[]; + }; + associationTypes?: { + associationType: ParsedXMLAssociationType | ParsedXMLAssociationType[]; + }; + selectionTypes?: { + selectionType: ParsedXMLSelectionType | ParsedXMLSelectionType[]; + }; + enumerationValues?: { + enumerationValue: ParsedXMLEnumerationValue | ParsedXMLEnumerationValue[]; + }; + properties?: ParsedXMLProperties; + returnType?: { + type: string; + isList?: string; + }; +} + +export interface ParsedXMLSystemProperty { + key: SystemPropertyKey; + category?: string; +} + +export interface ParsedXMLAttributeType { + name: AttributeType; +} + +export interface ParsedXMLAssociationType { + name: AssociationType; +} + +export interface ParsedXMLSelectionType { + name: SelectionType; +} + +export interface ParsedXMLEnumerationValue { + _: string; + key: string; +} \ No newline at end of file diff --git a/src/type-generator/utils.ts b/src/type-generator/utils.ts new file mode 100644 index 0000000..bb67c80 --- /dev/null +++ b/src/type-generator/utils.ts @@ -0,0 +1,88 @@ +import type { + AttributeType, + Property, +} from './types'; +import type { GenerateTargetPlatform } from './mendix-types'; +import { mapPropertyToMendixType } from './mendix-types'; + +export function mapPropertyTypeToTS(property: Property, target?: GenerateTargetPlatform): string { + const mapping = mapPropertyToMendixType(property, target); + + return mapping.type; +} + +export function mapAttributeTypeToTS(attributeType: AttributeType): string { + switch (attributeType) { + case 'String': + case 'HashString': + case 'Enum': + return 'string'; + case 'Boolean': + return 'boolean'; + case 'Integer': + case 'Long': + case 'AutoNumber': + case 'Float': + case 'Currency': + case 'Decimal': + return 'number'; + + case 'DateTime': + return 'Date | string'; + + case 'Binary': + return 'Blob | string'; + + default: + return 'any'; + } +} + +export function mapReturnTypeToTS(returnType: string): string { + switch (returnType) { + case 'Void': + return 'void'; + case 'Boolean': + return 'boolean'; + case 'Integer': + case 'Float': + case 'Decimal': + return 'number'; + case 'DateTime': + return 'Date | string'; + case 'String': + return 'string'; + case 'Object': + return 'object'; + default: + return 'any'; + } +} + +export function ensureArray(value: T | T[] | undefined): T[] { + if (!value) return []; + return Array.isArray(value) ? value : [value]; +} + +export function pascalCase(str: string): string { + return str + .split(/[-_\s]+/) + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(''); +} + +export function sanitizePropertyKey(key: string): string { + if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) { + return key; + } + return `'${key}'`; +} + +export function formatDescription(description: string): string { + return description + .trim() + .split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0) + .join(' '); +} \ No newline at end of file From 4ec8a831f7e66ef56928927d5b82fa4d170de8e8 Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Thu, 28 Aug 2025 15:51:19 +0900 Subject: [PATCH 07/10] fix: fix generate type name --- src/type-generator/generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/type-generator/generator.ts b/src/type-generator/generator.ts index 6f42a7c..372dd79 100644 --- a/src/type-generator/generator.ts +++ b/src/type-generator/generator.ts @@ -63,7 +63,7 @@ export function generateTypeDefinition(widget: WidgetDefinition, target: Generat } function generateInterfaceName(widgetName: string): string { - return `${pascalCase(widgetName)}Props`; + return `${pascalCase(widgetName)}ContainerProps`; } export function extractAllProperties(properties: PropertyGroup[] | Property[]): (Property | SystemProperty)[] { From 978f7c71078c71e80df7f2823893406f2647f643 Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Thu, 28 Aug 2025 15:51:29 +0900 Subject: [PATCH 08/10] fix: change postinstall script to prepare script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f6c31c..a8d3c67 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "watch": "rslib build --watch", "start": "pnpm build && node ./dist/cli.js", "package": "pnpm build && pnpm pack", - "postinstall": "node ./tools/copy-widget-schema.js" + "prepare": "node ./tools/copy-widget-schema.js" }, "main": "dist/index.cjs", "module": "dist/index.mjs", From 5214ccec435da79d747debfcb968be1bb90c7e3d Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Thu, 28 Aug 2025 15:51:46 +0900 Subject: [PATCH 09/10] feat: generate types on build, start --- src/commands/build/web/index.ts | 29 ++++++++--- src/commands/start/web/index.ts | 91 +++++++++++---------------------- 2 files changed, 54 insertions(+), 66 deletions(-) diff --git a/src/commands/build/web/index.ts b/src/commands/build/web/index.ts index 05a0188..c880620 100644 --- a/src/commands/build/web/index.ts +++ b/src/commands/build/web/index.ts @@ -12,9 +12,28 @@ import getWidgetName from '../../../utils/getWidgetName'; import getWidgetPackageJson from '../../../utils/getWidgetPackageJson'; import getMendixWidgetDirectory from '../../../utils/getMendixWidgetDirectory'; import getViteUserConfiguration from '../../../utils/getViteUserConfiguration'; +import { generateTypesFromFile } from '../../../type-generator'; const buildWebCommand = async (isProduction: boolean = false) => { - // try { + try { + showMessage('Generate types'); + + const widgetName = await getWidgetName(); + const originWidgetXmlPath = path.join(PROJECT_DIRECTORY, `src/${widgetName}.xml`); + const typingsPath = path.join(PROJECT_DIRECTORY, 'typings'); + const typingsDirExists = await pathIsExists(typingsPath); + + if (typingsDirExists) { + await fs.rm(typingsPath, { recursive: true, force: true }); + } + + await fs.mkdir(typingsPath); + + const newTypingsFilePath = path.join(typingsPath, `${widgetName}Props.d.ts`); + const typingContents = await generateTypesFromFile(originWidgetXmlPath, 'web'); + + await fs.writeFile(newTypingsFilePath, typingContents); + showMessage('Remove previous builds'); const distDir = path.join(PROJECT_DIRECTORY, DIST_DIRECTORY_NAME); @@ -46,10 +65,8 @@ const buildWebCommand = async (isProduction: boolean = false) => { resultViteConfig = await getViteDefaultConfig(false); } - const widgetName = await getWidgetName(); const originPackageXmlPath = path.join(PROJECT_DIRECTORY, 'src/package.xml'); const destPackageXmlPath = path.join(WEB_OUTPUT_DIRECTORY, 'package.xml'); - const originWidgetXmlPath = path.join(PROJECT_DIRECTORY, `src/${widgetName}.xml`); const destWidgetXmlPath = path.join(WEB_OUTPUT_DIRECTORY, `${widgetName}.xml`); await fs.copyFile(originPackageXmlPath, destPackageXmlPath); @@ -94,9 +111,9 @@ const buildWebCommand = async (isProduction: boolean = false) => { await fs.copyFile(mpkFileDestPath, mendixMpkFileDestPath); showMessage(`${COLOR_GREEN('Build complete.')}`); - // } catch (error) { - // showMessage(`${COLOR_ERROR('Build failed.')}\nError occurred: ${COLOR_ERROR((error as Error).stack)}`); - // } + } catch (error) { + showMessage(`${COLOR_ERROR('Build failed.')}\nError occurred: ${COLOR_ERROR((error as Error).stack)}`); + } }; export default buildWebCommand; \ No newline at end of file diff --git a/src/commands/start/web/index.ts b/src/commands/start/web/index.ts index 1db55c7..940db16 100644 --- a/src/commands/start/web/index.ts +++ b/src/commands/start/web/index.ts @@ -10,11 +10,32 @@ import pathIsExists from "../../../utils/pathIsExists"; import { getViteDefaultConfig } from '../../../configurations/vite'; import getWidgetName from '../../../utils/getWidgetName'; import getViteUserConfiguration from '../../../utils/getViteUserConfiguration'; +import { generateTypesFromFile } from '../../../type-generator'; + +const generateTyping = async () => { + const widgetName = await getWidgetName(); + const originWidgetXmlPath = path.join(PROJECT_DIRECTORY, `src/${widgetName}.xml`); + const typingsPath = path.join(PROJECT_DIRECTORY, 'typings'); + const typingsDirExists = await pathIsExists(typingsPath); + + if (typingsDirExists) { + await fs.rm(typingsPath, { recursive: true, force: true }); + } + + await fs.mkdir(typingsPath); + + const newTypingsFilePath = path.join(typingsPath, `${widgetName}Props.d.ts`); + const typingContents = await generateTypesFromFile(originWidgetXmlPath, 'web'); + + await fs.writeFile(newTypingsFilePath, typingContents); +}; const startWebCommand = async () => { try { showMessage('Start widget server'); + await generateTyping(); + const customViteConfigPath = path.join(PROJECT_DIRECTORY, VITE_CONFIGURATION_FILENAME); const viteConfigIsExists = await pathIsExists(customViteConfigPath); let resultViteConfig: UserConfig; @@ -362,66 +383,16 @@ const startWebCommand = async () => { } } }, - // { - // name: 'mendix-hotreload-react', - // enforce: 'pre', - // transform(code, id) { - // if (!id.includes('node_modules') && /\.(tsx?|jsx?)$/.test(id)) { - // let transformedCode = code; - - // transformedCode = transformedCode.replace( - // /import\s+(\w+)\s+from\s+['"]react['"]/g, - // 'const $1 = window.React' - // ); - - // transformedCode = transformedCode.replace( - // /import\s+\*\s+as\s+(\w+)\s+from\s+['"]react['"]/g, - // 'const $1 = window.React' - // ); - - // transformedCode = transformedCode.replace( - // /import\s+{([^}]+)}\s+from\s+['"]react['"]/g, - // (match, imports) => { - // const cleanImports = imports.replace(/\s+/g, ' ').trim(); - // return `const { ${cleanImports} } = window.React`; - // } - // ); - - // transformedCode = transformedCode.replace( - // /import\s+(\w+)\s*,\s*{([^}]+)}\s+from\s+['"]react['"]/g, - // (match, defaultImport, namedImports) => { - // const cleanImports = namedImports.replace(/\s+/g, ' ').trim(); - // return `const ${defaultImport} = window.React;\nconst { ${cleanImports} } = window.React`; - // } - // ); - - // transformedCode = transformedCode.replace( - // /import\s+(\w+)\s+from\s+['"]react-dom['"]/g, - // 'const $1 = window.ReactDOM' - // ); - - // transformedCode = transformedCode.replace( - // /import\s+{([^}]+)}\s+from\s+['"]react-dom['"]/g, - // 'const { $1 } = window.ReactDOM' - // ); - - // transformedCode = transformedCode.replace( - // /import\s+{([^}]+)}\s+from\s+['"]react-dom\/client['"]/g, - // 'const { $1 } = window.ReactDOM' - // ); - - // transformedCode = transformedCode.replace( - // /import\s+type\s+{([^}]+)}\s+from\s+['"]react['"]/g, - // '// Type import removed: $1' - // ); - - // return { - // code: transformedCode, - // map: null - // }; - // } - // }, - // }, + { + name: 'mendix-xml-watch-plugin', + configureServer(server) { + server.watcher.on('change', (file) => { + if (file.endsWith('xml')) { + generateTyping(); + } + }); + } + } ] }); From 5f2c9e9bae8217ee1fde30a4ae73b04ea2e276fe Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Thu, 28 Aug 2025 15:52:11 +0900 Subject: [PATCH 10/10] feat: bump version to 0.1.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a8d3c67..0e180be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@repixelcorp/hyper-pwt", - "version": "0.1.4", + "version": "0.1.5", "description": "A faster, more modern, superior alternative for Mendix PWT.", "repository": { "type": "git",