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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/pluggable-widgets-tools/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- 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()`.
- Consumers who extended the base config with `globals['ts-jest']` options must migrate those settings to the `@swc/jest` transform config.

### Fixed

- We updated the type generator to format types according to prettier. Inconsistencies would block the `release` command.


## [11.11.0] - 2026-06-04

### Added
Expand Down
19 changes: 8 additions & 11 deletions packages/pluggable-widgets-tools/bin/mx-scripts.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#! /usr/bin/env node
const { execSync, spawnSync } = require("child_process");
const { existsSync } = require("fs");
const { delimiter, dirname, join, parse } = require("path");
const { delimiter, join, parse } = require("path");
const { checkMigration } = require("../utils/migration");
const { checkForEnzymeUsage } = require("../dist/utils/enzyme-detector");
const { red, blue, bold, whiteBright } = require("ansi-colors");
const semver = require("semver");
const { auditPluggableWidgetsTools } = require("../dist/commands/audit");
const { auditPluggableWidgetsTools } = require("../dist/commands/audit.js");
const { prettierConfigPath } = require("../dist/utils/formatting.js");
const { toolsRoot, widgetRoot } = require("../dist/widget/paths.js");

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

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

if (args.indexOf("--subprojectPath") > -1) {
args.splice(args.indexOf("--subprojectPath"), 2);
Expand All @@ -46,7 +45,7 @@ checkNodeVersion();
const commandWithArgs = realCommand + " " + args.join(" ");
for (const subCommand of commandWithArgs.split(/&&/g)) {
const result = spawnSync(subCommand.trim(), [], {
cwd: process.cwd(),
cwd: widgetRoot,
env: {
...process.env,
PATH: [process.env.PATH].concat(nodeModulesBins).join(delimiter),
Expand All @@ -64,10 +63,8 @@ checkNodeVersion();
}
})();

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

function checkNodeVersion() {
const packageJson = require(join(__dirname, "../package.json"));
const packageJson = require(join(toolsRoot, "./package.json"));
const nodeRange = new semver.Range(packageJson.engines.node);

console.log("Checking node and npm version...");
Expand Down
31 changes: 11 additions & 20 deletions packages/pluggable-widgets-tools/configs/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,21 @@ import postcss from "rollup-plugin-postcss";
import terser from "@rollup/plugin-terser";
import shelljs from "shelljs";
import { widgetTyping } from "./rollup-plugin-widget-typing.mjs";
import {
editorConfigEntry,
isTypescript,
previewEntry,
projectPath,
sourcePath,
widgetEntry,
widgetName,
widgetPackage,
widgetVersion,
onwarn
} from "./shared.mjs";
import { editorConfigEntry, isTypescript, previewEntry, projectPath, widgetEntry, onwarn } from "./shared.mjs";
import { copyLicenseFile, createMpkFile, licenseCustomTemplate } from "./helpers/rollup-helper.mjs";
import url from "./rollup-plugin-assets.mjs";
import { widgetName, widgetOrganization, widgetVersion } from "../dist/widget/package.js";
import { widgetRoot } from "../dist/widget/paths.js";

const { loadConfigFile } = rollupLoadConfigFile;
const { cp } = shelljs;

const outDir = join(sourcePath, "/dist/tmp/widgets/");
const outWidgetDir = join(widgetPackage.replace(/\./g, "/"), widgetName.toLowerCase());
const outDir = join(widgetRoot, "/dist/tmp/widgets/");
const outWidgetDir = join(widgetOrganization.replace(/\./g, "/"), widgetName.toLowerCase());
const outWidgetFile = join(outWidgetDir, `${widgetName}`);
const absoluteOutPackageDir = join(outDir, outWidgetDir);
const mpkDir = join(sourcePath, "dist", widgetVersion);
const mpkFile = join(mpkDir, process.env.MPKOUTPUT ? process.env.MPKOUTPUT : `${widgetPackage}.${widgetName}.mpk`);
const mpkDir = join(widgetRoot, "dist", widgetVersion);
const mpkFile = join(mpkDir, process.env.MPKOUTPUT ? process.env.MPKOUTPUT : `${widgetOrganization}.${widgetName}.mpk`);
const assetsDirName = "assets";
const absoluteOutAssetsDir = join(absoluteOutPackageDir, assetsDirName);
const outAssetsDir = join(outWidgetDir, assetsDirName);
Expand Down Expand Up @@ -208,8 +199,8 @@ export default async args => {
});
}

const customConfigPathJS = join(sourcePath, "rollup.config.js");
const customConfigPathESM = join(sourcePath, "rollup.config.mjs");
const customConfigPathJS = join(widgetRoot, "rollup.config.js");
const customConfigPathESM = join(widgetRoot, "rollup.config.mjs");
const existingConfigPath = existsSync(customConfigPathJS)
? customConfigPathJS
: existsSync(customConfigPathESM)
Expand Down Expand Up @@ -300,7 +291,7 @@ export default async args => {
// configs affected by a change => we cannot know in advance which one will be "the last".
// So we run the same logic for all configs, letting the last one win.
command([
async () => config.licenses && copyLicenseFile(sourcePath, outDir),
async () => config.licenses && copyLicenseFile(widgetRoot, outDir),
async () =>
createMpkFile({
mpkDir,
Expand All @@ -316,7 +307,7 @@ export default async args => {

function getClientComponentPlugins() {
return [
isTypescript ? widgetTyping({ sourceDir: join(sourcePath, "src") }) : null,
isTypescript ? widgetTyping({ sourceDir: join(widgetRoot, "src") }) : null,
clear({ targets: [outDir, mpkDir] }),
command([
() => {
Expand Down
30 changes: 11 additions & 19 deletions packages/pluggable-widgets-tools/configs/rollup.config.native.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,18 @@ import terser from "@rollup/plugin-terser";
import shelljs from "shelljs";
import { widgetTyping } from "./rollup-plugin-widget-typing.mjs";
import { collectDependencies } from "./rollup-plugin-collect-dependencies.mjs";
import {
editorConfigEntry,
isTypescript,
projectPath,
sourcePath,
widgetEntry,
widgetName,
widgetPackage,
widgetVersion,
onwarn
} from "./shared.mjs";
import { editorConfigEntry, isTypescript, projectPath, widgetEntry, onwarn } from "./shared.mjs";
import { copyLicenseFile, createMpkFile, licenseCustomTemplate } from "./helpers/rollup-helper.mjs";
import { widgetName, widgetOrganization, widgetVersion } from "../dist/widget/package.js";
import { widgetRoot } from "../dist/widget/paths.js";

const { cp } = shelljs;
const { blue } = colors;

const outDir = join(sourcePath, "/dist/tmp/widgets/");
const outWidgetFile = join(widgetPackage.replace(/\./g, "/"), widgetName.toLowerCase(), `${widgetName}`);
const mpkDir = join(sourcePath, "dist", widgetVersion);
const mpkFile = join(mpkDir, process.env.MPKOUTPUT ? process.env.MPKOUTPUT : `${widgetPackage}.${widgetName}.mpk`);
const outDir = join(widgetRoot, "/dist/tmp/widgets/");
const outWidgetFile = join(widgetOrganization.replace(/\./g, "/"), widgetName.toLowerCase(), `${widgetName}`);
const mpkDir = join(widgetRoot, "dist", widgetVersion);
const mpkFile = join(mpkDir, process.env.MPKOUTPUT ? process.env.MPKOUTPUT : `${widgetOrganization}.${widgetName}.mpk`);

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

Expand Down Expand Up @@ -161,8 +153,8 @@ export default async args => {
});
}

const customConfigPathJS = join(sourcePath, "rollup.config.js");
const customConfigPathESM = join(sourcePath, "rollup.config.mjs");
const customConfigPathJS = join(widgetRoot, "rollup.config.js");
const customConfigPathESM = join(widgetRoot, "rollup.config.mjs");
const existingConfigPath = existsSync(customConfigPathJS)
? customConfigPathJS
: existsSync(customConfigPathESM)
Expand Down Expand Up @@ -237,7 +229,7 @@ export default async args => {
// configs affected by a change => we cannot know in advance which one will be "the last".
// So we run the same logic for all configs, letting the last one win.
command([
async () => config.licenses && copyLicenseFile(sourcePath, outDir),
async () => config.licenses && copyLicenseFile(widgetRoot, outDir),
async () =>
createMpkFile({
mpkDir,
Expand All @@ -253,7 +245,7 @@ export default async args => {

function getClientComponentPlugins() {
return [
isTypescript ? widgetTyping({ sourceDir: join(sourcePath, "src") }) : null,
isTypescript ? widgetTyping({ sourceDir: join(widgetRoot, "src") }) : null,
clear({ targets: [outDir, mpkDir] }),
command([
() => {
Expand Down
22 changes: 4 additions & 18 deletions packages/pluggable-widgets-tools/configs/shared.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { existsSync, readdirSync, promises as fs, readFileSync } from "node:fs";
import { existsSync, readdirSync, promises as fs } from "node:fs";
import { join, relative } from "node:path";
import { config } from "dotenv";
import colors from "ansi-colors";
import { throwOnIllegalChars, throwOnNoMatch } from "../dist/utils/validation.js";
import { dir as sourcePath, widgetName, json } from "../dist/widget/package.js";

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

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

export const sourcePath = process.cwd();

const widgetPackageJson = JSON.parse(readFileSync(join(sourcePath, "package.json")));
export const widgetName = widgetPackageJson.widgetName;
export const widgetPackage = widgetPackageJson.packagePath;
export const widgetVersion = widgetPackageJson.version;
if (!widgetName || !widgetPackageJson) {
throw new Error("Widget does not define widgetName in its package.json");
}

throwOnIllegalChars(widgetName, "a-zA-Z", "The `widgetName` property in package.json");
throwOnIllegalChars(widgetPackage, "a-zA-Z0-9_.-", "The `packagePath` property in package.json");
throwOnNoMatch(widgetPackage, /^([a-zA-Z0-9_-]+.)*[a-zA-Z0-9_-]+$/, "The `packagePath` property in package.json");

const widgetSrcFiles = readdirSync(join(sourcePath, "src")).map(file => join(sourcePath, "src", file));
export const widgetEntry = widgetSrcFiles.filter(file =>
file.match(new RegExp(`[/\\\\]${escape(widgetName)}\\.[jt]sx?$`, "i"))
Expand All @@ -47,7 +33,7 @@ export const isTypescript = [widgetEntry, editorConfigEntry, previewEntry].some(

export const projectPath = [
process.env.MX_PROJECT_PATH,
widgetPackageJson.config.projectPath,
json.config.projectPath,
join(sourcePath, "tests/testProject")
].filter(path => path && existsSync(path))[0];

Expand Down
2 changes: 1 addition & 1 deletion packages/pluggable-widgets-tools/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mendix/pluggable-widgets-tools",
"version": "11.11.0",
"version": "11.12.0",
"description": "Mendix Pluggable Widgets Tools",
"engines": {
"node": ">=20"
Expand Down
3 changes: 1 addition & 2 deletions packages/pluggable-widgets-tools/src/commands/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { exec } from "node:child_process";
import { maxSatisfying, minSatisfying } from "semver";
import { confirm } from "../cli/confirm";
import { readFile, writeFile } from "node:fs";
import { join } from "node:path";
import { path as packageJsonPath } from "../widget/package";

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

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

const overrides = updateable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { parseStringPromise } from "xml2js";
import { PackageXml } from "./PackageXml";
import { WidgetXml } from "./WidgetXml";
import { generateForWidget } from "./generate";
import { formatTypeScript } from "../utils/formatting";

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

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

let generatedContent;
let generatedContent: string;
try {
const sourceXml = (await parseStringPromise(source)) as WidgetXml;
generatedContent = generateForWidget(sourceXml, toWidgetName(sourcePath));
Expand All @@ -39,8 +40,9 @@ export async function transformPackage(content: string, basePath: string) {
);
}

const formattedContent = await formatTypeScript(generatedContent);
const resultPath = sourcePath.replace(/(\.xml)?$/, "Props.d.ts");
await writeFile(join(resultBasePath, resultPath), generatedContent);
await writeFile(join(resultBasePath, resultPath), formattedContent);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
import { join } from "path";
import { yellow } from "ansi-colors";
import { widgetRoot } from "../widget/paths";

export function checkForEnzymeUsage(srcDir: string = "src"): void {
const projectRoot = process.cwd();
const srcPath = join(projectRoot, srcDir);
const srcPath = join(widgetRoot, srcDir);

if (!existsSync(srcPath)) {
return;
Expand Down Expand Up @@ -36,7 +36,7 @@ export function checkForEnzymeUsage(srcDir: string = "src"): void {
content
)
) {
enzymeFiles.push(fullPath.replace(projectRoot, "."));
enzymeFiles.push(fullPath.replace(widgetRoot, "."));
}
}
} catch (error) {
Expand Down
26 changes: 26 additions & 0 deletions packages/pluggable-widgets-tools/src/utils/formatting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { existsSync } from "fs";
import { join } from "path";
import { format, Options } from "prettier";
import { toolsRoot, widgetRoot } from "../widget/paths";

const baseConfigPath = join(toolsRoot, "./configs/prettier.base.json");
const widgetConfigPath = join(widgetRoot, "./prettier.config.js");
export const prettierConfigPath = existsSync(widgetConfigPath) ? widgetConfigPath : baseConfigPath;

let prettierTypescriptConfig: Options | undefined;

/**
* Uses prettier to format the given TypeScript sourcecode.
* @param source The TypeScript snippet that needs to be formatted
*/
export async function formatTypeScript(source: string): Promise<string> {
if (prettierTypescriptConfig === undefined) {
const prettierConfig = await import(prettierConfigPath);
prettierTypescriptConfig = {
...prettierConfig,
parser: "babel-ts"
};
}

return await format(source, prettierTypescriptConfig);
}
3 changes: 2 additions & 1 deletion packages/pluggable-widgets-tools/src/utils/npmAudit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert from "node:assert";
import { exec } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { widgetRoot } from "../widget/paths";

export type Report = {
auditReportVersion: 2;
Expand Down Expand Up @@ -70,7 +71,7 @@ export function collectVulnerabilities(report: Report, dependency: Dependency):
}

export async function run(): Promise<Report> {
const packageLock = join(process.cwd(), "package-lock.json");
const packageLock = join(widgetRoot, "package-lock.json");
assert(
existsSync(packageLock),
"Expected to find an npm lockfile. To run npm audit, dependencies must be installed with npm."
Expand Down
Loading
Loading