From 0e3b079481ebcf7061ead60dac7af35a072ff61a Mon Sep 17 00:00:00 2001 From: Mateusz Wojczal Date: Fri, 7 Aug 2026 12:55:16 +0200 Subject: [PATCH] fix(runner): drop themeName when wiring a theme into a demo (DEV-2047) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by running the panel against a real example rather than reading its output: the React starter sets themeName="ht-theme-main", the panel added theme={customTheme} beside it, and Handsontable refuses both — Both `theme` and `themeName` are defined in your configuration. These options are aliases and cannot be used together. The `themeName` option will be ignored. The theme did apply (theme wins), so this was a warning rather than a failure, but every themed demo logged it and the dead prop stayed in code the user can read and download. The prop being replaced is now removed, in JSX and in settings objects alike, leaving licenseKey and everything else untouched. --- runner/apps/authoring/src/theme/codegen.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/runner/apps/authoring/src/theme/codegen.ts b/runner/apps/authoring/src/theme/codegen.ts index 4da45d22..4ea3df8f 100644 --- a/runner/apps/authoring/src/theme/codegen.ts +++ b/runner/apps/authoring/src/theme/codegen.ts @@ -186,15 +186,22 @@ function wireTheme(files: Record): WireTarget | null { // 1. JSX/Vue wrapper element. Replace an existing theme prop rather than // adding a second one — several starters already set their own. if (/<(HotTable|hot-table)\b/.test(source)) { - next = /\btheme=\{[^}]*\}/.test(source) + const withTheme = /\btheme=\{[^}]*\}/.test(source) ? source.replace(/\btheme=\{[^}]*\}/, "theme={customTheme}") : source.replace(/<(HotTable|hot-table)\b/, "<$1 theme={customTheme}"); + // `theme` and `themeName` are aliases and Handsontable refuses to take + // both — it warns and drops themeName. Leaving the dead prop behind + // means every themed demo logs a warning it cannot act on, so the one + // being replaced goes. + next = stripThemeName(withTheme); } else if (/new Handsontable\(\s*[A-Za-z_$][\w$]*\s*,\s*\{/.test(source)) { // 2. Vanilla settings object. - next = source.replace(/(new Handsontable\(\s*[A-Za-z_$][\w$]*\s*,\s*\{)/, "$1\n theme: customTheme,"); + next = stripThemeName( + source.replace(/(new Handsontable\(\s*[A-Za-z_$][\w$]*\s*,\s*\{)/, "$1\n theme: customTheme,"), + ); } else if (/gridSettings[^=]*=\s*\{/.test(source)) { // 3. Angular's settings object. - next = source.replace(/(gridSettings[^=]*=\s*\{)/, "$1\n theme: customTheme,"); + next = stripThemeName(source.replace(/(gridSettings[^=]*=\s*\{)/, "$1\n theme: customTheme,")); } if (!next) continue; @@ -208,6 +215,14 @@ function wireTheme(files: Record): WireTarget | null { return null; } +/** Remove a `themeName` prop or setting: it is an alias of `theme`, and + * Handsontable warns and ignores it when both are present. */ +function stripThemeName(source: string): string { + return source + .replace(/\n?[^\n]*\bthemeName\s*=\s*["'][^"']*["'][^\n]*/g, "") + .replace(/\n?[^\n]*\bthemeName\s*:\s*["'][^"']*["'],?[^\n]*/g, ""); +} + /** Does the example look like TypeScript? Decides the module's extension. */ function isTypescript(files: Record): boolean { return Object.keys(files).some((p) => /\.tsx?$/.test(p));