From a8367c8b3a45e38ca6e8dd11b5a7b9b1913d0784 Mon Sep 17 00:00:00 2001 From: xhivo Date: Mon, 1 Jun 2026 00:53:59 +0200 Subject: [PATCH 1/4] Add HMR and React Fast Refresh support --- .../frontend/src/Dashboard.tsx | 2 +- .../frontend/src/Panel.tsx | 2 +- .../frontend/src/Settings.tsx | 4 +-- .../frontend/vite.dev.config.ts | 32 +++++++++++++++++-- .../{{ cookiecutter.package_name }}/core.py | 10 +++--- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/src/Dashboard.tsx b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/src/Dashboard.tsx index b4d225f..742fe56 100644 --- a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/src/Dashboard.tsx +++ b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/src/Dashboard.tsx @@ -38,7 +38,7 @@ function {{ cookiecutter.plugin_name }}DashboardItem({ // This is the function which is called by InvenTree to render the actual dashboard // component -export function render{{ cookiecutter.plugin_name }}DashboardItem(context: InvenTreePluginContext) { +export function Render{{ cookiecutter.plugin_name }}DashboardItem(context: InvenTreePluginContext) { checkPluginVersion(context); return <{{ cookiecutter.plugin_name }}DashboardItem context={context} />; } diff --git a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/src/Panel.tsx b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/src/Panel.tsx index ab08c70..32815a6 100644 --- a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/src/Panel.tsx +++ b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/src/Panel.tsx @@ -249,7 +249,7 @@ function {{ cookiecutter.plugin_name }}Panel({ } // This is the function which is called by InvenTree to render the actual panel component -export function render{{ cookiecutter.plugin_name }}Panel(context: InvenTreePluginContext) { +export function Render{{ cookiecutter.plugin_name }}Panel(context: InvenTreePluginContext) { checkPluginVersion(context); {% if cookiecutter.frontend.translation -%} diff --git a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/src/Settings.tsx b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/src/Settings.tsx index 4ff70e6..cf74844 100644 --- a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/src/Settings.tsx +++ b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/src/Settings.tsx @@ -27,8 +27,8 @@ function PluginSettingsDisplay({ } -export function renderPluginSettings(context: InvenTreePluginContext) { +export function RenderPluginSettings(context: InvenTreePluginContext) { return ( ); -} \ No newline at end of file +} diff --git a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts index 9b7506d..51753d3 100644 --- a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts +++ b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts @@ -1,5 +1,5 @@ // Primary vite config - we extend this for dev mode -import { defineConfig } from 'vite' +import { defineConfig, Plugin } from 'vite' import { viteExternalsPlugin } from 'vite-plugin-externals' import viteConfig, { externalLibs } from './vite.config' @@ -8,6 +8,32 @@ import react from "@vitejs/plugin-react-swc" import { lingui } from "@lingui/vite-plugin" {%- endif %} +function inventreeHmrPlugin(): Plugin { + const fileRegex = /\.(js|jsx|ts|tsx)(\?|$)/; + + const hmrBlock = [ + '', + '// __inventree_hmr_injected__', + 'if (import.meta.hot) {', + ' import.meta.hot.accept((newModule) => {', + ' window.__plugin_hmr_reload?.(newModule);', + ' })', + '}', + ]; + + return { + name: 'inventree-hmr-plugin', + + transform(code, id) { + if (!fileRegex.test(id)) return; + if (id.includes("node_modules")) return; + if (code.includes("__inventree_hmr_injected__")) return; + + return code + hmrBlock.join('\n'); + } + } +} + /** * Vite config to run the frontend plugin in development mode. * @@ -40,10 +66,12 @@ export default defineConfig((cfg) => { {% if cookiecutter.frontend.translation -%} lingui(), react({ - plugins: [["@lingui/swc-plugin", {}]] + plugins: [["@lingui/swc-plugin", {}]], + reactRefreshHost: 'http://localhost:5173', }), {%- endif %} viteExternalsPlugin(externalLibs), + inventreeHmrPlugin(), ]; return config; diff --git a/plugin_creator/template/{{ cookiecutter.plugin_name }}/{{ cookiecutter.package_name }}/core.py b/plugin_creator/template/{{ cookiecutter.plugin_name }}/{{ cookiecutter.package_name }}/core.py index 2b15b3a..d06063f 100644 --- a/plugin_creator/template/{{ cookiecutter.plugin_name }}/{{ cookiecutter.package_name }}/core.py +++ b/plugin_creator/template/{{ cookiecutter.plugin_name }}/{{ cookiecutter.package_name }}/core.py @@ -42,7 +42,7 @@ class {{ cookiecutter.plugin_name }}(InvenTreePlugin): {% if "UserInterfaceMixin" in cookiecutter.plugin_mixins.mixin_list -%} {%- if cookiecutter.frontend.features.settings -%} # Render custom UI elements to the plugin settings page - ADMIN_SOURCE = "Settings.js:renderPluginSettings" + ADMIN_SOURCE = "Settings.js:RenderPluginSettings" {%- endif -%} {%- endif -%} @@ -156,7 +156,7 @@ def get_ui_panels(self, request, context: dict, **kwargs): 'title': '{{ cookiecutter.plugin_title }}', 'description': 'Custom panel description', 'icon': 'ti:mood-smile:outline', - 'source': self.plugin_static_file('Panel.js:render{{ cookiecutter.plugin_name }}Panel'), + 'source': self.plugin_static_file('Panel.js:Render{{ cookiecutter.plugin_name }}Panel'), 'context': { # Provide additional context data to the panel {%- if "SettingsMixin" in cookiecutter.plugin_mixins.mixin_list %} @@ -185,7 +185,7 @@ def get_ui_dashboard_items(self, request, context: dict, **kwargs): 'title': '{{ cookiecutter.plugin_title }} Dashboard Item', 'description': 'Custom dashboard item', 'icon': 'ti:dashboard:outline', - 'source': self.plugin_static_file('Dashboard.js:render{{ cookiecutter.plugin_name }}DashboardItem'), + 'source': self.plugin_static_file('Dashboard.js:Render{{ cookiecutter.plugin_name }}DashboardItem'), 'context': { # Provide additional context data to the dashboard item {%- if "SettingsMixin" in cookiecutter.plugin_mixins.mixin_list %} @@ -207,7 +207,9 @@ def get_ui_spotlight_actions(self, request, context, **kwargs): 'title': 'Hello Action', 'description': 'Hello from {{ cookiecutter.plugin_name }}', 'icon': 'ti:heart-handshake:outline', - 'source': self.plugin_static_file('Spotlight.js:{{ cookiecutter.plugin_name }}SpotlightAction'), + 'source': self.plugin_static_file( + 'Spotlight.js:{{ cookiecutter.plugin_name[0]|upper }}{{ cookiecutter.plugin_name[1:] }}SpotlightAction' + ), } ] {%- endif -%} From 4a0a1093557e02626bab8a8a9929afd47bd9d704 Mon Sep 17 00:00:00 2001 From: xhivo Date: Mon, 1 Jun 2026 17:46:45 +0200 Subject: [PATCH 2/4] Update inventreeHmrPlugin to include the URL The URL is now passed to the callback, which then checks that the same file is being loaded before replacing the module. --- .../frontend/vite.dev.config.ts | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts index 51753d3..24d952f 100644 --- a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts +++ b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts @@ -1,5 +1,5 @@ // Primary vite config - we extend this for dev mode -import { defineConfig, Plugin } from 'vite' +import { defineConfig } from 'vite' import { viteExternalsPlugin } from 'vite-plugin-externals' import viteConfig, { externalLibs } from './vite.config' @@ -8,7 +8,11 @@ import react from "@vitejs/plugin-react-swc" import { lingui } from "@lingui/vite-plugin" {%- endif %} +import type { ResolvedConfig, Plugin } from 'vite' + function inventreeHmrPlugin(): Plugin { + let isDev = false; + const fileRegex = /\.(js|jsx|ts|tsx)(\?|$)/; const hmrBlock = [ @@ -16,20 +20,29 @@ function inventreeHmrPlugin(): Plugin { '// __inventree_hmr_injected__', 'if (import.meta.hot) {', ' import.meta.hot.accept((newModule) => {', - ' window.__plugin_hmr_reload?.(newModule);', + ' window.__plugin_hmr_reload?.({mod: newModule, url: import.meta.url});', ' })', '}', ]; return { name: 'inventree-hmr-plugin', + enforce: 'post', + + configResolved(config: ResolvedConfig) { + isDev = config.command === 'serve'; + }, transform(code, id) { + if (!isDev) return; if (!fileRegex.test(id)) return; - if (id.includes("node_modules")) return; - if (code.includes("__inventree_hmr_injected__")) return; + if (id.includes('node_modules')) return; + if (code.includes('__inventree_hmr_injected__')) return; - return code + hmrBlock.join('\n'); + return { + code: code + hmrBlock.join('\n'), + map: null, + } } } } From 0fcdb5f110a9183a628c66467505ecd7ee39e392 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 2 Jun 2026 10:40:38 +1000 Subject: [PATCH 3/4] Add comment --- .../{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts index 24d952f..d3dc3e1 100644 --- a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts +++ b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts @@ -10,6 +10,7 @@ import { lingui } from "@lingui/vite-plugin" import type { ResolvedConfig, Plugin } from 'vite' +// Enable HMR support for this plugin by hooking into the InvenTree vite dev server function inventreeHmrPlugin(): Plugin { let isDev = false; From e9e781a85c7bb334ea0b5d66ca520b5c21e08385 Mon Sep 17 00:00:00 2001 From: xhivo Date: Tue, 2 Jun 2026 22:13:56 +0200 Subject: [PATCH 4/4] Change HMR plugin to use a callback registry --- .../frontend/vite.dev.config.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts index d3dc3e1..67338aa 100644 --- a/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts +++ b/plugin_creator/template/{{ cookiecutter.plugin_name }}/frontend/vite.dev.config.ts @@ -8,12 +8,10 @@ import react from "@vitejs/plugin-react-swc" import { lingui } from "@lingui/vite-plugin" {%- endif %} -import type { ResolvedConfig, Plugin } from 'vite' +import type { Plugin } from 'vite' // Enable HMR support for this plugin by hooking into the InvenTree vite dev server function inventreeHmrPlugin(): Plugin { - let isDev = false; - const fileRegex = /\.(js|jsx|ts|tsx)(\?|$)/; const hmrBlock = [ @@ -21,7 +19,10 @@ function inventreeHmrPlugin(): Plugin { '// __inventree_hmr_injected__', 'if (import.meta.hot) {', ' import.meta.hot.accept((newModule) => {', - ' window.__plugin_hmr_reload?.({mod: newModule, url: import.meta.url});', + ' const key = new URL(import.meta.url).origin + new URL(import.meta.url).pathname;', + ' window.__plugin_hmr_callbacks?.[key]?.forEach(callback => {', + ' callback(newModule);', + ' });', ' })', '}', ]; @@ -30,12 +31,7 @@ function inventreeHmrPlugin(): Plugin { name: 'inventree-hmr-plugin', enforce: 'post', - configResolved(config: ResolvedConfig) { - isDev = config.command === 'serve'; - }, - transform(code, id) { - if (!isDev) return; if (!fileRegex.test(id)) return; if (id.includes('node_modules')) return; if (code.includes('__inventree_hmr_injected__')) return;