Skip to content

Commit 1d1954f

Browse files
authored
[WTF-2518]: Enforce widgetName constraint in bundler (#158)
## Checklist - Contains unit tests ❌ - Contains breaking changes ✅ - Compatible with: Any - Did you update version and changelog? ✅ - PR title properly formatted (`[XX-000]: description`)? ✅ ## This PR contains - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Documentation - [ ] Other (describe) ## What is the purpose of this PR? Widget names are only allowed to contain lower and uppercase letters. This is checked by the generator when the developer is prompted for a widget name. However, this validation is never done during bundling. Since Studio Pro assumes this, a widget which had its name changed after generation could cause the deployment of an app to fail. ## Relevant changes A check was added to the bundler when the widgetName is retrieved from the widget's package.json. ## What should be covered while testing? - Widgets with allowed names are able to build. - Widgets with unallowed names (including characters that are not [a-zA-Z]) cause the build to fail.
2 parents 7ec333b + ee8bbe8 commit 1d1954f

File tree

6 files changed

+94
-1
lines changed

6 files changed

+94
-1
lines changed

packages/generator-widget/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1212

1313
- We changed the copyright prompt to prefill the current year.
1414

15+
- We now enforce validation when choosing an organization name for a widget.
16+
1517
## [10.24.0] - 2025-09-24
1618

1719
### Changed

packages/generator-widget/generators/app/lib/prompttexts.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ function promptWidgetProperties(mxProjectDir, widgetName) {
99
if (/^([a-zA-Z]+)$/.test(input)) {
1010
return true;
1111
}
12-
return "Your widget name can only contain one or more letters (a-z & A-Z). Please provide a valid name";
12+
return "Your widget name may only contain characters matching [a-zA-Z]. Please provide a valid name.";
1313
},
1414
message: "What is the name of your widget?",
1515
default: widgetName ? widgetName : "MyWidget"
@@ -23,6 +23,15 @@ function promptWidgetProperties(mxProjectDir, widgetName) {
2323
{
2424
type: "input",
2525
name: "organization",
26+
validate: input => {
27+
if (/[^a-zA-Z0-9_.-]/.test(input)) {
28+
return "Your organization name may only contain characters matching [a-zA-Z0-9_.-]. Please provide a valid name.";
29+
}
30+
if (!/^([a-zA-Z0-9_-]+.)*[a-zA-Z0-9_-]+$/.test(input)) {
31+
return "Your organization name must follow the structure (namespace.)org-name, for example 'mendix' or 'com.mendix.widgets'. Please provide a valid name.";
32+
}
33+
return true;
34+
},
2635
message: "Organization name",
2736
default: "Mendix",
2837
store: true

packages/pluggable-widgets-tools/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1414

1515
- 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.
1616

17+
- 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`).
18+
1719
## [11.3.0] - 2025-11-12
1820

1921
### Changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { existsSync, readdirSync, promises as fs, readFileSync } from "node:fs";
44
import { join, relative } from "node:path";
55
import { config } from "dotenv";
66
import colors from "ansi-colors";
7+
import { throwOnIllegalChars, throwOnNoMatch } from "../dist/utils/validation.js";
78

89
config({ path: join(process.cwd(), ".env") });
910

@@ -25,6 +26,10 @@ if (!widgetName || !widgetPackageJson) {
2526
throw new Error("Widget does not define widgetName in its package.json");
2627
}
2728

29+
throwOnIllegalChars(widgetName, "a-zA-Z", "The `widgetName` property in package.json")
30+
throwOnIllegalChars(widgetPackage, "a-zA-Z0-9_.-", "The `packagePath` property in package.json")
31+
throwOnNoMatch(widgetPackage, /^([a-zA-Z0-9_-]+.)*[a-zA-Z0-9_-]+$/, "The `packagePath` property in package.json")
32+
2833
const widgetSrcFiles = readdirSync(join(sourcePath, "src")).map(file => join(sourcePath, "src", file));
2934
export const widgetEntry = widgetSrcFiles.filter(file =>
3035
file.match(new RegExp(`[/\\\\]${escape(widgetName)}\\.[jt]sx?$`, "i"))
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { WidgetValidationError, throwOnIllegalChars, throwOnNoMatch } from "../validation"
2+
3+
describe("Validation Utilities", () => {
4+
5+
describe("throwOnIllegalChars", () => {
6+
7+
it("throws when the input does not match the pattern", () => {
8+
expect(throwOnIllegalChars.bind(null, "abc", '0-9', "Test")).toThrow(WidgetValidationError)
9+
})
10+
11+
it("does not throw when the input does match the pattern", () => {
12+
expect(throwOnIllegalChars.bind(null, "abc", 'a-z', "Test")).not.toThrow()
13+
})
14+
})
15+
16+
describe("throwOnNoMatch", () => {
17+
18+
it("throws when the input does not match the pattern", () => {
19+
expect(throwOnNoMatch.bind(null, "abc", /^$/, "Test")).toThrow(WidgetValidationError)
20+
})
21+
22+
it("does not throw when the input does match the pattern", () => {
23+
expect(throwOnNoMatch.bind(null, "abc", /[a-z]/, "Test")).not.toThrow()
24+
})
25+
})
26+
27+
})
28+
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
2+
export class WidgetValidationError extends Error {
3+
name = "Widget Validation Error"
4+
5+
constructor(message: string) {
6+
super(message)
7+
}
8+
}
9+
10+
/**
11+
* Asserts that the given input string only contains the characters specified by `legalCharacters`.
12+
* @param input The string under test
13+
* @param legalCharacters The characters that the input string may contain. Follows character class notation from regex (inside the [])
14+
* @param message The message that is included in the error. Specify where the input is used and can be corrected.
15+
* @throws WidgetValidationError If the input contains characters not allowed by legalCharacters
16+
*/
17+
export function throwOnIllegalChars(input: string, legalCharacters: string, message: string): void {
18+
const pattern = new RegExp(`([^${legalCharacters}])`, "g")
19+
const illegalChars = input.match(pattern);
20+
21+
if (illegalChars === null) {
22+
return
23+
}
24+
25+
const formatted = illegalChars
26+
.reduce((unique, c) => unique.includes(c) ? unique : [...unique, c], [] as string[])
27+
.map(c => `"${c}"`)
28+
.join(", ");
29+
30+
throw new WidgetValidationError(`${message} contains illegal characters ${formatted}. Allowed characters are [${legalCharacters}].`)
31+
}
32+
33+
/**
34+
* Asserts that the given input string matches the given pattern.
35+
* @param input The string under test
36+
* @param pattern The pattern the input must match
37+
* @param message The message that is included in the error. Specify where the input is used and can be corrected.
38+
* @throws WidgetValidationError If the input contains characters not allowed by legalCharacters
39+
*/
40+
export function throwOnNoMatch(input: string, pattern: RegExp, message: string) {
41+
if (pattern.test(input)) {
42+
return
43+
}
44+
45+
throw new WidgetValidationError(`${message} does not match the pattern ${pattern}.`)
46+
}
47+

0 commit comments

Comments
 (0)