Skip to content

Commit c644783

Browse files
authored
[WTF-2641]: Format generated types (#182)
## Checklist - Contains unit tests ❌ - Contains breaking changes ❌ - Did you update version and changelog? ✅ - PR title properly formatted (`[XX-000]: description`)? ✅ ## This PR contains - [x] Bug fix - [ ] Feature - [ ] Refactor - [ ] Documentation - [ ] Other (describe) ## What is the purpose of this PR? Avoid linting errors when running the `release` command. As a side-effect, the generated types will now respect the widget's prettier config. ## Relevant changes Before writing the generated types to the definition file, the type generator now runs prettier to format the source. ## What should be covered while testing? #### Widget with many client types **Prerequisites** - A widget (new or existing) **Steps** 1. Modify the widget xml to include properties of these [types](https://docs.mendix.com/apidocs-mxsdk/apidocs/pluggable-widgets-property-types): - Expression Property - Action Property - Attribute Property - List Datasource - Linked ActionProperty - Linked AttributeProperty - Association Property with ReferenceSet - File Property - Image Property - Icon Property - Selection Property (Multi and Single) 2. Run `npx @mendix/pluggable-widgets-tools build` 3. Run `npx @mendix/pluggable-widgets-tools lint` **Result** - With this version of PWT: Step 3 should run without errors - With earlier version of PWT: Step 3 will report formatting errors on the typings file #### Widget with custom prettier rules **Prerequisites** - A widget (new or existing) **Steps** 1. Edit the widget's `prettier.config.js`, add the following settings: ```js { semi: false, singleQuote: true } ``` 4. Run `npx @mendix/pluggable-widgets-tools build` 5. Run `npx @mendix/pluggable-widgets-tools lint` **Result** - With this version of PWT: Step 3 should run without errors - With earlier version of PWT: Step 3 will report formatting errors on the typings file
2 parents c483b98 + 7aae11b commit c644783

14 files changed

Lines changed: 152 additions & 91 deletions

File tree

packages/pluggable-widgets-tools/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1919
- The `jest-jasmine2` runner has been removed. Tests using Jasmine-specific globals (`jasmine.createSpy()`, `jasmine.objectContaining()`, etc.) will throw `ReferenceError: jasmine is not defined`. Replace with Jest equivalents: `jest.fn()`, `expect.objectContaining()`.
2020
- Consumers who extended the base config with `globals['ts-jest']` options must migrate those settings to the `@swc/jest` transform config.
2121

22+
### Fixed
23+
24+
- We updated the type generator to format types according to prettier. Inconsistencies would block the `release` command.
25+
26+
2227
## [11.11.0] - 2026-06-04
2328

2429
### Added

packages/pluggable-widgets-tools/bin/mx-scripts.js

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
#! /usr/bin/env node
22
const { execSync, spawnSync } = require("child_process");
33
const { existsSync } = require("fs");
4-
const { delimiter, dirname, join, parse } = require("path");
4+
const { delimiter, join, parse } = require("path");
55
const { checkMigration } = require("../utils/migration");
66
const { checkForEnzymeUsage } = require("../dist/utils/enzyme-detector");
77
const { red, blue, bold, whiteBright } = require("ansi-colors");
88
const semver = require("semver");
9-
const { auditPluggableWidgetsTools } = require("../dist/commands/audit");
9+
const { auditPluggableWidgetsTools } = require("../dist/commands/audit.js");
10+
const { prettierConfigPath } = require("../dist/utils/formatting.js");
11+
const { toolsRoot, widgetRoot } = require("../dist/widget/paths.js");
1012

1113
checkNodeVersion();
1214
(async () => {
@@ -16,10 +18,7 @@ checkNodeVersion();
1618
console.log(red("An error occurred while checking migration dependencies: ", e));
1719
}
1820

19-
const [, currentScriptPath, cmd, ...args] = process.argv;
20-
const toolsRoot = currentScriptPath.endsWith("pluggable-widgets-tools")
21-
? join(dirname(currentScriptPath), "../@mendix/pluggable-widgets-tools")
22-
: join(dirname(currentScriptPath), "..");
21+
const [, , cmd, ...args] = process.argv;
2322

2423
if (args.indexOf("--subprojectPath") > -1) {
2524
args.splice(args.indexOf("--subprojectPath"), 2);
@@ -46,7 +45,7 @@ checkNodeVersion();
4645
const commandWithArgs = realCommand + " " + args.join(" ");
4746
for (const subCommand of commandWithArgs.split(/&&/g)) {
4847
const result = spawnSync(subCommand.trim(), [], {
49-
cwd: process.cwd(),
48+
cwd: widgetRoot,
5049
env: {
5150
...process.env,
5251
PATH: [process.env.PATH].concat(nodeModulesBins).join(delimiter),
@@ -64,10 +63,8 @@ checkNodeVersion();
6463
}
6564
})();
6665

67-
function getRealCommand(cmd, toolsRoot) {
66+
function getRealCommand(cmd) {
6867
const eslintCommand = "eslint --config .eslintrc.js --ext .jsx,.js,.ts,.tsx src";
69-
const prettierConfigRootPath = join(__dirname, "../../../prettier.config.js");
70-
const prettierConfigPath = existsSync(prettierConfigRootPath) ? prettierConfigRootPath : "prettier.config.js";
7168
const prettierCommand = `prettier --config "${prettierConfigPath}" "{src,typings,tests}/**/*.{js,jsx,ts,tsx,scss}"`;
7269
const rollupCommandWeb = `rollup --config "${join(toolsRoot, "configs/rollup.config.mjs")}"`;
7370
const rollupCommandNative = `rollup --config "${join(toolsRoot, "configs/rollup.config.native.mjs")}"`;
@@ -147,7 +144,7 @@ function findNodeModulesBin() {
147144
}
148145

149146
function checkNodeVersion() {
150-
const packageJson = require(join(__dirname, "../package.json"));
147+
const packageJson = require(join(toolsRoot, "./package.json"));
151148
const nodeRange = new semver.Range(packageJson.engines.node);
152149

153150
console.log("Checking node and npm version...");

packages/pluggable-widgets-tools/configs/rollup.config.mjs

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,21 @@ import postcss from "rollup-plugin-postcss";
2020
import terser from "@rollup/plugin-terser";
2121
import shelljs from "shelljs";
2222
import { widgetTyping } from "./rollup-plugin-widget-typing.mjs";
23-
import {
24-
editorConfigEntry,
25-
isTypescript,
26-
previewEntry,
27-
projectPath,
28-
sourcePath,
29-
widgetEntry,
30-
widgetName,
31-
widgetPackage,
32-
widgetVersion,
33-
onwarn
34-
} from "./shared.mjs";
23+
import { editorConfigEntry, isTypescript, previewEntry, projectPath, widgetEntry, onwarn } from "./shared.mjs";
3524
import { copyLicenseFile, createMpkFile, licenseCustomTemplate } from "./helpers/rollup-helper.mjs";
3625
import url from "./rollup-plugin-assets.mjs";
26+
import { widgetName, widgetOrganization, widgetVersion } from "../dist/widget/package.js";
27+
import { widgetRoot } from "../dist/widget/paths.js";
3728

3829
const { loadConfigFile } = rollupLoadConfigFile;
3930
const { cp } = shelljs;
4031

41-
const outDir = join(sourcePath, "/dist/tmp/widgets/");
42-
const outWidgetDir = join(widgetPackage.replace(/\./g, "/"), widgetName.toLowerCase());
32+
const outDir = join(widgetRoot, "/dist/tmp/widgets/");
33+
const outWidgetDir = join(widgetOrganization.replace(/\./g, "/"), widgetName.toLowerCase());
4334
const outWidgetFile = join(outWidgetDir, `${widgetName}`);
4435
const absoluteOutPackageDir = join(outDir, outWidgetDir);
45-
const mpkDir = join(sourcePath, "dist", widgetVersion);
46-
const mpkFile = join(mpkDir, process.env.MPKOUTPUT ? process.env.MPKOUTPUT : `${widgetPackage}.${widgetName}.mpk`);
36+
const mpkDir = join(widgetRoot, "dist", widgetVersion);
37+
const mpkFile = join(mpkDir, process.env.MPKOUTPUT ? process.env.MPKOUTPUT : `${widgetOrganization}.${widgetName}.mpk`);
4738
const assetsDirName = "assets";
4839
const absoluteOutAssetsDir = join(absoluteOutPackageDir, assetsDirName);
4940
const outAssetsDir = join(outWidgetDir, assetsDirName);
@@ -208,8 +199,8 @@ export default async args => {
208199
});
209200
}
210201

211-
const customConfigPathJS = join(sourcePath, "rollup.config.js");
212-
const customConfigPathESM = join(sourcePath, "rollup.config.mjs");
202+
const customConfigPathJS = join(widgetRoot, "rollup.config.js");
203+
const customConfigPathESM = join(widgetRoot, "rollup.config.mjs");
213204
const existingConfigPath = existsSync(customConfigPathJS)
214205
? customConfigPathJS
215206
: existsSync(customConfigPathESM)
@@ -300,7 +291,7 @@ export default async args => {
300291
// configs affected by a change => we cannot know in advance which one will be "the last".
301292
// So we run the same logic for all configs, letting the last one win.
302293
command([
303-
async () => config.licenses && copyLicenseFile(sourcePath, outDir),
294+
async () => config.licenses && copyLicenseFile(widgetRoot, outDir),
304295
async () =>
305296
createMpkFile({
306297
mpkDir,
@@ -316,7 +307,7 @@ export default async args => {
316307

317308
function getClientComponentPlugins() {
318309
return [
319-
isTypescript ? widgetTyping({ sourceDir: join(sourcePath, "src") }) : null,
310+
isTypescript ? widgetTyping({ sourceDir: join(widgetRoot, "src") }) : null,
320311
clear({ targets: [outDir, mpkDir] }),
321312
command([
322313
() => {

packages/pluggable-widgets-tools/configs/rollup.config.native.mjs

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,18 @@ import terser from "@rollup/plugin-terser";
1616
import shelljs from "shelljs";
1717
import { widgetTyping } from "./rollup-plugin-widget-typing.mjs";
1818
import { collectDependencies } from "./rollup-plugin-collect-dependencies.mjs";
19-
import {
20-
editorConfigEntry,
21-
isTypescript,
22-
projectPath,
23-
sourcePath,
24-
widgetEntry,
25-
widgetName,
26-
widgetPackage,
27-
widgetVersion,
28-
onwarn
29-
} from "./shared.mjs";
19+
import { editorConfigEntry, isTypescript, projectPath, widgetEntry, onwarn } from "./shared.mjs";
3020
import { copyLicenseFile, createMpkFile, licenseCustomTemplate } from "./helpers/rollup-helper.mjs";
21+
import { widgetName, widgetOrganization, widgetVersion } from "../dist/widget/package.js";
22+
import { widgetRoot } from "../dist/widget/paths.js";
3123

3224
const { cp } = shelljs;
3325
const { blue } = colors;
3426

35-
const outDir = join(sourcePath, "/dist/tmp/widgets/");
36-
const outWidgetFile = join(widgetPackage.replace(/\./g, "/"), widgetName.toLowerCase(), `${widgetName}`);
37-
const mpkDir = join(sourcePath, "dist", widgetVersion);
38-
const mpkFile = join(mpkDir, process.env.MPKOUTPUT ? process.env.MPKOUTPUT : `${widgetPackage}.${widgetName}.mpk`);
27+
const outDir = join(widgetRoot, "/dist/tmp/widgets/");
28+
const outWidgetFile = join(widgetOrganization.replace(/\./g, "/"), widgetName.toLowerCase(), `${widgetName}`);
29+
const mpkDir = join(widgetRoot, "dist", widgetVersion);
30+
const mpkFile = join(mpkDir, process.env.MPKOUTPUT ? process.env.MPKOUTPUT : `${widgetOrganization}.${widgetName}.mpk`);
3931

4032
const extensions = [".js", ".jsx", ".tsx", ".ts"];
4133

@@ -161,8 +153,8 @@ export default async args => {
161153
});
162154
}
163155

164-
const customConfigPathJS = join(sourcePath, "rollup.config.js");
165-
const customConfigPathESM = join(sourcePath, "rollup.config.mjs");
156+
const customConfigPathJS = join(widgetRoot, "rollup.config.js");
157+
const customConfigPathESM = join(widgetRoot, "rollup.config.mjs");
166158
const existingConfigPath = existsSync(customConfigPathJS)
167159
? customConfigPathJS
168160
: existsSync(customConfigPathESM)
@@ -237,7 +229,7 @@ export default async args => {
237229
// configs affected by a change => we cannot know in advance which one will be "the last".
238230
// So we run the same logic for all configs, letting the last one win.
239231
command([
240-
async () => config.licenses && copyLicenseFile(sourcePath, outDir),
232+
async () => config.licenses && copyLicenseFile(widgetRoot, outDir),
241233
async () =>
242234
createMpkFile({
243235
mpkDir,
@@ -253,7 +245,7 @@ export default async args => {
253245

254246
function getClientComponentPlugins() {
255247
return [
256-
isTypescript ? widgetTyping({ sourceDir: join(sourcePath, "src") }) : null,
248+
isTypescript ? widgetTyping({ sourceDir: join(widgetRoot, "src") }) : null,
257249
clear({ targets: [outDir, mpkDir] }),
258250
command([
259251
() => {

packages/pluggable-widgets-tools/configs/shared.mjs

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { existsSync, readdirSync, promises as fs, readFileSync } from "node:fs";
1+
import { existsSync, readdirSync, promises as fs } from "node:fs";
22
import { join, relative } from "node:path";
33
import { config } from "dotenv";
44
import colors from "ansi-colors";
5-
import { throwOnIllegalChars, throwOnNoMatch } from "../dist/utils/validation.js";
5+
import { dir as sourcePath, widgetName, json } from "../dist/widget/package.js";
66

7-
config({ path: join(process.cwd(), ".env"), quiet: true });
7+
config({ path: join(sourcePath, ".env"), quiet: true });
88

99
export async function listDir(path) {
1010
const entries = await fs.readdir(path, { withFileTypes: true });
@@ -14,20 +14,6 @@ export async function listDir(path) {
1414
.concat(...(await Promise.all(entries.filter(e => e.isDirectory()).map(e => listDir(join(path, e.name))))));
1515
}
1616

17-
export const sourcePath = process.cwd();
18-
19-
const widgetPackageJson = JSON.parse(readFileSync(join(sourcePath, "package.json")));
20-
export const widgetName = widgetPackageJson.widgetName;
21-
export const widgetPackage = widgetPackageJson.packagePath;
22-
export const widgetVersion = widgetPackageJson.version;
23-
if (!widgetName || !widgetPackageJson) {
24-
throw new Error("Widget does not define widgetName in its package.json");
25-
}
26-
27-
throwOnIllegalChars(widgetName, "a-zA-Z", "The `widgetName` property in package.json");
28-
throwOnIllegalChars(widgetPackage, "a-zA-Z0-9_.-", "The `packagePath` property in package.json");
29-
throwOnNoMatch(widgetPackage, /^([a-zA-Z0-9_-]+.)*[a-zA-Z0-9_-]+$/, "The `packagePath` property in package.json");
30-
3117
const widgetSrcFiles = readdirSync(join(sourcePath, "src")).map(file => join(sourcePath, "src", file));
3218
export const widgetEntry = widgetSrcFiles.filter(file =>
3319
file.match(new RegExp(`[/\\\\]${escape(widgetName)}\\.[jt]sx?$`, "i"))
@@ -47,7 +33,7 @@ export const isTypescript = [widgetEntry, editorConfigEntry, previewEntry].some(
4733

4834
export const projectPath = [
4935
process.env.MX_PROJECT_PATH,
50-
widgetPackageJson.config.projectPath,
36+
json.config.projectPath,
5137
join(sourcePath, "tests/testProject")
5238
].filter(path => path && existsSync(path))[0];
5339

packages/pluggable-widgets-tools/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mendix/pluggable-widgets-tools",
3-
"version": "11.11.0",
3+
"version": "11.12.0",
44
"description": "Mendix Pluggable Widgets Tools",
55
"engines": {
66
"node": ">=20"

packages/pluggable-widgets-tools/src/commands/audit.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { exec } from "node:child_process";
55
import { maxSatisfying, minSatisfying } from "semver";
66
import { confirm } from "../cli/confirm";
77
import { readFile, writeFile } from "node:fs";
8-
import { join } from "node:path";
8+
import { path as packageJsonPath } from "../widget/package";
99

1010
const pluggableWidgetsTools = "@mendix/pluggable-widgets-tools" as NpmAudit.PackageName;
1111

@@ -58,7 +58,6 @@ export async function auditPluggableWidgetsTools(fix: boolean = false) {
5858
(fix || (await confirm("Add overrides to package.json for vulnerable packages?")))
5959
) {
6060
console.log("Adding overrides");
61-
const packageJsonPath = join(process.cwd(), "package.json");
6261
const widgetPackage = await promisify(readFile)(packageJsonPath, "utf8").then(raw => JSON.parse(raw));
6362

6463
const overrides = updateable

packages/pluggable-widgets-tools/src/typings-generator/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { parseStringPromise } from "xml2js";
44
import { PackageXml } from "./PackageXml";
55
import { WidgetXml } from "./WidgetXml";
66
import { generateForWidget } from "./generate";
7+
import { formatTypeScript } from "../utils/formatting";
78

89
const { mkdir, readFile, stat, writeFile } = promises;
910

@@ -29,7 +30,7 @@ export async function transformPackage(content: string, basePath: string) {
2930
const sourcePath = widgetFileXml.$.path;
3031
const source = await readFile(join(basePath, sourcePath), "utf-8");
3132

32-
let generatedContent;
33+
let generatedContent: string;
3334
try {
3435
const sourceXml = (await parseStringPromise(source)) as WidgetXml;
3536
generatedContent = generateForWidget(sourceXml, toWidgetName(sourcePath));
@@ -39,8 +40,9 @@ export async function transformPackage(content: string, basePath: string) {
3940
);
4041
}
4142

43+
const formattedContent = await formatTypeScript(generatedContent);
4244
const resultPath = sourcePath.replace(/(\.xml)?$/, "Props.d.ts");
43-
await writeFile(join(resultBasePath, resultPath), generatedContent);
45+
await writeFile(join(resultBasePath, resultPath), formattedContent);
4446
}
4547
}
4648

packages/pluggable-widgets-tools/src/utils/enzyme-detector.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
22
import { join } from "path";
33
import { yellow } from "ansi-colors";
4+
import { widgetRoot } from "../widget/paths";
45

56
export function checkForEnzymeUsage(srcDir: string = "src"): void {
6-
const projectRoot = process.cwd();
7-
const srcPath = join(projectRoot, srcDir);
7+
const srcPath = join(widgetRoot, srcDir);
88

99
if (!existsSync(srcPath)) {
1010
return;
@@ -36,7 +36,7 @@ export function checkForEnzymeUsage(srcDir: string = "src"): void {
3636
content
3737
)
3838
) {
39-
enzymeFiles.push(fullPath.replace(projectRoot, "."));
39+
enzymeFiles.push(fullPath.replace(widgetRoot, "."));
4040
}
4141
}
4242
} catch (error) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { existsSync } from "fs";
2+
import { join } from "path";
3+
import { format, Options } from "prettier";
4+
import { toolsRoot, widgetRoot } from "../widget/paths";
5+
6+
const baseConfigPath = join(toolsRoot, "./configs/prettier.base.json");
7+
const widgetConfigPath = join(widgetRoot, "./prettier.config.js");
8+
export const prettierConfigPath = existsSync(widgetConfigPath) ? widgetConfigPath : baseConfigPath;
9+
10+
let prettierTypescriptConfig: Options | undefined;
11+
12+
/**
13+
* Uses prettier to format the given TypeScript sourcecode.
14+
* @param source The TypeScript snippet that needs to be formatted
15+
*/
16+
export async function formatTypeScript(source: string): Promise<string> {
17+
if (prettierTypescriptConfig === undefined) {
18+
const prettierConfig = await import(prettierConfigPath);
19+
prettierTypescriptConfig = {
20+
...prettierConfig,
21+
parser: "babel-ts"
22+
};
23+
}
24+
25+
return await format(source, prettierTypescriptConfig);
26+
}

0 commit comments

Comments
 (0)