diff --git a/packages/generator-widget/CHANGELOG.md b/packages/generator-widget/CHANGELOG.md index 9d4f7bb4..203ec15a 100644 --- a/packages/generator-widget/CHANGELOG.md +++ b/packages/generator-widget/CHANGELOG.md @@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - We changed the copyright prompt to prefill the current year. +- We now enforce validation when choosing an organization name for a widget. + ## [10.24.0] - 2025-09-24 ### Changed diff --git a/packages/generator-widget/generators/app/lib/prompttexts.js b/packages/generator-widget/generators/app/lib/prompttexts.js index 4decd73c..b1ce7ad1 100644 --- a/packages/generator-widget/generators/app/lib/prompttexts.js +++ b/packages/generator-widget/generators/app/lib/prompttexts.js @@ -9,7 +9,7 @@ function promptWidgetProperties(mxProjectDir, widgetName) { if (/^([a-zA-Z]+)$/.test(input)) { return true; } - return "Your widget name can only contain one or more letters (a-z & A-Z). Please provide a valid name"; + return "Your widget name may only contain characters matching [a-zA-Z]. Please provide a valid name."; }, message: "What is the name of your widget?", default: widgetName ? widgetName : "MyWidget" @@ -23,6 +23,15 @@ function promptWidgetProperties(mxProjectDir, widgetName) { { type: "input", name: "organization", + validate: input => { + if (/[^a-zA-Z0-9_.-]/.test(input)) { + return "Your organization name may only contain characters matching [a-zA-Z0-9_.-]. Please provide a valid name."; + } + if (!/^([a-zA-Z0-9_-]+.)*[a-zA-Z0-9_-]+$/.test(input)) { + return "Your organization name must follow the structure (namespace.)org-name, for example 'mendix' or 'com.mendix.widgets'. Please provide a valid name."; + } + return true; + }, message: "Organization name", default: "Mendix", store: true diff --git a/packages/pluggable-widgets-tools/CHANGELOG.md b/packages/pluggable-widgets-tools/CHANGELOG.md index fb9ae935..06f35784 100644 --- a/packages/pluggable-widgets-tools/CHANGELOG.md +++ b/packages/pluggable-widgets-tools/CHANGELOG.md @@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - We fixed an issue where `require` was not transformed to `import` for the `es` output format which could result in an error when the widget was used in a project with React client enabled. +- We now enforce the same validation for the `widgetName` in the widget bundler as we do in the generator. Validation is now also enforced for the organization name (`packagePath`). + ## [11.3.0] - 2025-11-12 ### Changed diff --git a/packages/pluggable-widgets-tools/configs/shared.mjs b/packages/pluggable-widgets-tools/configs/shared.mjs index 80098b07..e58f8be1 100644 --- a/packages/pluggable-widgets-tools/configs/shared.mjs +++ b/packages/pluggable-widgets-tools/configs/shared.mjs @@ -4,6 +4,7 @@ import { existsSync, readdirSync, promises as fs, readFileSync } 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"; config({ path: join(process.cwd(), ".env") }); @@ -25,6 +26,10 @@ 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")) diff --git a/packages/pluggable-widgets-tools/src/utils/__tests__/validation.spec.ts b/packages/pluggable-widgets-tools/src/utils/__tests__/validation.spec.ts new file mode 100644 index 00000000..60a68cbc --- /dev/null +++ b/packages/pluggable-widgets-tools/src/utils/__tests__/validation.spec.ts @@ -0,0 +1,28 @@ +import { WidgetValidationError, throwOnIllegalChars, throwOnNoMatch } from "../validation" + +describe("Validation Utilities", () => { + + describe("throwOnIllegalChars", () => { + + it("throws when the input does not match the pattern", () => { + expect(throwOnIllegalChars.bind(null, "abc", '0-9', "Test")).toThrow(WidgetValidationError) + }) + + it("does not throw when the input does match the pattern", () => { + expect(throwOnIllegalChars.bind(null, "abc", 'a-z', "Test")).not.toThrow() + }) + }) + + describe("throwOnNoMatch", () => { + + it("throws when the input does not match the pattern", () => { + expect(throwOnNoMatch.bind(null, "abc", /^$/, "Test")).toThrow(WidgetValidationError) + }) + + it("does not throw when the input does match the pattern", () => { + expect(throwOnNoMatch.bind(null, "abc", /[a-z]/, "Test")).not.toThrow() + }) + }) + +}) + diff --git a/packages/pluggable-widgets-tools/src/utils/validation.ts b/packages/pluggable-widgets-tools/src/utils/validation.ts new file mode 100644 index 00000000..efe5d65d --- /dev/null +++ b/packages/pluggable-widgets-tools/src/utils/validation.ts @@ -0,0 +1,47 @@ + +export class WidgetValidationError extends Error { + name = "Widget Validation Error" + + constructor(message: string) { + super(message) + } +} + +/** + * Asserts that the given input string only contains the characters specified by `legalCharacters`. + * @param input The string under test + * @param legalCharacters The characters that the input string may contain. Follows character class notation from regex (inside the []) + * @param message The message that is included in the error. Specify where the input is used and can be corrected. + * @throws WidgetValidationError If the input contains characters not allowed by legalCharacters + */ +export function throwOnIllegalChars(input: string, legalCharacters: string, message: string): void { + const pattern = new RegExp(`([^${legalCharacters}])`, "g") + const illegalChars = input.match(pattern); + + if (illegalChars === null) { + return + } + + const formatted = illegalChars + .reduce((unique, c) => unique.includes(c) ? unique : [...unique, c], [] as string[]) + .map(c => `"${c}"`) + .join(", "); + + throw new WidgetValidationError(`${message} contains illegal characters ${formatted}. Allowed characters are [${legalCharacters}].`) +} + +/** + * Asserts that the given input string matches the given pattern. + * @param input The string under test + * @param pattern The pattern the input must match + * @param message The message that is included in the error. Specify where the input is used and can be corrected. + * @throws WidgetValidationError If the input contains characters not allowed by legalCharacters + */ +export function throwOnNoMatch(input: string, pattern: RegExp, message: string) { + if (pattern.test(input)) { + return + } + + throw new WidgetValidationError(`${message} does not match the pattern ${pattern}.`) +} +