diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..23b9a80 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm lint:fix diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d6117c5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.biome": "always" + } +} diff --git a/benchmark/benchmarkWidget/.eslintrc.js b/benchmark/benchmarkWidget/.eslintrc.js index a81cc74..1bab70f 100644 --- a/benchmark/benchmarkWidget/.eslintrc.js +++ b/benchmark/benchmarkWidget/.eslintrc.js @@ -1,5 +1,5 @@ const base = require("@mendix/pluggable-widgets-tools/configs/eslint.ts.base.json"); module.exports = { - ...base + ...base, }; diff --git a/benchmark/benchmarkWidget/prettier.config.js b/benchmark/benchmarkWidget/prettier.config.js index 013fbe7..abfef46 100644 --- a/benchmark/benchmarkWidget/prettier.config.js +++ b/benchmark/benchmarkWidget/prettier.config.js @@ -1,6 +1,10 @@ const base = require("@mendix/pluggable-widgets-tools/configs/prettier.base.json"); module.exports = { - ...base, - plugins: [require.resolve("@prettier/plugin-xml")], + ...base, + plugins: [ + require.resolve( + "@prettier/plugin-xml", + ), + ], }; diff --git a/benchmark/benchmarkWidget/src/BenchmarkWidget.editorConfig.ts b/benchmark/benchmarkWidget/src/BenchmarkWidget.editorConfig.ts index e70f58b..6100b79 100644 --- a/benchmark/benchmarkWidget/src/BenchmarkWidget.editorConfig.ts +++ b/benchmark/benchmarkWidget/src/BenchmarkWidget.editorConfig.ts @@ -1,115 +1,144 @@ import { BenchmarkWidgetPreviewProps } from "../typings/BenchmarkWidgetProps"; -export type Platform = "web" | "desktop"; +export type Platform = + | "web" + | "desktop"; -export type Properties = PropertyGroup[]; +export type Properties = + PropertyGroup[]; -type PropertyGroup = { +type PropertyGroup = + { caption: string; propertyGroups?: PropertyGroup[]; properties?: Property[]; -}; + }; type Property = { - key: string; - caption: string; - description?: string; - objectHeaders?: string[]; // used for customizing object grids - objects?: ObjectProperties[]; - properties?: Properties[]; + key: string; + caption: string; + description?: string; + objectHeaders?: string[]; // used for customizing object grids + objects?: ObjectProperties[]; + properties?: Properties[]; }; -type ObjectProperties = { +type ObjectProperties = + { properties: PropertyGroup[]; captions?: string[]; // used for customizing object grids -}; + }; -export type Problem = { +export type Problem = + { property?: string; // key of the property, at which the problem exists - severity?: "error" | "warning" | "deprecation"; // default = "error" + severity?: + | "error" + | "warning" + | "deprecation"; // default = "error" message: string; // description of the problem studioMessage?: string; // studio-specific message, defaults to message url?: string; // link with more information about the problem studioUrl?: string; // studio-specific link -}; + }; type BaseProps = { - type: "Image" | "Container" | "RowLayout" | "Text" | "DropZone" | "Selectable" | "Datasource"; - grow?: number; // optionally sets a growth factor if used in a layout (default = 1) + type: + | "Image" + | "Container" + | "RowLayout" + | "Text" + | "DropZone" + | "Selectable" + | "Datasource"; + grow?: number; // optionally sets a growth factor if used in a layout (default = 1) }; -type ImageProps = BaseProps & { +type ImageProps = + BaseProps & { type: "Image"; document?: string; // svg image data?: string; // base64 image property?: object; // widget image property object from Values API width?: number; // sets a fixed maximum width height?: number; // sets a fixed maximum height -}; + }; -type ContainerProps = BaseProps & { - type: "Container" | "RowLayout"; +type ContainerProps = + BaseProps & { + type: + | "Container" + | "RowLayout"; children: PreviewProps[]; // any other preview element borders?: boolean; // sets borders around the layout to visually group its children borderRadius?: number; // integer. Can be used to create rounded borders backgroundColor?: string; // HTML color, formatted #RRGGBB borderWidth?: number; // sets the border width padding?: number; // integer. adds padding around the container -}; + }; -type RowLayoutProps = ContainerProps & { +type RowLayoutProps = + ContainerProps & { type: "RowLayout"; - columnSize?: "fixed" | "grow"; // default is fixed -}; + columnSize?: + | "fixed" + | "grow"; // default is fixed + }; -type TextProps = BaseProps & { +type TextProps = + BaseProps & { type: "Text"; content: string; // text that should be shown fontSize?: number; // sets the font size fontColor?: string; // HTML color, formatted #RRGGBB bold?: boolean; italic?: boolean; -}; + }; -type DropZoneProps = BaseProps & { +type DropZoneProps = + BaseProps & { type: "DropZone"; property: object; // widgets property object from Values API placeholder: string; // text to be shown inside the dropzone when empty showDataSourceHeader?: boolean; // true by default. Toggles whether to show a header containing information about the datasource -}; + }; -type SelectableProps = BaseProps & { +type SelectableProps = + BaseProps & { type: "Selectable"; object: object; // object property instance from the Value API child: PreviewProps; // any type of preview property to visualize the object instance -}; + }; -type DatasourceProps = BaseProps & { +type DatasourceProps = + BaseProps & { type: "Datasource"; - property: object | null; // datasource property object from Values API + property: + | object + | null; // datasource property object from Values API child?: PreviewProps; // any type of preview property component (optional) -}; + }; export type PreviewProps = - | ImageProps - | ContainerProps - | RowLayoutProps - | TextProps - | DropZoneProps - | SelectableProps - | DatasourceProps; + | ImageProps + | ContainerProps + | RowLayoutProps + | TextProps + | DropZoneProps + | SelectableProps + | DatasourceProps; export function getProperties( - _values: BenchmarkWidgetPreviewProps, - defaultProperties: Properties /* , target: Platform*/ + _values: BenchmarkWidgetPreviewProps, + defaultProperties: Properties /* , target: Platform*/, ): Properties { - // Do the values manipulation here to control the visibility of properties in Studio and Studio Pro conditionally. - /* Example + // Do the values manipulation here to control the visibility of properties in Studio and Studio Pro conditionally. + /* Example if (values.myProperty === "custom") { delete defaultProperties.properties.myOtherProperty; } */ - return defaultProperties; + return defaultProperties; } // export function check(_values: BenchmarkWidgetPreviewProps): Problem[] { diff --git a/benchmark/benchmarkWidget/src/BenchmarkWidget.editorPreview.tsx b/benchmark/benchmarkWidget/src/BenchmarkWidget.editorPreview.tsx index 8ec2074..2ba5fe5 100644 --- a/benchmark/benchmarkWidget/src/BenchmarkWidget.editorPreview.tsx +++ b/benchmark/benchmarkWidget/src/BenchmarkWidget.editorPreview.tsx @@ -1,9 +1,17 @@ -import { ReactElement, createElement } from "react"; +import { + ReactElement, + createElement, +} from "react"; export function preview(): ReactElement { - return
Benchmark Widget
; + return ( +
+ Benchmark + Widget +
+ ); } export function getPreviewCss(): string { - return require("./ui/BenchmarkWidget.css"); + return require("./ui/BenchmarkWidget.css"); } diff --git a/benchmark/benchmarkWidget/src/BenchmarkWidget.tsx b/benchmark/benchmarkWidget/src/BenchmarkWidget.tsx index 0aded0f..d476dc3 100644 --- a/benchmark/benchmarkWidget/src/BenchmarkWidget.tsx +++ b/benchmark/benchmarkWidget/src/BenchmarkWidget.tsx @@ -1,8 +1,13 @@ -import { ReactElement, createElement } from "react"; +import { + ReactElement, + createElement, +} from "react"; import { CalendarWidget } from "./components/CalendarWidget"; import "./ui/BenchmarkWidget.css"; export function BenchmarkWidget(): ReactElement { - return ; + return ( + + ); } diff --git a/benchmark/benchmarkWidget/src/components/CalendarWidget.tsx b/benchmark/benchmarkWidget/src/components/CalendarWidget.tsx index bbbc2c9..6fb515c 100644 --- a/benchmark/benchmarkWidget/src/components/CalendarWidget.tsx +++ b/benchmark/benchmarkWidget/src/components/CalendarWidget.tsx @@ -1,18 +1,34 @@ -import { ReactElement, createElement } from "react"; -import { Calendar, dayjsLocalizer } from "react-big-calendar"; +import { + ReactElement, + createElement, +} from "react"; +import { + Calendar, + dayjsLocalizer, +} from "react-big-calendar"; import dayjs from "dayjs"; import "react-big-calendar/lib/css/react-big-calendar.css"; -const localizer = dayjsLocalizer(dayjs); +const localizer = + dayjsLocalizer( + dayjs, + ); export function CalendarWidget(): ReactElement { - return ( - - ); + return ( + + ); } diff --git a/benchmark/benchmarkWidget/src/ui/BenchmarkWidget.css b/benchmark/benchmarkWidget/src/ui/BenchmarkWidget.css index 1c2d0a2..950373b 100644 --- a/benchmark/benchmarkWidget/src/ui/BenchmarkWidget.css +++ b/benchmark/benchmarkWidget/src/ui/BenchmarkWidget.css @@ -2,5 +2,4 @@ Place your custom CSS here */ .widget-hello-world { - } diff --git a/benchmark/benchmarkWidget/tsconfig.json b/benchmark/benchmarkWidget/tsconfig.json index 80cca52..666fb0c 100644 --- a/benchmark/benchmarkWidget/tsconfig.json +++ b/benchmark/benchmarkWidget/tsconfig.json @@ -3,5 +3,8 @@ "compilerOptions": { "baseUrl": "./" }, - "include": ["./src", "./typings"] + "include": [ + "./src", + "./typings" + ] } diff --git a/benchmark/benchmarkWidget/typings/BenchmarkWidgetProps.d.ts b/benchmark/benchmarkWidget/typings/BenchmarkWidgetProps.d.ts index 93d71af..cb3a6fb 100644 --- a/benchmark/benchmarkWidget/typings/BenchmarkWidgetProps.d.ts +++ b/benchmark/benchmarkWidget/typings/BenchmarkWidgetProps.d.ts @@ -1,12 +1,12 @@ /** * This file was automatically generated by @repixelcorp/hyper-pwt v0.3.1 * 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. */ -import { CSSProperties } from 'mendix'; +import { CSSProperties } from "mendix"; /** * Props for Benchmark Widget @@ -27,7 +27,10 @@ export interface BenchmarkWidgetContainerProps { sampleText?: string; } -import type { CSSProperties, PreviewValue } from 'react'; +import type { + CSSProperties, + PreviewValue, +} from "react"; /** * Preview props for Benchmark Widget @@ -42,7 +45,10 @@ export interface BenchmarkWidgetPreviewProps { /** * The render mode of the widget preview */ - renderMode?: "design" | "xray" | "structure"; + renderMode?: + | "design" + | "xray" + | "structure"; /** * @deprecated Use class property instead */ diff --git a/benchmark/package.json b/benchmark/package.json index ee9c705..1ace6e1 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -18,4 +18,4 @@ "fs-extra": "^11.2.0" }, "devDependencies": {} -} \ No newline at end of file +} diff --git a/benchmark/src/benchmark.js b/benchmark/src/benchmark.js index c5e00af..4cf0401 100644 --- a/benchmark/src/benchmark.js +++ b/benchmark/src/benchmark.js @@ -1,52 +1,52 @@ -import { execSync } from 'child_process'; -import fs from 'fs-extra'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import chalk from 'chalk'; -import Table from 'cli-table3'; +import { execSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import chalk from "chalk"; +import Table from "cli-table3"; +import fs from "fs-extra"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const rootDir = path.join(__dirname, '..'); -const widgetDir = path.join(rootDir, 'benchmarkWidget'); +const rootDir = path.join(__dirname, ".."); +const widgetDir = path.join(rootDir, "benchmarkWidget"); class WidgetBuildBenchmark { constructor(options = {}) { - this.verbose = options.verbose || process.argv.includes('--verbose'); - this.jsonOutput = options.json || process.argv.includes('--json'); + this.verbose = options.verbose || process.argv.includes("--verbose"); + this.jsonOutput = options.json || process.argv.includes("--json"); this.results = { standardBuild: {}, hyperBuild: {}, - comparison: {} + comparison: {}, }; } - log(message, type = 'info') { + log(message, type = "info") { if (this.jsonOutput) return; - + const prefix = { - info: chalk.blue('[INFO]'), - success: chalk.green('[SUCCESS]'), - warning: chalk.yellow('[WARNING]'), - error: chalk.red('[ERROR]'), - debug: chalk.gray('[DEBUG]') + info: chalk.blue("[INFO]"), + success: chalk.green("[SUCCESS]"), + warning: chalk.yellow("[WARNING]"), + error: chalk.red("[ERROR]"), + debug: chalk.gray("[DEBUG]"), }; - + console.log(`${prefix[type]} ${message}`); } async clean() { - this.log('Cleaning previous build artifacts...'); - const distPath = path.join(widgetDir, 'dist'); - const tmpPath = path.join(widgetDir, 'tmp'); - + this.log("Cleaning previous build artifacts..."); + const distPath = path.join(widgetDir, "dist"); + const tmpPath = path.join(widgetDir, "tmp"); + await fs.remove(distPath); await fs.remove(tmpPath); - + // Clean any .mpk files const files = await fs.readdir(widgetDir); for (const file of files) { - if (file.endsWith('.mpk')) { + if (file.endsWith(".mpk")) { await fs.remove(path.join(widgetDir, file)); } } @@ -54,62 +54,63 @@ class WidgetBuildBenchmark { runBuild(command, buildType) { this.log(`Running ${buildType} build...`); - + const startTime = Date.now(); const startMemory = process.memoryUsage(); - + try { const output = execSync(`npm run ${command}`, { cwd: widgetDir, - stdio: this.verbose ? 'inherit' : 'pipe', - encoding: 'utf8' + stdio: this.verbose ? "inherit" : "pipe", + encoding: "utf8", }); - + const endTime = Date.now(); const endMemory = process.memoryUsage(); - + const buildTime = endTime - startTime; - const memoryUsed = (endMemory.heapUsed - startMemory.heapUsed) / 1024 / 1024; - - this.log(`${buildType} build completed in ${buildTime}ms`, 'success'); - + const memoryUsed = + (endMemory.heapUsed - startMemory.heapUsed) / 1024 / 1024; + + this.log(`${buildType} build completed in ${buildTime}ms`, "success"); + return { success: true, buildTime, memoryUsed, - output: this.verbose ? output : null + output: this.verbose ? output : null, }; } catch (error) { - this.log(`${buildType} build failed: ${error.message}`, 'error'); + this.log(`${buildType} build failed: ${error.message}`, "error"); return { success: false, - error: error.message + error: error.message, }; } } - async analyzeBuildOutput(buildType) { - const distPath = path.join(widgetDir, 'dist'); - const distMpkPath = path.join(distPath, '1.0.0'); - + async analyzeBuildOutput() { + const distPath = path.join(widgetDir, "dist"); + const distMpkPath = path.join(distPath, "1.0.0"); + const analysis = { distSize: 0, fileCount: 0, files: [], - mpkSize: 0 + mpkSize: 0, }; // Analyze dist folder if (await fs.pathExists(distPath)) { const files = await this.getFilesRecursively(distPath); analysis.fileCount = files.length; - + for (const file of files) { const stats = await fs.stat(file); const relPath = path.relative(distPath, file); analysis.files.push({ path: relPath, - size: stats.size + size: stats.size, }); analysis.distSize += stats.size; } @@ -117,8 +118,8 @@ class WidgetBuildBenchmark { // Find and analyze .mpk file const widgetFiles = await fs.readdir(distMpkPath); - const mpkFile = widgetFiles.find(f => f.endsWith('.mpk')); - + const mpkFile = widgetFiles.find((f) => f.endsWith(".mpk")); + if (mpkFile) { const mpkPath = path.join(distMpkPath, mpkFile); const stats = await fs.stat(mpkPath); @@ -132,39 +133,39 @@ class WidgetBuildBenchmark { async getFilesRecursively(dir) { const files = []; const items = await fs.readdir(dir); - + for (const item of items) { const fullPath = path.join(dir, item); const stats = await fs.stat(fullPath); - + if (stats.isDirectory()) { - files.push(...await this.getFilesRecursively(fullPath)); + files.push(...(await this.getFilesRecursively(fullPath))); } else { files.push(fullPath); } } - + return files; } async saveBuildArtifacts(buildType) { - const artifactsDir = path.join(rootDir, 'artifacts', buildType); + const artifactsDir = path.join(rootDir, "artifacts", buildType); await fs.ensureDir(artifactsDir); - + // Copy dist folder - const distPath = path.join(widgetDir, 'dist'); + const distPath = path.join(widgetDir, "dist"); if (await fs.pathExists(distPath)) { - await fs.copy(distPath, path.join(artifactsDir, 'dist')); + await fs.copy(distPath, path.join(artifactsDir, "dist")); } - + // Copy .mpk file const widgetFiles = await fs.readdir(widgetDir); - const mpkFile = widgetFiles.find(f => f.endsWith('.mpk')); - + const mpkFile = widgetFiles.find((f) => f.endsWith(".mpk")); + if (mpkFile) { await fs.copy( path.join(widgetDir, mpkFile), - path.join(artifactsDir, mpkFile) + path.join(artifactsDir, mpkFile), ); } } @@ -172,31 +173,47 @@ class WidgetBuildBenchmark { calculateComparison() { const std = this.results.standardBuild; const hyper = this.results.hyperBuild; - + if (!std.metrics || !hyper.metrics) { return null; } - + return { buildTime: { difference: hyper.metrics.buildTime - std.metrics.buildTime, - percentage: ((hyper.metrics.buildTime - std.metrics.buildTime) / std.metrics.buildTime * 100).toFixed(2) + percentage: ( + ((hyper.metrics.buildTime - std.metrics.buildTime) / + std.metrics.buildTime) * + 100 + ).toFixed(2), }, memoryUsage: { difference: hyper.metrics.memoryUsed - std.metrics.memoryUsed, - percentage: ((hyper.metrics.memoryUsed - std.metrics.memoryUsed) / std.metrics.memoryUsed * 100).toFixed(2) + percentage: ( + ((hyper.metrics.memoryUsed - std.metrics.memoryUsed) / + std.metrics.memoryUsed) * + 100 + ).toFixed(2), }, distSize: { difference: hyper.analysis.distSize - std.analysis.distSize, - percentage: ((hyper.analysis.distSize - std.analysis.distSize) / std.analysis.distSize * 100).toFixed(2) + percentage: ( + ((hyper.analysis.distSize - std.analysis.distSize) / + std.analysis.distSize) * + 100 + ).toFixed(2), }, mpkSize: { difference: hyper.analysis.mpkSize - std.analysis.mpkSize, - percentage: ((hyper.analysis.mpkSize - std.analysis.mpkSize) / std.analysis.mpkSize * 100).toFixed(2) + percentage: ( + ((hyper.analysis.mpkSize - std.analysis.mpkSize) / + std.analysis.mpkSize) * + 100 + ).toFixed(2), }, fileCount: { - difference: hyper.analysis.fileCount - std.analysis.fileCount - } + difference: hyper.analysis.fileCount - std.analysis.fileCount, + }, }; } @@ -206,14 +223,14 @@ class WidgetBuildBenchmark { return; } - console.log('\n' + chalk.bold.cyan('=== Build Benchmark Results ===\n')); + console.log(`\n${chalk.bold.cyan("=== Build Benchmark Results ===\n")}`); // Build metrics table const metricsTable = new Table({ - head: ['Metric', 'Standard Build', 'Hyper Build', 'Difference'], + head: ["Metric", "Standard Build", "Hyper Build", "Difference"], style: { - head: ['cyan'] - } + head: ["cyan"], + }, }); const std = this.results.standardBuild; @@ -223,55 +240,89 @@ class WidgetBuildBenchmark { if (std.metrics && hyper.metrics && comp) { metricsTable.push( [ - 'Build Time', + "Build Time", `${std.metrics.buildTime}ms`, `${hyper.metrics.buildTime}ms`, - this.formatDifference(comp.buildTime.difference, comp.buildTime.percentage, 'ms') + this.formatDifference( + comp.buildTime.difference, + comp.buildTime.percentage, + "ms", + ), ], [ - 'Memory Usage', + "Memory Usage", `${std.metrics.memoryUsed.toFixed(2)}MB`, `${hyper.metrics.memoryUsed.toFixed(2)}MB`, - this.formatDifference(comp.memoryUsage.difference, comp.memoryUsage.percentage, 'MB') + this.formatDifference( + comp.memoryUsage.difference, + comp.memoryUsage.percentage, + "MB", + ), ], [ - 'Dist Size', + "Dist Size", this.formatBytes(std.analysis.distSize), this.formatBytes(hyper.analysis.distSize), - this.formatDifference(comp.distSize.difference, comp.distSize.percentage, 'bytes', true) + this.formatDifference( + comp.distSize.difference, + comp.distSize.percentage, + "bytes", + true, + ), ], [ - 'MPK Size', + "MPK Size", this.formatBytes(std.analysis.mpkSize), this.formatBytes(hyper.analysis.mpkSize), - this.formatDifference(comp.mpkSize.difference, comp.mpkSize.percentage, 'bytes', true) + this.formatDifference( + comp.mpkSize.difference, + comp.mpkSize.percentage, + "bytes", + true, + ), ], [ - 'File Count', + "File Count", std.analysis.fileCount, hyper.analysis.fileCount, - comp.fileCount.difference > 0 ? `+${comp.fileCount.difference}` : `${comp.fileCount.difference}` - ] + comp.fileCount.difference > 0 + ? `+${comp.fileCount.difference}` + : `${comp.fileCount.difference}`, + ], ); console.log(metricsTable.toString()); // Summary - console.log('\n' + chalk.bold('Summary:')); - + console.log(`\n${chalk.bold("Summary:")}`); + const buildTimeImproved = comp.buildTime.difference < 0; const sizeImproved = comp.mpkSize.difference < 0; - + if (buildTimeImproved) { - console.log(chalk.green(`✓ Hyper build is ${Math.abs(comp.buildTime.percentage)}% faster`)); + console.log( + chalk.green( + `✓ Hyper build is ${Math.abs(comp.buildTime.percentage)}% faster`, + ), + ); } else { - console.log(chalk.yellow(`⚠ Hyper build is ${comp.buildTime.percentage}% slower`)); + console.log( + chalk.yellow(`⚠ Hyper build is ${comp.buildTime.percentage}% slower`), + ); } - + if (sizeImproved) { - console.log(chalk.green(`✓ Hyper build produces ${Math.abs(comp.mpkSize.percentage)}% smaller package`)); + console.log( + chalk.green( + `✓ Hyper build produces ${Math.abs(comp.mpkSize.percentage)}% smaller package`, + ), + ); } else if (comp.mpkSize.difference > 0) { - console.log(chalk.yellow(`⚠ Hyper build produces ${comp.mpkSize.percentage}% larger package`)); + console.log( + chalk.yellow( + `⚠ Hyper build produces ${comp.mpkSize.percentage}% larger package`, + ), + ); } else { console.log(chalk.blue(`• Package size is identical`)); } @@ -279,10 +330,12 @@ class WidgetBuildBenchmark { } formatDifference(diff, percentage, unit, isBytes = false) { - const value = isBytes ? this.formatBytes(Math.abs(diff)) : `${Math.abs(diff).toFixed(2)}${unit}`; - const sign = diff > 0 ? '+' : '-'; + const value = isBytes + ? this.formatBytes(Math.abs(diff)) + : `${Math.abs(diff).toFixed(2)}${unit}`; + const sign = diff > 0 ? "+" : "-"; const color = diff > 0 ? chalk.red : chalk.green; - + return color(`${sign}${value} (${sign}${Math.abs(percentage)}%)`); } @@ -294,63 +347,71 @@ class WidgetBuildBenchmark { async run() { try { - this.log('Starting Widget Build Benchmark', 'info'); - this.log(`Widget directory: ${widgetDir}`, 'debug'); + this.log("Starting Widget Build Benchmark", "info"); + this.log(`Widget directory: ${widgetDir}`, "debug"); // Install dependencies if needed - if (!await fs.pathExists(path.join(widgetDir, 'node_modules'))) { - this.log('Installing widget dependencies...'); - execSync('npm install', { cwd: widgetDir, stdio: 'inherit' }); + if (!(await fs.pathExists(path.join(widgetDir, "node_modules")))) { + this.log("Installing widget dependencies..."); + execSync("npm install", { + cwd: widgetDir, + stdio: "inherit", + }); } // Standard build - this.log('\n' + chalk.bold('Phase 1: Standard Build'), 'info'); + this.log(`\n${chalk.bold("Phase 1: Standard Build")}`, "info"); await this.clean(); - const standardMetrics = this.runBuild('build', 'Standard'); - + const standardMetrics = this.runBuild("build", "Standard"); + if (standardMetrics.success) { - const standardAnalysis = await this.analyzeBuildOutput('standard'); - await this.saveBuildArtifacts('standard'); - + const standardAnalysis = await this.analyzeBuildOutput("standard"); + await this.saveBuildArtifacts("standard"); + this.results.standardBuild = { metrics: standardMetrics, - analysis: standardAnalysis + analysis: standardAnalysis, }; } else { - throw new Error('Standard build failed'); + throw new Error("Standard build failed"); } // Hyper build - this.log('\n' + chalk.bold('Phase 2: Hyper Build'), 'info'); + this.log(`\n${chalk.bold("Phase 2: Hyper Build")}`, "info"); await this.clean(); - const hyperMetrics = this.runBuild('build:hyper', 'Hyper'); - + const hyperMetrics = this.runBuild("build:hyper", "Hyper"); + if (hyperMetrics.success) { - const hyperAnalysis = await this.analyzeBuildOutput('hyper'); - await this.saveBuildArtifacts('hyper'); - + const hyperAnalysis = await this.analyzeBuildOutput("hyper"); + await this.saveBuildArtifacts("hyper"); + this.results.hyperBuild = { metrics: hyperMetrics, - analysis: hyperAnalysis + analysis: hyperAnalysis, }; } else { - throw new Error('Hyper build failed'); + throw new Error("Hyper build failed"); } // Calculate comparison this.results.comparison = this.calculateComparison(); // Save results - const resultsPath = path.join(rootDir, 'results', `benchmark-${Date.now()}.json`); - await fs.ensureDir(path.join(rootDir, 'results')); - await fs.writeJson(resultsPath, this.results, { spaces: 2 }); - this.log(`Results saved to: ${resultsPath}`, 'success'); + const resultsPath = path.join( + rootDir, + "results", + `benchmark-${Date.now()}.json`, + ); + await fs.ensureDir(path.join(rootDir, "results")); + await fs.writeJson(resultsPath, this.results, { + spaces: 2, + }); + this.log(`Results saved to: ${resultsPath}`, "success"); // Display results this.displayResults(); - } catch (error) { - this.log(`Benchmark failed: ${error.message}`, 'error'); + this.log(`Benchmark failed: ${error.message}`, "error"); if (this.verbose) { console.error(error); } @@ -361,4 +422,4 @@ class WidgetBuildBenchmark { // Run benchmark const benchmark = new WidgetBuildBenchmark(); -benchmark.run(); \ No newline at end of file +benchmark.run(); diff --git a/benchmark/src/compare.js b/benchmark/src/compare.js index ca08c36..e096781 100644 --- a/benchmark/src/compare.js +++ b/benchmark/src/compare.js @@ -1,24 +1,29 @@ -import fs from 'fs-extra'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import chalk from 'chalk'; -import Table from 'cli-table3'; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import chalk from "chalk"; +import Table from "cli-table3"; +import fs from "fs-extra"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const rootDir = path.join(__dirname, '..'); +const rootDir = path.join(__dirname, ".."); class BuildComparator { constructor() { - this.artifactsDir = path.join(rootDir, 'artifacts'); + this.artifactsDir = path.join(rootDir, "artifacts"); } async compareFiles() { - const standardDir = path.join(this.artifactsDir, 'standard', 'dist'); - const hyperDir = path.join(this.artifactsDir, 'hyper', 'dist'); - - if (!await fs.pathExists(standardDir) || !await fs.pathExists(hyperDir)) { - console.error(chalk.red('Build artifacts not found. Please run benchmark first.')); + const standardDir = path.join(this.artifactsDir, "standard", "dist"); + const hyperDir = path.join(this.artifactsDir, "hyper", "dist"); + + if ( + !(await fs.pathExists(standardDir)) || + !(await fs.pathExists(hyperDir)) + ) { + console.error( + chalk.red("Build artifacts not found. Please run benchmark first."), + ); return; } @@ -26,11 +31,16 @@ class BuildComparator { const hyperFiles = await this.getFileMap(hyperDir); const table = new Table({ - head: ['File', 'Standard Size', 'Hyper Size', 'Difference'], - style: { head: ['cyan'] } + head: ["File", "Standard Size", "Hyper Size", "Difference"], + style: { + head: ["cyan"], + }, }); - const allFiles = new Set([...Object.keys(standardFiles), ...Object.keys(hyperFiles)]); + const allFiles = new Set([ + ...Object.keys(standardFiles), + ...Object.keys(hyperFiles), + ]); let totalStandard = 0; let totalHyper = 0; @@ -39,66 +49,80 @@ class BuildComparator { const stdSize = standardFiles[file] || 0; const hyperSize = hyperFiles[file] || 0; const diff = hyperSize - stdSize; - + totalStandard += stdSize; totalHyper += hyperSize; if (stdSize === 0) { table.push([ file, - '-', + "-", this.formatBytes(hyperSize), - chalk.yellow('New file') + chalk.yellow("New file"), ]); } else if (hyperSize === 0) { table.push([ file, this.formatBytes(stdSize), - '-', - chalk.red('Removed') + "-", + chalk.red("Removed"), ]); } else { const percentage = ((diff / stdSize) * 100).toFixed(1); - const diffStr = diff > 0 - ? chalk.red(`+${this.formatBytes(diff)} (+${percentage}%)`) - : diff < 0 - ? chalk.green(`${this.formatBytes(diff)} (${percentage}%)`) - : chalk.gray('No change'); - + const diffStr = + diff > 0 + ? chalk.red(`+${this.formatBytes(diff)} (+${percentage}%)`) + : diff < 0 + ? chalk.green(`${this.formatBytes(diff)} (${percentage}%)`) + : chalk.gray("No change"); + table.push([ file, this.formatBytes(stdSize), this.formatBytes(hyperSize), - diffStr + diffStr, ]); } } // Add total row table.push([ - chalk.bold('TOTAL'), + chalk.bold("TOTAL"), chalk.bold(this.formatBytes(totalStandard)), chalk.bold(this.formatBytes(totalHyper)), - chalk.bold(this.formatDifference(totalHyper - totalStandard, totalStandard)) + chalk.bold( + this.formatDifference(totalHyper - totalStandard, totalStandard), + ), ]); - console.log('\n' + chalk.bold.cyan('=== File Size Comparison ===\n')); + console.log(`\n${chalk.bold.cyan("=== File Size Comparison ===\n")}`); console.log(table.toString()); // Check for content differences - await this.compareFileContents(standardFiles, hyperFiles, standardDir, hyperDir); + await this.compareFileContents( + standardFiles, + hyperFiles, + standardDir, + hyperDir, + ); } async compareFileContents(standardFiles, hyperFiles, standardDir, hyperDir) { - const jsFiles = Object.keys(standardFiles).filter(f => f.endsWith('.js')); + const jsFiles = Object.keys(standardFiles).filter((f) => f.endsWith(".js")); let identicalCount = 0; let differentCount = 0; for (const file of jsFiles) { if (hyperFiles[file]) { - const stdContent = await fs.readFile(path.join(standardDir, file), 'utf8'); - const hyperContent = await fs.readFile(path.join(hyperDir, file), 'utf8'); - + const stdContent = await fs.readFile( + path.join(standardDir, file), + "utf8", + ); + const hyperContent = await fs.readFile( + path.join(hyperDir, file), + "utf8", + ); + if (stdContent === hyperContent) { identicalCount++; } else { @@ -107,12 +131,12 @@ class BuildComparator { } } - console.log('\n' + chalk.bold('Content Analysis:')); + console.log(`\n${chalk.bold("Content Analysis:")}`); console.log(`• ${identicalCount} files have identical content`); console.log(`• ${differentCount} files have different content`); } - async getFileMap(dir, basePath = '') { + async getFileMap(dir, basePath = "") { const files = {}; const items = await fs.readdir(dir); @@ -146,10 +170,10 @@ class BuildComparator { } else if (diff < 0) { return chalk.green(`${this.formatBytes(diff)} (${percentage}%)`); } - return chalk.gray('No change'); + return chalk.gray("No change"); } } // Run comparison const comparator = new BuildComparator(); -comparator.compareFiles(); \ No newline at end of file +comparator.compareFiles(); diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..47cbfbd --- /dev/null +++ b/biome.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git" + }, + "files": { + "ignoreUnknown": false, + "includes": [ + "**", + "!dist/**/*", + "!**/benchmark/artifacts", + "!**/benchmark/benchmarkWidget" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/package.json b/package.json index 6279a47..5971f15 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,12 @@ "watch": "rslib build --watch", "start": "pnpm build && node ./dist/cli.js", "package": "pnpm build && pnpm pack", - "prepare": "node ./tools/copy-widget-schema.js" + "prepare": "husky", + "lint": "biome check", + "lint:fix": "biome check --write" + }, + "lint-staged": { + "*": "biome check --write" }, "main": "dist/index.cjs", "module": "dist/index.mjs", @@ -35,10 +40,12 @@ ], "author": "Repixel Co., Ltd.", "license": "MIT", - "packageManager": "pnpm@10.15.0", + "packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67", "devDependencies": { + "@biomejs/biome": "2.2.2", "@rslib/core": "0.12.2", "@types/node": "22.17.2", + "husky": "9.1.7", "type-fest": "4.41.0", "typescript": "5.9.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 872c695..c82b972 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 5.0.2 - version: 5.0.2(rolldown-vite@7.1.5(@types/node@22.17.2)(esbuild@0.25.9)(jiti@2.5.1)) + version: 5.0.2(rolldown-vite@7.1.5(@types/node@22.17.2)(esbuild@0.25.9)(jiti@2.5.1)(yaml@2.8.1)) chalk: specifier: 5.6.0 version: 5.6.0 @@ -28,17 +28,23 @@ importers: version: 0.36.0(rollup@4.49.0)(typescript@5.9.2) vite: specifier: npm:rolldown-vite@7.1.5 - version: rolldown-vite@7.1.5(@types/node@22.17.2)(esbuild@0.25.9)(jiti@2.5.1) + version: rolldown-vite@7.1.5(@types/node@22.17.2)(esbuild@0.25.9)(jiti@2.5.1)(yaml@2.8.1) zip-a-folder: specifier: 3.1.9 version: 3.1.9 devDependencies: + '@biomejs/biome': + specifier: 2.2.2 + version: 2.2.2 '@rslib/core': specifier: 0.12.2 version: 0.12.2(typescript@5.9.2) '@types/node': specifier: 22.17.2 version: 22.17.2 + husky: + specifier: 9.1.7 + version: 9.1.7 type-fest: specifier: 4.41.0 version: 4.41.0 @@ -193,6 +199,59 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} + '@biomejs/biome@2.2.2': + resolution: {integrity: sha512-j1omAiQWCkhuLgwpMKisNKnsM6W8Xtt1l0WZmqY/dFj8QPNkIoTvk4tSsi40FaAAkBE1PU0AFG2RWFBWenAn+w==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.2.2': + resolution: {integrity: sha512-6ePfbCeCPryWu0CXlzsWNZgVz/kBEvHiPyNpmViSt6A2eoDf4kXs3YnwQPzGjy8oBgQulrHcLnJL0nkCh80mlQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.2.2': + resolution: {integrity: sha512-Tn4JmVO+rXsbRslml7FvKaNrlgUeJot++FkvYIhl1OkslVCofAtS35MPlBMhXgKWF9RNr9cwHanrPTUUXcYGag==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.2.2': + resolution: {integrity: sha512-/MhYg+Bd6renn6i1ylGFL5snYUn/Ct7zoGVKhxnro3bwekiZYE8Kl39BSb0MeuqM+72sThkQv4TnNubU9njQRw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.2.2': + resolution: {integrity: sha512-JfrK3gdmWWTh2J5tq/rcWCOsImVyzUnOS2fkjhiYKCQ+v8PqM+du5cfB7G1kXas+7KQeKSWALv18iQqdtIMvzw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.2.2': + resolution: {integrity: sha512-ZCLXcZvjZKSiRY/cFANKg+z6Fhsf9MHOzj+NrDQcM+LbqYRT97LyCLWy2AS+W2vP+i89RyRM+kbGpUzbRTYWig==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.2.2': + resolution: {integrity: sha512-Ogb+77edO5LEP/xbNicACOWVLt8mgC+E1wmpUakr+O4nKwLt9vXe74YNuT3T1dUBxC/SnrVmlzZFC7kQJEfquQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.2.2': + resolution: {integrity: sha512-wBe2wItayw1zvtXysmHJQoQqXlTzHSpQRyPpJKiNIR21HzH/CrZRDFic1C1jDdp+zAPtqhNExa0owKMbNwW9cQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.2.2': + resolution: {integrity: sha512-DAuHhHekGfiGb6lCcsT4UyxQmVwQiBCBUMwVra/dcOSs9q8OhfaZgey51MlekT3p8UwRqtXQfFuEJBhJNdLZwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@emnapi/core@1.4.5': resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} @@ -927,6 +986,11 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1371,6 +1435,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + zip-a-folder@3.1.9: resolution: {integrity: sha512-0TPP3eK5mbZxHnOE8w/Jg6gwxsxZOrA3hXHMfC3I4mcTvyJwNt7GZP8i6uiAMVNu43QTmVz0ngEMKcjgpLZLmQ==} @@ -1536,6 +1605,41 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@biomejs/biome@2.2.2': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.2.2 + '@biomejs/cli-darwin-x64': 2.2.2 + '@biomejs/cli-linux-arm64': 2.2.2 + '@biomejs/cli-linux-arm64-musl': 2.2.2 + '@biomejs/cli-linux-x64': 2.2.2 + '@biomejs/cli-linux-x64-musl': 2.2.2 + '@biomejs/cli-win32-arm64': 2.2.2 + '@biomejs/cli-win32-x64': 2.2.2 + + '@biomejs/cli-darwin-arm64@2.2.2': + optional: true + + '@biomejs/cli-darwin-x64@2.2.2': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.2.2': + optional: true + + '@biomejs/cli-linux-arm64@2.2.2': + optional: true + + '@biomejs/cli-linux-x64-musl@2.2.2': + optional: true + + '@biomejs/cli-linux-x64@2.2.2': + optional: true + + '@biomejs/cli-win32-arm64@2.2.2': + optional: true + + '@biomejs/cli-win32-x64@2.2.2': + optional: true + '@emnapi/core@1.4.5': dependencies: '@emnapi/wasi-threads': 1.0.4 @@ -1928,7 +2032,7 @@ snapshots: '@types/scheduler@0.26.0': {} - '@vitejs/plugin-react@5.0.2(rolldown-vite@7.1.5(@types/node@22.17.2)(esbuild@0.25.9)(jiti@2.5.1))': + '@vitejs/plugin-react@5.0.2(rolldown-vite@7.1.5(@types/node@22.17.2)(esbuild@0.25.9)(jiti@2.5.1)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.3 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.3) @@ -1936,7 +2040,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.34 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: rolldown-vite@7.1.5(@types/node@22.17.2)(esbuild@0.25.9)(jiti@2.5.1) + vite: rolldown-vite@7.1.5(@types/node@22.17.2)(esbuild@0.25.9)(jiti@2.5.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -2157,6 +2261,8 @@ snapshots: graceful-fs@4.2.11: {} + husky@9.1.7: {} + ieee754@1.2.1: {} inherits@2.0.4: {} @@ -2369,7 +2475,7 @@ snapshots: dependencies: minimatch: 5.1.6 - rolldown-vite@7.1.5(@types/node@22.17.2)(esbuild@0.25.9)(jiti@2.5.1): + rolldown-vite@7.1.5(@types/node@22.17.2)(esbuild@0.25.9)(jiti@2.5.1)(yaml@2.8.1): dependencies: fdir: 6.5.0(picomatch@4.0.3) lightningcss: 1.30.1 @@ -2382,6 +2488,7 @@ snapshots: esbuild: 0.25.9 fsevents: 2.3.3 jiti: 2.5.1 + yaml: 2.8.1 rolldown@1.0.0-beta.34: dependencies: @@ -2566,6 +2673,9 @@ snapshots: yallist@3.1.1: {} + yaml@2.8.1: + optional: true + zip-a-folder@3.1.9: dependencies: archiver: 7.0.1 diff --git a/rslib.config.ts b/rslib.config.ts index 70632f4..b07ef5f 100644 --- a/rslib.config.ts +++ b/rslib.config.ts @@ -1,68 +1,68 @@ -import { defineConfig } from "@rslib/core"; - -export default defineConfig({ - lib: [ - { - format: 'cjs', - bundle: true, - dts: true, - source: { - entry: { - cli: 'src/cli.ts' - } - }, - output: { - filename: { - js: '[name].js' - } - } - }, - { - format: 'esm', - bundle: true, - dts: true, - source: { - entry: { - index: 'src/index.ts' - } - }, - output: { - filename: { - js: '[name].mjs' - } - } - }, - { - format: 'cjs', - bundle: true, - dts: false, - source: { - entry: { - index: 'src/index.ts' - } - }, - output: { - filename: { - js: '[name].cjs' - } - } - } - ], - output: { - minify: { - js: true, - jsOptions: { - minimizerOptions: { - mangle: true, - minify: true, - compress: { - defaults: true, - unused: true, - dead_code: true, - toplevel: true - } - } - } - } - } -}); \ No newline at end of file +import { defineConfig } from "@rslib/core"; + +export default defineConfig({ + lib: [ + { + format: "cjs", + bundle: true, + dts: true, + source: { + entry: { + cli: "src/cli.ts", + }, + }, + output: { + filename: { + js: "[name].js", + }, + }, + }, + { + format: "esm", + bundle: true, + dts: true, + source: { + entry: { + index: "src/index.ts", + }, + }, + output: { + filename: { + js: "[name].mjs", + }, + }, + }, + { + format: "cjs", + bundle: true, + dts: false, + source: { + entry: { + index: "src/index.ts", + }, + }, + output: { + filename: { + js: "[name].cjs", + }, + }, + }, + ], + output: { + minify: { + js: true, + jsOptions: { + minimizerOptions: { + mangle: true, + minify: true, + compress: { + defaults: true, + unused: true, + dead_code: true, + toplevel: true, + }, + }, + }, + }, + }, +}); diff --git a/src/cli.ts b/src/cli.ts index 7ca916e..0454ef9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,30 +1,34 @@ -#!/usr/bin/env node - -import { program } from "commander"; - -import packageJson from "../package.json"; -import buildWebCommand from "./commands/build/web"; -import startWebCommand from "./commands/start/web"; - -program.version(packageJson.version, '-v, --version', 'display current version'); - -program - .command('build:web') - .summary('build web widget') - .action(async () => { - await buildWebCommand(); - }); - -program - .command('release:web') - .summary('release web widget') - .action(async () => { - await buildWebCommand(true); - }); - -program - .command('start:web') - .summary('start web widget live reload') - .action(startWebCommand); - -program.parse(); \ No newline at end of file +#!/usr/bin/env node + +import { program } from "commander"; + +import packageJson from "../package.json"; +import buildWebCommand from "./commands/build/web"; +import startWebCommand from "./commands/start/web"; + +program.version( + packageJson.version, + "-v, --version", + "display current version", +); + +program + .command("build:web") + .summary("build web widget") + .action(async () => { + await buildWebCommand(); + }); + +program + .command("release:web") + .summary("release web widget") + .action(async () => { + await buildWebCommand(true); + }); + +program + .command("start:web") + .summary("start web widget live reload") + .action(startWebCommand); + +program.parse(); diff --git a/src/commands/build/web/index.ts b/src/commands/build/web/index.ts index 472c400..e799860 100644 --- a/src/commands/build/web/index.ts +++ b/src/commands/build/web/index.ts @@ -1,121 +1,163 @@ -import fs from 'fs/promises'; -import path from 'path'; -import { InlineConfig, UserConfig, build as viteBuild } from 'vite'; -import { zip } from 'zip-a-folder'; - -import { COLOR_ERROR, COLOR_GREEN, DIST_DIRECTORY_NAME, PROJECT_DIRECTORY, VITE_CONFIGURATION_FILENAME, WEB_OUTPUT_DIRECTORY } from '../../../constants'; -import pathIsExists from '../../../utils/pathIsExists'; -import getWidgetVersion from '../../../utils/getWidgetVersion'; -import showMessage from '../../../utils/showMessage'; -import { getEditorConfigDefaultConfig, getEditorPreviewDefaultConfig, getViteDefaultConfig } from '../../../configurations/vite'; -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 { - 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); - const distIsExists = await pathIsExists(distDir); - - if (distIsExists) { - await fs.rm(distDir, { recursive: true, force: true }); - } - - await fs.mkdir(distDir); - - showMessage('Copy resources'); - - const widgetVersion = await getWidgetVersion(); - const outputDir = path.join(distDir, widgetVersion); - - await fs.mkdir(outputDir); - await fs.mkdir(WEB_OUTPUT_DIRECTORY, { recursive: true }); - - const customViteConfigPath = path.join(PROJECT_DIRECTORY, VITE_CONFIGURATION_FILENAME); - const viteConfigIsExists = await pathIsExists(customViteConfigPath); - let resultViteConfig: UserConfig; - - if (viteConfigIsExists) { - const userConfig = await getViteUserConfiguration(customViteConfigPath); - - resultViteConfig = await getViteDefaultConfig(false, userConfig); - } else { - resultViteConfig = await getViteDefaultConfig(false); - } - - const originPackageXmlPath = path.join(PROJECT_DIRECTORY, 'src/package.xml'); - const destPackageXmlPath = path.join(WEB_OUTPUT_DIRECTORY, 'package.xml'); - const destWidgetXmlPath = path.join(WEB_OUTPUT_DIRECTORY, `${widgetName}.xml`); - - await fs.copyFile(originPackageXmlPath, destPackageXmlPath); - await fs.copyFile(originWidgetXmlPath, destWidgetXmlPath); - - showMessage('Start build'); - - const editorConfigViteConfig = await getEditorConfigDefaultConfig(isProduction); - const editorPreviewViteConfig = await getEditorPreviewDefaultConfig(isProduction); - const viteBuildConfigs: InlineConfig[] = [ - { - ...resultViteConfig, - configFile: false, - root: PROJECT_DIRECTORY - }, - { - ...editorConfigViteConfig, - configFile: false, - root: PROJECT_DIRECTORY, - logLevel: 'silent' - }, - { - ...editorPreviewViteConfig, - configFile: false, - root: PROJECT_DIRECTORY, - logLevel: 'silent' - } - ]; - - await Promise.all(viteBuildConfigs.map(async (config) => { - await viteBuild(config); - })); - - showMessage('Generate mpk file'); - - const packageJson = await getWidgetPackageJson(); - const packageName = packageJson.packagePath; - const mpkFileName = `${packageName}.${widgetName}.mpk`; - const mpkFileDestPath = path.join(outputDir, mpkFileName); - const mendixWidgetDirectory = await getMendixWidgetDirectory(); - const mendixMpkFileDestPath = path.join(mendixWidgetDirectory, mpkFileName); - - await zip(WEB_OUTPUT_DIRECTORY, mpkFileDestPath); - 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)}`); - } -}; - -export default buildWebCommand; \ No newline at end of file +import fs from "node:fs/promises"; +import path from "node:path"; +import { type InlineConfig, type UserConfig, build as viteBuild } from "vite"; +import { zip } from "zip-a-folder"; +import { + getEditorConfigDefaultConfig, + getEditorPreviewDefaultConfig, + getViteDefaultConfig, +} from "../../../configurations/vite"; +import { + COLOR_ERROR, + COLOR_GREEN, + DIST_DIRECTORY_NAME, + PROJECT_DIRECTORY, + VITE_CONFIGURATION_FILENAME, + WEB_OUTPUT_DIRECTORY, +} from "../../../constants"; +import { generateTypesFromFile } from "../../../type-generator"; +import getMendixWidgetDirectory from "../../../utils/getMendixWidgetDirectory"; +import getViteUserConfiguration from "../../../utils/getViteUserConfiguration"; +import getWidgetName from "../../../utils/getWidgetName"; +import getWidgetPackageJson from "../../../utils/getWidgetPackageJson"; +import getWidgetVersion from "../../../utils/getWidgetVersion"; +import pathIsExists from "../../../utils/pathIsExists"; +import showMessage from "../../../utils/showMessage"; + +const buildWebCommand = async (isProduction: boolean = false) => { + 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); + const distIsExists = await pathIsExists(distDir); + + if (distIsExists) { + await fs.rm(distDir, { + recursive: true, + force: true, + }); + } + + await fs.mkdir(distDir); + + showMessage("Copy resources"); + + const widgetVersion = await getWidgetVersion(); + const outputDir = path.join(distDir, widgetVersion); + + await fs.mkdir(outputDir); + await fs.mkdir(WEB_OUTPUT_DIRECTORY, { + recursive: true, + }); + + const customViteConfigPath = path.join( + PROJECT_DIRECTORY, + VITE_CONFIGURATION_FILENAME, + ); + const viteConfigIsExists = await pathIsExists(customViteConfigPath); + let resultViteConfig: UserConfig; + + if (viteConfigIsExists) { + const userConfig = await getViteUserConfiguration(customViteConfigPath); + + resultViteConfig = await getViteDefaultConfig(false, userConfig); + } else { + resultViteConfig = await getViteDefaultConfig(false); + } + + const originPackageXmlPath = path.join( + PROJECT_DIRECTORY, + "src/package.xml", + ); + const destPackageXmlPath = path.join(WEB_OUTPUT_DIRECTORY, "package.xml"); + const destWidgetXmlPath = path.join( + WEB_OUTPUT_DIRECTORY, + `${widgetName}.xml`, + ); + + await fs.copyFile(originPackageXmlPath, destPackageXmlPath); + await fs.copyFile(originWidgetXmlPath, destWidgetXmlPath); + + showMessage("Start build"); + + const editorConfigViteConfig = + await getEditorConfigDefaultConfig(isProduction); + const editorPreviewViteConfig = + await getEditorPreviewDefaultConfig(isProduction); + const viteBuildConfigs: InlineConfig[] = [ + { + ...resultViteConfig, + configFile: false, + root: PROJECT_DIRECTORY, + }, + { + ...editorConfigViteConfig, + configFile: false, + root: PROJECT_DIRECTORY, + logLevel: "silent", + }, + { + ...editorPreviewViteConfig, + configFile: false, + root: PROJECT_DIRECTORY, + logLevel: "silent", + }, + ]; + + await Promise.all( + viteBuildConfigs.map(async (config) => { + await viteBuild(config); + }), + ); + + showMessage("Generate mpk file"); + + const packageJson = await getWidgetPackageJson(); + const packageName = packageJson.packagePath; + const mpkFileName = `${packageName}.${widgetName}.mpk`; + const mpkFileDestPath = path.join(outputDir, mpkFileName); + const mendixWidgetDirectory = await getMendixWidgetDirectory(); + const mendixMpkFileDestPath = path.join(mendixWidgetDirectory, mpkFileName); + + await zip(WEB_OUTPUT_DIRECTORY, mpkFileDestPath); + 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)}`, + ); + } +}; + +export default buildWebCommand; diff --git a/src/commands/start/web/index.ts b/src/commands/start/web/index.ts index c825766..d850925 100644 --- a/src/commands/start/web/index.ts +++ b/src/commands/start/web/index.ts @@ -1,136 +1,167 @@ -import fs from 'fs/promises'; -import path from 'path'; -import { UserConfig, createServer } from 'vite'; -import { PluginOption } from 'vite'; - -import { CLI_DIRECTORY, COLOR_ERROR, COLOR_GREEN, PROJECT_DIRECTORY, VITE_CONFIGURATION_FILENAME } from "../../../constants"; -import showMessage from "../../../utils/showMessage"; -import getViteWatchOutputDirectory from "../../../utils/getViteWatchOutputDirectory"; -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'; -import { mendixHotreloadReactPlugin } from '../../../configurations/vite/plugins/mendix-hotreload-react-plugin'; -import { mendixPatchViteClientPlugin } from '../../../configurations/vite/plugins/mendix-patch-vite-client-plugin'; -import typescript from 'rollup-plugin-typescript2'; - -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; - const widgetName = await getWidgetName(); - - if (viteConfigIsExists) { - const userConfig = await getViteUserConfiguration(customViteConfigPath); - - resultViteConfig = await getViteDefaultConfig(false, userConfig); - } else { - resultViteConfig = await getViteDefaultConfig(false); - } - - const viteCachePath = path.join(PROJECT_DIRECTORY, 'node_modules/.vite'); - const viteCachePathExists = await pathIsExists(viteCachePath); - - if (viteCachePathExists) { - await fs.rm(viteCachePath, { recursive: true, force: true }); - } - - const viteServer = await createServer({ - ...resultViteConfig, - root: PROJECT_DIRECTORY, - server: { - fs: { - strict: false - }, - watch: { - usePolling: true, - interval: 100 - }, - }, - plugins: [ - typescript({ - tsconfig: path.join(PROJECT_DIRECTORY, 'tsconfig.json'), - tsconfigOverride: { - compilerOptions: { - jsx: 'preserve', - preserveConstEnums: false, - isolatedModules: false, - declaration: false - } - }, - include: ["src/**/*.ts", "src/**/*.tsx"], - exclude: ["node_modules/**", "src/**/*.d.ts"], - check: false, - }), - ...resultViteConfig.plugins as PluginOption[], - mendixHotreloadReactPlugin(), - mendixPatchViteClientPlugin(), - { - name: 'mendix-xml-watch-plugin', - configureServer(server) { - server.watcher.on('change', (file) => { - if (file.endsWith('xml')) { - generateTyping(); - } - }); - } - }, - ], - }); - - await viteServer.listen(); - - showMessage('Generate hot reload widget'); - - const hotReloadTemplate = path.join(CLI_DIRECTORY, 'src/configurations/hotReload/widget.proxy.js.template'); - const hotReloadContents = await fs.readFile(hotReloadTemplate, 'utf-8'); - const devServerUrl = viteServer.resolvedUrls?.local[0] || ''; - const newHotReloadContents = hotReloadContents - .replaceAll('{{ WIDGET_NAME }}', widgetName) - .replaceAll('{{ DEV_SERVER_URL }}', devServerUrl) - - const distDir = await getViteWatchOutputDirectory(); - const distIsExists = await pathIsExists(distDir); - const hotReloadWidgetPath = path.join(distDir, `${widgetName}.mjs`); - const dummyCssPath = path.join(distDir, `${widgetName}.css`); - - if (distIsExists) { - await fs.rm(distDir, { recursive: true, force: true }); - } - - await fs.mkdir(distDir, { recursive: true }); - await fs.writeFile(hotReloadWidgetPath, newHotReloadContents); - await fs.writeFile(dummyCssPath, ''); - - showMessage(`${COLOR_GREEN('Widget hot reload is ready!')}`); - showMessage(`${COLOR_GREEN('Mendix webpage will refresh shortly. Hot reload will work after refreshing.')}`); - } catch (error) { - showMessage(`${COLOR_ERROR('Build failed.')}\nError occurred: ${COLOR_ERROR((error as Error).message)}`); - } -}; - -export default startWebCommand; \ No newline at end of file +import fs from "node:fs/promises"; +import path from "node:path"; +import typescript from "rollup-plugin-typescript2"; +import { createServer, type PluginOption, type UserConfig } from "vite"; +import { getViteDefaultConfig } from "../../../configurations/vite"; +import { mendixHotreloadReactPlugin } from "../../../configurations/vite/plugins/mendix-hotreload-react-plugin"; +import { mendixPatchViteClientPlugin } from "../../../configurations/vite/plugins/mendix-patch-vite-client-plugin"; +import { + CLI_DIRECTORY, + COLOR_ERROR, + COLOR_GREEN, + PROJECT_DIRECTORY, + VITE_CONFIGURATION_FILENAME, +} from "../../../constants"; +import { generateTypesFromFile } from "../../../type-generator"; +import getViteUserConfiguration from "../../../utils/getViteUserConfiguration"; +import getViteWatchOutputDirectory from "../../../utils/getViteWatchOutputDirectory"; +import getWidgetName from "../../../utils/getWidgetName"; +import pathIsExists from "../../../utils/pathIsExists"; +import showMessage from "../../../utils/showMessage"; + +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; + const widgetName = await getWidgetName(); + + if (viteConfigIsExists) { + const userConfig = await getViteUserConfiguration(customViteConfigPath); + + resultViteConfig = await getViteDefaultConfig(false, userConfig); + } else { + resultViteConfig = await getViteDefaultConfig(false); + } + + const viteCachePath = path.join(PROJECT_DIRECTORY, "node_modules/.vite"); + const viteCachePathExists = await pathIsExists(viteCachePath); + + if (viteCachePathExists) { + await fs.rm(viteCachePath, { + recursive: true, + force: true, + }); + } + + const viteServer = await createServer({ + ...resultViteConfig, + root: PROJECT_DIRECTORY, + server: { + fs: { + strict: false, + }, + watch: { + usePolling: true, + interval: 100, + }, + }, + plugins: [ + typescript({ + tsconfig: path.join(PROJECT_DIRECTORY, "tsconfig.json"), + tsconfigOverride: { + compilerOptions: { + jsx: "preserve", + preserveConstEnums: false, + isolatedModules: false, + declaration: false, + }, + }, + include: ["src/**/*.ts", "src/**/*.tsx"], + exclude: ["node_modules/**", "src/**/*.d.ts"], + check: false, + }), + ...(resultViteConfig.plugins as PluginOption[]), + mendixHotreloadReactPlugin(), + mendixPatchViteClientPlugin(), + { + name: "mendix-xml-watch-plugin", + configureServer(server) { + server.watcher.on("change", (file) => { + if (file.endsWith("xml")) { + generateTyping(); + } + }); + }, + }, + ], + }); + + await viteServer.listen(); + + showMessage("Generate hot reload widget"); + + const hotReloadTemplate = path.join( + CLI_DIRECTORY, + "src/configurations/hotReload/widget.proxy.js.template", + ); + const hotReloadContents = await fs.readFile(hotReloadTemplate, "utf-8"); + const devServerUrl = viteServer.resolvedUrls?.local[0] || ""; + const newHotReloadContents = hotReloadContents + .replaceAll("{{ WIDGET_NAME }}", widgetName) + .replaceAll("{{ DEV_SERVER_URL }}", devServerUrl); + + const distDir = await getViteWatchOutputDirectory(); + const distIsExists = await pathIsExists(distDir); + const hotReloadWidgetPath = path.join(distDir, `${widgetName}.mjs`); + const dummyCssPath = path.join(distDir, `${widgetName}.css`); + + if (distIsExists) { + await fs.rm(distDir, { + recursive: true, + force: true, + }); + } + + await fs.mkdir(distDir, { + recursive: true, + }); + await fs.writeFile(hotReloadWidgetPath, newHotReloadContents); + await fs.writeFile(dummyCssPath, ""); + + showMessage(`${COLOR_GREEN("Widget hot reload is ready!")}`); + showMessage( + `${COLOR_GREEN("Mendix webpage will refresh shortly. Hot reload will work after refreshing.")}`, + ); + } catch (error) { + showMessage( + `${COLOR_ERROR("Build failed.")}\nError occurred: ${COLOR_ERROR((error as Error).message)}`, + ); + } +}; + +export default startWebCommand; diff --git a/src/configurations/typescript/tsconfig.base.json b/src/configurations/typescript/tsconfig.base.json index f02bb19..93a0799 100644 --- a/src/configurations/typescript/tsconfig.base.json +++ b/src/configurations/typescript/tsconfig.base.json @@ -24,4 +24,4 @@ "preserveConstEnums": false, "isolatedModules": false } -} \ No newline at end of file +} diff --git a/src/configurations/vite/index.ts b/src/configurations/vite/index.ts index 1c56827..479e77d 100644 --- a/src/configurations/vite/index.ts +++ b/src/configurations/vite/index.ts @@ -1,153 +1,165 @@ -import { UserConfig } from "vite"; -import react from '@vitejs/plugin-react'; -import path from "path"; -import typescript from "rollup-plugin-typescript2"; - -import getWidgetName from "../../utils/getWidgetName"; -import { PROJECT_DIRECTORY, WEB_OUTPUT_DIRECTORY } from "../../constants"; -import getViteOutputDirectory from "../../utils/getViteOutputDirectory"; -import { PWTConfig } from "../.."; - -export const getEditorConfigDefaultConfig = async (isProduction: boolean): Promise => { - const widgetName = await getWidgetName(); - - return { - plugins: [], - build: { - outDir: WEB_OUTPUT_DIRECTORY, - minify: isProduction ? true : false, - emptyOutDir: false, - sourcemap: isProduction ? false : true, - lib: { - entry: path.join(PROJECT_DIRECTORY, `/src/${widgetName}.editorConfig.ts`), - name: `${widgetName}.editorConfig`, - fileName: () => { - return `${widgetName}.editorConfig.js`; - }, - formats: ['umd'] - }, - }, - }; -}; - -export const getEditorPreviewDefaultConfig = async (isProduction: boolean): Promise => { - const widgetName = await getWidgetName(); - - return { - plugins: [ - react({ - jsxRuntime: 'classic' - }) - ], - define: { - 'process.env': {}, - 'process.env.NODE_ENV': '"production"' - }, - build: { - outDir: WEB_OUTPUT_DIRECTORY, - minify: isProduction ? true : false, - emptyOutDir: false, - sourcemap: isProduction ? false : true, - lib: { - entry: path.join(PROJECT_DIRECTORY, `/src/${widgetName}.editorPreview.tsx`), - name: `${widgetName}.editorPreview`, - fileName: () => { - return `${widgetName}.editorPreview.js`; - }, - formats: ['umd'] - }, - rolldownOptions: { - external: [ - 'react', - 'react-dom', - 'react-dom/client', - 'react/jsx-runtime', - 'react/jsx-dev-runtime', - /^mendix($|\/)/ - ], - output: { - globals: { - react: 'React', - 'react-dom': 'ReactDOM', - 'react-dom/client': 'ReactDOM' - } - } - } - }, - }; -}; - -export const getViteDefaultConfig = async (isProduction: boolean, userCustomConfig?: PWTConfig): Promise => { - const widgetName = await getWidgetName(); - const viteOutputDirectory = await getViteOutputDirectory(); - - return { - plugins: [ - react({ - ...userCustomConfig?.reactPluginOptions || {}, - jsxRuntime: 'classic' - }) - ], - define: { - 'process.env': {}, - 'process.env.NODE_ENV': isProduction ? '"production"' : '"development"' - }, - build: { - outDir: viteOutputDirectory, - minify: isProduction ? true : false, - cssMinify: isProduction ? true : false, - sourcemap: isProduction ? false : true, - lib: { - formats: isProduction ? ['umd'] : ['es', 'umd'], - entry: path.join(PROJECT_DIRECTORY, `/src/${widgetName}.tsx`), - name: widgetName, - fileName: (format, entry) => { - if (format === 'umd') { - return `${widgetName}.js`; - } - - if (format === 'es') { - return `${widgetName}.mjs`; - } - - return entry; - }, - cssFileName: widgetName - }, - rolldownOptions: { - plugins: [ - typescript({ - tsconfig: path.join(PROJECT_DIRECTORY, 'tsconfig.json'), - tsconfigOverride: { - compilerOptions: { - jsx: 'preserve', - preserveConstEnums: false, - isolatedModules: false, - declaration: false - } - }, - include: ["src/**/*.ts", "src/**/*.tsx"], - exclude: ["node_modules/**", "src/**/*.d.ts"], - check: false, - }) - ], - external: [ - 'react', - 'react-dom', - 'react-dom/client', - 'react/jsx-runtime', - 'react/jsx-dev-runtime', - /^mendix($|\/)/ - ], - output: { - globals: { - react: 'React', - 'react-dom': 'ReactDOM', - 'react-dom/client': 'ReactDOM' - } - } - } - }, - ...userCustomConfig - } -}; \ No newline at end of file +import path from "node:path"; +import react from "@vitejs/plugin-react"; +import typescript from "rollup-plugin-typescript2"; +import type { UserConfig } from "vite"; +import type { PWTConfig } from "../.."; +import { PROJECT_DIRECTORY, WEB_OUTPUT_DIRECTORY } from "../../constants"; +import getViteOutputDirectory from "../../utils/getViteOutputDirectory"; +import getWidgetName from "../../utils/getWidgetName"; + +export const getEditorConfigDefaultConfig = async ( + isProduction: boolean, +): Promise => { + const widgetName = await getWidgetName(); + + return { + plugins: [], + build: { + outDir: WEB_OUTPUT_DIRECTORY, + minify: !!isProduction, + emptyOutDir: false, + sourcemap: !isProduction, + lib: { + entry: path.join( + PROJECT_DIRECTORY, + `/src/${widgetName}.editorConfig.ts`, + ), + name: `${widgetName}.editorConfig`, + fileName: () => { + return `${widgetName}.editorConfig.js`; + }, + formats: ["umd"], + }, + }, + }; +}; + +export const getEditorPreviewDefaultConfig = async ( + isProduction: boolean, +): Promise => { + const widgetName = await getWidgetName(); + + return { + plugins: [ + react({ + jsxRuntime: "classic", + }), + ], + define: { + "process.env": {}, + "process.env.NODE_ENV": '"production"', + }, + build: { + outDir: WEB_OUTPUT_DIRECTORY, + minify: !!isProduction, + emptyOutDir: false, + sourcemap: !isProduction, + lib: { + entry: path.join( + PROJECT_DIRECTORY, + `/src/${widgetName}.editorPreview.tsx`, + ), + name: `${widgetName}.editorPreview`, + fileName: () => { + return `${widgetName}.editorPreview.js`; + }, + formats: ["umd"], + }, + rolldownOptions: { + external: [ + "react", + "react-dom", + "react-dom/client", + "react/jsx-runtime", + "react/jsx-dev-runtime", + /^mendix($|\/)/, + ], + output: { + globals: { + react: "React", + "react-dom": "ReactDOM", + "react-dom/client": "ReactDOM", + }, + }, + }, + }, + }; +}; + +export const getViteDefaultConfig = async ( + isProduction: boolean, + userCustomConfig?: PWTConfig, +): Promise => { + const widgetName = await getWidgetName(); + const viteOutputDirectory = await getViteOutputDirectory(); + + return { + plugins: [ + react({ + ...(userCustomConfig?.reactPluginOptions || {}), + jsxRuntime: "classic", + }), + ], + define: { + "process.env": {}, + "process.env.NODE_ENV": isProduction ? '"production"' : '"development"', + }, + build: { + outDir: viteOutputDirectory, + minify: !!isProduction, + cssMinify: !!isProduction, + sourcemap: !isProduction, + lib: { + formats: isProduction ? ["umd"] : ["es", "umd"], + entry: path.join(PROJECT_DIRECTORY, `/src/${widgetName}.tsx`), + name: widgetName, + fileName: (format, entry) => { + if (format === "umd") { + return `${widgetName}.js`; + } + + if (format === "es") { + return `${widgetName}.mjs`; + } + + return entry; + }, + cssFileName: widgetName, + }, + rolldownOptions: { + plugins: [ + typescript({ + tsconfig: path.join(PROJECT_DIRECTORY, "tsconfig.json"), + tsconfigOverride: { + compilerOptions: { + jsx: "preserve", + preserveConstEnums: false, + isolatedModules: false, + declaration: false, + }, + }, + include: ["src/**/*.ts", "src/**/*.tsx"], + exclude: ["node_modules/**", "src/**/*.d.ts"], + check: false, + }), + ], + external: [ + "react", + "react-dom", + "react-dom/client", + "react/jsx-runtime", + "react/jsx-dev-runtime", + /^mendix($|\/)/, + ], + output: { + globals: { + react: "React", + "react-dom": "ReactDOM", + "react-dom/client": "ReactDOM", + }, + }, + }, + }, + ...userCustomConfig, + }; +}; diff --git a/src/configurations/vite/plugins/mendix-hotreload-react-plugin.ts b/src/configurations/vite/plugins/mendix-hotreload-react-plugin.ts index 91e6880..7d3691c 100644 --- a/src/configurations/vite/plugins/mendix-hotreload-react-plugin.ts +++ b/src/configurations/vite/plugins/mendix-hotreload-react-plugin.ts @@ -1,34 +1,49 @@ -import { Plugin } from "vite"; +import type { Plugin } from "vite"; // @note When the React version of Mendix is updated, the following content must also be updated. // @todo Depending on the React version, we need to consider whether there is a way to handle this automatically rather than manually. export function mendixHotreloadReactPlugin(): Plugin { return { - name: 'mendix-hotreload-react-18.2.0', - enforce: 'pre', + name: "mendix-hotreload-react-18.2.0", + enforce: "pre", resolveId(id) { - if (id === 'react') { - return { id: 'mendix:react', external: true }; + if (id === "react") { + return { + id: "mendix:react", + external: true, + }; } - if (id === 'react-dom') { - return { id: 'mendix:react-dom', external: true }; + if (id === "react-dom") { + return { + id: "mendix:react-dom", + external: true, + }; } - if (id === 'react-dom/client') { - return { id: 'mendix:react-dom/client', external: true }; + if (id === "react-dom/client") { + return { + id: "mendix:react-dom/client", + external: true, + }; } - if (id === 'react/jsx-runtime') { - return { id: 'mendix:react/jsx-runtime', external: true }; + if (id === "react/jsx-runtime") { + return { + id: "mendix:react/jsx-runtime", + external: true, + }; } - if (id === 'react/jsx-dev-runtime') { - return { id: 'mendix:react/jsx-dev-runtime', external: true }; + if (id === "react/jsx-dev-runtime") { + return { + id: "mendix:react/jsx-dev-runtime", + external: true, + }; } }, load(id) { - if (id === 'mendix:react') { + if (id === "mendix:react") { return ` const React = window.React; @@ -71,7 +86,7 @@ export function mendixHotreloadReactPlugin(): Plugin { `; } - if (id === 'mendix:react-dom') { + if (id === "mendix:react-dom") { return ` const ReactDOM = window.ReactDOM; @@ -91,7 +106,7 @@ export function mendixHotreloadReactPlugin(): Plugin { `; } - if (id === 'mendix:react-dom/client') { + if (id === "mendix:react-dom/client") { return ` const ReactDOMClient = window.ReactDOMClient; @@ -102,7 +117,7 @@ export function mendixHotreloadReactPlugin(): Plugin { `; } - if (id === 'mendix:react/jsx-runtime') { + if (id === "mendix:react/jsx-runtime") { return ` const ReactJSXRuntime = window.ReactJSXRuntime; @@ -114,7 +129,7 @@ export function mendixHotreloadReactPlugin(): Plugin { `; } - if (id === 'mendix:react/jsx-dev-runtime') { + if (id === "mendix:react/jsx-dev-runtime") { return ` const ReactJSXDevRuntime = window.ReactJSXDevRuntime; @@ -124,6 +139,6 @@ export function mendixHotreloadReactPlugin(): Plugin { export default ReactJSXDevRuntime; `; } - } + }, }; -} \ No newline at end of file +} diff --git a/src/configurations/vite/plugins/mendix-patch-vite-client-plugin.ts b/src/configurations/vite/plugins/mendix-patch-vite-client-plugin.ts index 5513bb2..095290c 100644 --- a/src/configurations/vite/plugins/mendix-patch-vite-client-plugin.ts +++ b/src/configurations/vite/plugins/mendix-patch-vite-client-plugin.ts @@ -1,18 +1,20 @@ -import { Plugin } from "vite"; +import type { Plugin } from "vite"; export function mendixPatchViteClientPlugin(): Plugin { return { - name: 'mendix-patch-vite-client', - enforce: 'pre', - apply: 'serve', + name: "mendix-patch-vite-client", + enforce: "pre", + apply: "serve", configureServer(server) { server.middlewares.use(async (req, res, next) => { - const url = req.url || ''; + const url = req.url || ""; - if (url.includes('@vite/client.mjs')) { - const transformed = await server.transformRequest('/@vite/client.mjs'); - let code = transformed?.code || ''; - const rePageReload = /const\s+pageReload\s*=\s*debounceReload\(\s*(\d+)\s*\)/; + if (url.includes("@vite/client.mjs")) { + const transformed = + await server.transformRequest("/@vite/client.mjs"); + let code = transformed?.code || ""; + const rePageReload = + /const\s+pageReload\s*=\s*debounceReload\(\s*(\d+)\s*\)/; const m = code.match(rePageReload); if (m) { @@ -41,16 +43,22 @@ const __mx_debounceReload = (time) => { }; `; - code = code.replace(rePageReload, `${injectScript}\nconst pageReload = __mx_debounceReload(${delay})`); + code = code.replace( + rePageReload, + `${injectScript}\nconst pageReload = __mx_debounceReload(${delay})`, + ); } - res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader( + "Content-Type", + "application/javascript; charset=utf-8", + ); res.end(code); return; } next(); }); - } + }, }; -} \ No newline at end of file +} diff --git a/src/constants/index.ts b/src/constants/index.ts index e871067..7fe2d98 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,18 +1,24 @@ -import chalk from "chalk"; -import path from "path"; - -export const PROJECT_DIRECTORY = process.cwd(); - -export const CLI_DIRECTORY = path.join(PROJECT_DIRECTORY, 'node_modules/@repixelcorp/hyper-pwt'); - -export const DIST_DIRECTORY_NAME = 'dist'; - -export const WEB_OUTPUT_DIRECTORY = path.join(PROJECT_DIRECTORY, `/${DIST_DIRECTORY_NAME}/tmp/widgets`); - -export const VITE_CONFIGURATION_FILENAME = 'vite.config.mjs'; - -export const COLOR_NAME = chalk.bold.blueBright; - -export const COLOR_ERROR = chalk.bold.red; - -export const COLOR_GREEN = chalk.bold.greenBright; \ No newline at end of file +import path from "node:path"; +import chalk from "chalk"; + +export const PROJECT_DIRECTORY = process.cwd(); + +export const CLI_DIRECTORY = path.join( + PROJECT_DIRECTORY, + "node_modules/@repixelcorp/hyper-pwt", +); + +export const DIST_DIRECTORY_NAME = "dist"; + +export const WEB_OUTPUT_DIRECTORY = path.join( + PROJECT_DIRECTORY, + `/${DIST_DIRECTORY_NAME}/tmp/widgets`, +); + +export const VITE_CONFIGURATION_FILENAME = "vite.config.mjs"; + +export const COLOR_NAME = chalk.bold.blueBright; + +export const COLOR_ERROR = chalk.bold.red; + +export const COLOR_GREEN = chalk.bold.greenBright; diff --git a/src/index.ts b/src/index.ts index d13c5e8..f3f9cfb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,15 @@ -import type { UserConfig } from "vite"; -import reactPlugin from "@vitejs/plugin-react"; - -export type PWTConfig = UserConfig & { - reactPluginOptions?: Parameters[0]; -}; - -export type PWTConfigFnPromise = () => Promise; -export type PWTConfigFn = () => PWTConfig | Promise; - -export function definePWTConfig(config: PWTConfigFn | PWTConfigFnPromise): PWTConfigFn | PWTConfigFnPromise { - return config; -} +import type reactPlugin from "@vitejs/plugin-react"; +import type { UserConfig } from "vite"; + +export type PWTConfig = UserConfig & { + reactPluginOptions?: Parameters[0]; +}; + +export type PWTConfigFnPromise = () => Promise; +export type PWTConfigFn = () => PWTConfig | Promise; + +export function definePWTConfig( + config: PWTConfigFn | PWTConfigFnPromise, +): PWTConfigFn | PWTConfigFnPromise { + return config; +} diff --git a/src/type-generator/generator.ts b/src/type-generator/generator.ts index 372dd79..0b34c7a 100644 --- a/src/type-generator/generator.ts +++ b/src/type-generator/generator.ts @@ -1,146 +1,166 @@ -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)}ContainerProps`; -} - -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 +import { generateHeaderComment } from "./header"; +import type { GenerateTargetPlatform } from "./mendix-types"; +import { generateMendixImports, getMendixImports } from "./mendix-types"; +import { + extractSystemProperties, + generateSystemProps, + getSystemPropsImports, + hasLabelProperty, +} from "./system-props"; +import type { + Property, + PropertyGroup, + SystemProperty, + WidgetDefinition, +} from "./types"; +import { + formatDescription, + mapPropertyTypeToTS, + pascalCase, + sanitizePropertyKey, +} from "./utils"; + +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)}ContainerProps`; +} + +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; +} diff --git a/src/type-generator/header.ts b/src/type-generator/header.ts index 0ff29ba..1237084 100644 --- a/src/type-generator/header.ts +++ b/src/type-generator/header.ts @@ -1,37 +1,43 @@ -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 +import { readFileSync } from "node:fs"; +import { join } from "node: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. + */ + +`; +} diff --git a/src/type-generator/index.ts b/src/type-generator/index.ts index d9cc96c..ff8ba20 100644 --- a/src/type-generator/index.ts +++ b/src/type-generator/index.ts @@ -1,25 +1,36 @@ -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 +import { readFile } from "node:fs/promises"; +import { generateTypeDefinition } from "./generator"; +import type { GenerateTargetPlatform } from "./mendix-types"; +import { parseWidgetXML } from "./parser"; +import { generatePreviewTypeDefinition } from "./preview-types"; + +export { generateTypeDefinition } from "./generator"; +export { parseWidgetXML } from "./parser"; +export { generatePreviewTypeDefinition } from "./preview-types"; +export type { + Property, + PropertyGroup, + PropertyType, + WidgetDefinition, +} 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); +} diff --git a/src/type-generator/mendix-types.ts b/src/type-generator/mendix-types.ts index 624d9b9..76ae1bc 100644 --- a/src/type-generator/mendix-types.ts +++ b/src/type-generator/mendix-types.ts @@ -1,359 +1,385 @@ -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 (baseType.includes('Big')) { - imports.add('Big'); - } - - 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 (baseType.includes('Big')) { - imports.add('Big'); - } - - 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 'Currency': - case 'Decimal': - return 'Big'; - case 'Float': - return 'number'; - 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 'Long': - case 'AutoNumber': - case 'Decimal': - return 'Big'; - case 'Float': - return 'number'; - 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 reactImports = imports.filter(imp => imp === 'ReactNode'); - const bigJsImports = imports.filter(imp => imp === 'Big'); - const mendixImports = imports.filter(imp => imp !== 'ReactNode' && imp !== 'Big'); - - let output = ''; - - if (reactImports.length > 0) { - output += `import { ${reactImports.join(', ')} } from 'react';\n`; - } - - if (mendixImports.length > 0) { - output += `import { ${mendixImports.join(', ')} } from 'mendix';\n`; - } - - if (bigJsImports.length > 0) { - output += `import { ${bigJsImports.join(', ')} } from 'big.js';\n`; - } - - return output; -} +import type { AttributeType, Property } 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 (baseType.includes("Big")) { + imports.add("Big"); + } + + 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 (baseType.includes("Big")) { + imports.add("Big"); + } + + 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 "Currency": + case "Decimal": + return "Big"; + case "Float": + return "number"; + 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 "Long": + case "AutoNumber": + case "Decimal": + return "Big"; + case "Float": + return "number"; + 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 reactImports = imports.filter((imp) => imp === "ReactNode"); + const bigJsImports = imports.filter((imp) => imp === "Big"); + const mendixImports = imports.filter( + (imp) => imp !== "ReactNode" && imp !== "Big", + ); + + let output = ""; + + if (reactImports.length > 0) { + output += `import { ${reactImports.join(", ")} } from 'react';\n`; + } + + if (mendixImports.length > 0) { + output += `import { ${mendixImports.join(", ")} } from 'mendix';\n`; + } + + if (bigJsImports.length > 0) { + output += `import { ${bigJsImports.join(", ")} } from 'big.js';\n`; + } + + return output; +} diff --git a/src/type-generator/parser.ts b/src/type-generator/parser.ts index 3b8bc77..5e2f2e7 100644 --- a/src/type-generator/parser.ts +++ b/src/type-generator/parser.ts @@ -1,194 +1,205 @@ -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 +import { XMLParser } from "fast-xml-parser"; +import type { + AssociationType, + AttributeType, + EnumerationValue, + ParsedXMLAssociationType, + ParsedXMLAttributeType, + ParsedXMLEnumerationValue, + ParsedXMLProperty, + ParsedXMLPropertyGroup, + ParsedXMLSelectionType, + ParsedXMLSystemProperty, + ParsedXMLWidget, + Property, + PropertyGroup, + SelectionType, + SystemProperty, + WidgetDefinition, +} 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): 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 "String", + 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, + })); +} diff --git a/src/type-generator/preview-types.ts b/src/type-generator/preview-types.ts index 9d61759..593b39f 100644 --- a/src/type-generator/preview-types.ts +++ b/src/type-generator/preview-types.ts @@ -1,221 +1,215 @@ -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 +import { + extractSystemProperties, + generatePreviewSystemProps, + hasLabelProperty, +} from "./system-props"; +import type { + Property, + PropertyGroup, + SystemProperty, + WidgetDefinition, +} from "./types"; +import { formatDescription, pascalCase, sanitizePropertyKey } from "./utils"; + +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; +} diff --git a/src/type-generator/system-props.ts b/src/type-generator/system-props.ts index 8b20b02..0c15689 100644 --- a/src/type-generator/system-props.ts +++ b/src/type-generator/system-props.ts @@ -1,87 +1,93 @@ -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 +import type { GenerateTargetPlatform } from "./mendix-types"; +import type { Property, PropertyGroup, SystemProperty } 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; +} diff --git a/src/type-generator/types.ts b/src/type-generator/types.ts index 0398013..a21d07b 100644 --- a/src/type-generator/types.ts +++ b/src/type-generator/types.ts @@ -1,174 +1,182 @@ -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 +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; +} diff --git a/src/type-generator/utils.ts b/src/type-generator/utils.ts index bb87078..59d5f6f 100644 --- a/src/type-generator/utils.ts +++ b/src/type-generator/utils.ts @@ -1,92 +1,92 @@ -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; -} - +import type { GenerateTargetPlatform } from "./mendix-types"; +import { mapPropertyToMendixType } from "./mendix-types"; +import type { AttributeType, Property } from "./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 'Currency': - case 'Decimal': - return 'Big'; - case 'Float': - return 'number'; + case "String": + case "HashString": + case "Enum": + return "string"; + case "Boolean": + return "boolean"; + case "Integer": + case "Long": + case "AutoNumber": + case "Currency": + case "Decimal": + return "Big"; + case "Float": + return "number"; - case 'DateTime': - return 'Date | string'; + case "DateTime": + return "Date | string"; - case 'Binary': - return 'Blob | string'; + case "Binary": + return "Blob | string"; default: - return 'any'; + return "any"; } } - + export function mapReturnTypeToTS(returnType: string): string { switch (returnType) { - case 'Void': - return 'void'; - case 'Boolean': - return 'boolean'; - case 'Integer': - case 'Long': - case 'AutoNumber': - case 'Decimal': - return 'Big'; - case 'Float': - return 'number'; - case 'DateTime': - return 'Date | string'; - case 'String': - return 'string'; - case 'Object': - return 'object'; + case "Void": + return "void"; + case "Boolean": + return "boolean"; + case "Integer": + case "Long": + case "AutoNumber": + case "Decimal": + return "Big"; + case "Float": + return "number"; + case "DateTime": + return "Date | string"; + case "String": + return "string"; + case "Object": + return "object"; default: - return 'any'; + 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(' '); + +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(" "); } diff --git a/src/types.d.ts b/src/types.d.ts index d634f8c..e8a8653 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,13 +1,13 @@ -import { PackageJson } from "type-fest"; - -type WidgetConfig = { - projectPath: string; - mendixHost: string; - developmentPort: number; -} - -export type WidgetPackageJson = PackageJson & { - widgetName: string; - config: WidgetConfig; - packagePath: string; -}; \ No newline at end of file +import { PackageJson } from "type-fest"; + +type WidgetConfig = { + projectPath: string; + mendixHost: string; + developmentPort: number; +}; + +export type WidgetPackageJson = PackageJson & { + widgetName: string; + config: WidgetConfig; + packagePath: string; +}; diff --git a/src/utils/getMendixProjectDirectory.ts b/src/utils/getMendixProjectDirectory.ts index e0b5d83..3fa3576 100644 --- a/src/utils/getMendixProjectDirectory.ts +++ b/src/utils/getMendixProjectDirectory.ts @@ -1,13 +1,13 @@ -import path from "path"; - -import { PROJECT_DIRECTORY } from "../constants"; -import getWidgetPackageJson from "./getWidgetPackageJson"; - -const getMendixProjectDiectory = async () => { - const packageJson = await getWidgetPackageJson(); - const widgetPath = PROJECT_DIRECTORY; - - return path.join(widgetPath, packageJson.config.projectPath); -}; - -export default getMendixProjectDiectory; \ No newline at end of file +import path from "node:path"; + +import { PROJECT_DIRECTORY } from "../constants"; +import getWidgetPackageJson from "./getWidgetPackageJson"; + +const getMendixProjectDiectory = async () => { + const packageJson = await getWidgetPackageJson(); + const widgetPath = PROJECT_DIRECTORY; + + return path.join(widgetPath, packageJson.config.projectPath); +}; + +export default getMendixProjectDiectory; diff --git a/src/utils/getMendixWidgetDirectory.ts b/src/utils/getMendixWidgetDirectory.ts index 2bf26c0..ec8a742 100644 --- a/src/utils/getMendixWidgetDirectory.ts +++ b/src/utils/getMendixWidgetDirectory.ts @@ -1,11 +1,11 @@ -import path from "path"; - -import getMendixProjectDiectory from "./getMendixProjectDirectory"; - -const getMendixWidgetDirectory = async () => { - const mendixPath = await getMendixProjectDiectory(); - - return path.join(mendixPath, 'widgets'); -}; - -export default getMendixWidgetDirectory; \ No newline at end of file +import path from "node:path"; + +import getMendixProjectDiectory from "./getMendixProjectDirectory"; + +const getMendixWidgetDirectory = async () => { + const mendixPath = await getMendixProjectDiectory(); + + return path.join(mendixPath, "widgets"); +}; + +export default getMendixWidgetDirectory; diff --git a/src/utils/getViteOutputDirectory.ts b/src/utils/getViteOutputDirectory.ts index adb2378..ef4c41e 100644 --- a/src/utils/getViteOutputDirectory.ts +++ b/src/utils/getViteOutputDirectory.ts @@ -1,16 +1,21 @@ -import path from "path"; - -import { DIST_DIRECTORY_NAME, PROJECT_DIRECTORY } from "../constants"; -import getWidgetPackageJson from "./getWidgetPackageJson"; - -const getViteOutputDirectory = async (): Promise => { - const packageJson = await getWidgetPackageJson(); - const packagePath = packageJson.packagePath; - const widgetName = packageJson.widgetName; - const packageDirectories = packagePath.split('.'); - const outputDir = path.join(PROJECT_DIRECTORY, `/${DIST_DIRECTORY_NAME}/tmp/widgets`, ...packageDirectories, widgetName.toLowerCase()); - - return outputDir; -}; - -export default getViteOutputDirectory; \ No newline at end of file +import path from "node:path"; + +import { DIST_DIRECTORY_NAME, PROJECT_DIRECTORY } from "../constants"; +import getWidgetPackageJson from "./getWidgetPackageJson"; + +const getViteOutputDirectory = async (): Promise => { + const packageJson = await getWidgetPackageJson(); + const packagePath = packageJson.packagePath; + const widgetName = packageJson.widgetName; + const packageDirectories = packagePath.split("."); + const outputDir = path.join( + PROJECT_DIRECTORY, + `/${DIST_DIRECTORY_NAME}/tmp/widgets`, + ...packageDirectories, + widgetName.toLowerCase(), + ); + + return outputDir; +}; + +export default getViteOutputDirectory; diff --git a/src/utils/getViteUserConfiguration.ts b/src/utils/getViteUserConfiguration.ts index 3483564..07df724 100644 --- a/src/utils/getViteUserConfiguration.ts +++ b/src/utils/getViteUserConfiguration.ts @@ -1,8 +1,9 @@ -import { PWTConfig, PWTConfigFn, PWTConfigFnPromise } from ".."; +import type { PWTConfig, PWTConfigFn, PWTConfigFnPromise } from ".."; const getViteUserConfiguration = async (path: string): Promise => { const getUserConfig = await import(`file://${path}`); - const getUserConfigFn: PWTConfigFn | PWTConfigFnPromise = getUserConfig.default; + const getUserConfigFn: PWTConfigFn | PWTConfigFnPromise = + getUserConfig.default; const userConfig = getUserConfigFn(); if (userConfig instanceof Promise) { @@ -14,4 +15,4 @@ const getViteUserConfiguration = async (path: string): Promise => { return userConfig; }; -export default getViteUserConfiguration; \ No newline at end of file +export default getViteUserConfiguration; diff --git a/src/utils/getViteWatchOutputDirectory.ts b/src/utils/getViteWatchOutputDirectory.ts index 69c6c6f..03ab31f 100644 --- a/src/utils/getViteWatchOutputDirectory.ts +++ b/src/utils/getViteWatchOutputDirectory.ts @@ -1,22 +1,22 @@ -import path from "path"; - -import { DIST_DIRECTORY_NAME, PROJECT_DIRECTORY } from "../constants"; -import getWidgetPackageJson from "./getWidgetPackageJson"; - -const getViteWatchOutputDirectory = async (): Promise => { - const packageJson = await getWidgetPackageJson(); - const packagePath = packageJson.packagePath; - const widgetName = packageJson.widgetName; - const packageDirectories = packagePath.split('.'); - const outputDir = path.join( - PROJECT_DIRECTORY, - packageJson.config.projectPath, - 'deployment/web/widgets', - ...packageDirectories, - widgetName.toLowerCase() - ); - - return outputDir; -}; - -export default getViteWatchOutputDirectory; \ No newline at end of file +import path from "node:path"; + +import { PROJECT_DIRECTORY } from "../constants"; +import getWidgetPackageJson from "./getWidgetPackageJson"; + +const getViteWatchOutputDirectory = async (): Promise => { + const packageJson = await getWidgetPackageJson(); + const packagePath = packageJson.packagePath; + const widgetName = packageJson.widgetName; + const packageDirectories = packagePath.split("."); + const outputDir = path.join( + PROJECT_DIRECTORY, + packageJson.config.projectPath, + "deployment/web/widgets", + ...packageDirectories, + widgetName.toLowerCase(), + ); + + return outputDir; +}; + +export default getViteWatchOutputDirectory; diff --git a/src/utils/getWidgetName.ts b/src/utils/getWidgetName.ts index 757a140..77cae90 100644 --- a/src/utils/getWidgetName.ts +++ b/src/utils/getWidgetName.ts @@ -1,13 +1,15 @@ -import getWidgetPackageJson from './getWidgetPackageJson'; - -const getWidgetName = async (): Promise => { - const widgetPackageJson = await getWidgetPackageJson(); - - if (!widgetPackageJson.widgetName) { - throw new Error('widget name is missing, please check your package.json file.'); - } - - return widgetPackageJson.widgetName; -}; - -export default getWidgetName; \ No newline at end of file +import getWidgetPackageJson from "./getWidgetPackageJson"; + +const getWidgetName = async (): Promise => { + const widgetPackageJson = await getWidgetPackageJson(); + + if (!widgetPackageJson.widgetName) { + throw new Error( + "widget name is missing, please check your package.json file.", + ); + } + + return widgetPackageJson.widgetName; +}; + +export default getWidgetName; diff --git a/src/utils/getWidgetPackageJson.ts b/src/utils/getWidgetPackageJson.ts index 5034f40..41a7e99 100644 --- a/src/utils/getWidgetPackageJson.ts +++ b/src/utils/getWidgetPackageJson.ts @@ -1,19 +1,19 @@ -import fs from 'fs/promises'; -import path from 'path'; - -import { PROJECT_DIRECTORY } from '../constants'; -import { WidgetPackageJson } from '../types'; - -const getWidgetPackageJson = async (): Promise => { - try { - const packageJsonPath = path.join(PROJECT_DIRECTORY, 'package.json'); - const packageJsonFile = await fs.readFile(packageJsonPath, 'utf-8'); - const packageJsonData: WidgetPackageJson = JSON.parse(packageJsonFile); - - return packageJsonData; - } catch (error) { - throw new Error('package.json file is not exists.'); - } -}; - -export default getWidgetPackageJson; \ No newline at end of file +import fs from "node:fs/promises"; +import path from "node:path"; + +import { PROJECT_DIRECTORY } from "../constants"; +import type { WidgetPackageJson } from "../types"; + +const getWidgetPackageJson = async (): Promise => { + try { + const packageJsonPath = path.join(PROJECT_DIRECTORY, "package.json"); + const packageJsonFile = await fs.readFile(packageJsonPath, "utf-8"); + const packageJsonData: WidgetPackageJson = JSON.parse(packageJsonFile); + + return packageJsonData; + } catch (_error) { + throw new Error("package.json file is not exists."); + } +}; + +export default getWidgetPackageJson; diff --git a/src/utils/getWidgetVersion.ts b/src/utils/getWidgetVersion.ts index 6bc2471..d971209 100644 --- a/src/utils/getWidgetVersion.ts +++ b/src/utils/getWidgetVersion.ts @@ -1,13 +1,15 @@ -import getWidgetPackageJson from './getWidgetPackageJson'; - -const getWidgetVersion = async (): Promise => { - const widgetPackageJson = await getWidgetPackageJson(); - - if (!widgetPackageJson.version) { - throw new Error('widget version is missing, please check your package.json file.'); - } - - return widgetPackageJson.version; -}; - -export default getWidgetVersion; \ No newline at end of file +import getWidgetPackageJson from "./getWidgetPackageJson"; + +const getWidgetVersion = async (): Promise => { + const widgetPackageJson = await getWidgetPackageJson(); + + if (!widgetPackageJson.version) { + throw new Error( + "widget version is missing, please check your package.json file.", + ); + } + + return widgetPackageJson.version; +}; + +export default getWidgetVersion; diff --git a/src/utils/pathIsExists.ts b/src/utils/pathIsExists.ts index 1c26262..8e33753 100644 --- a/src/utils/pathIsExists.ts +++ b/src/utils/pathIsExists.ts @@ -1,13 +1,13 @@ -import fs from "fs/promises"; - -const pathIsExists = async (directoryPath: string): Promise => { - try { - await fs.access(directoryPath, fs.constants.F_OK); - - return true; - } catch (error) { - return false; - } -}; - -export default pathIsExists; \ No newline at end of file +import fs from "node:fs/promises"; + +const pathIsExists = async (directoryPath: string): Promise => { + try { + await fs.access(directoryPath, fs.constants.F_OK); + + return true; + } catch (_error) { + return false; + } +}; + +export default pathIsExists; diff --git a/src/utils/showMessage.ts b/src/utils/showMessage.ts index 519f476..85f7d64 100644 --- a/src/utils/showMessage.ts +++ b/src/utils/showMessage.ts @@ -1,7 +1,7 @@ -import { COLOR_NAME } from "../constants"; - -const showMessage = (message: string) => { - console.log(`${COLOR_NAME('[hyper-pwt]')} ${message}`); -}; - -export default showMessage; \ No newline at end of file +import { COLOR_NAME } from "../constants"; + +const showMessage = (message: string) => { + console.log(`${COLOR_NAME("[hyper-pwt]")} ${message}`); +}; + +export default showMessage; diff --git a/tools/copy-widget-schema.js b/tools/copy-widget-schema.js index 9fafc2d..5b25f80 100644 --- a/tools/copy-widget-schema.js +++ b/tools/copy-widget-schema.js @@ -1,20 +1,26 @@ -const fs = require('fs'); -const path = require('path'); +const fs = require("node:fs"); +const path = require("node:path"); -const sourcePath = path.join(__dirname, '..', 'node_modules', 'mendix', 'custom_widget.xsd'); -const targetPath = path.join(__dirname, '..', 'custom_widget.xsd'); +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'); + + 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'); + 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); + console.error("Failed to copy custom_widget.xsd:", error.message); process.exit(1); -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index f59ef9e..6a77df9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,5 +8,5 @@ "module": "preserve", "moduleResolution": "bundler", "target": "esnext" - }, -} \ No newline at end of file + } +}