mirror of
https://github.com/inventree/InvenTree.git
synced 2026-06-11 19:27:02 +00:00
[plugin] HMR lib hooks (#12108)
* Expose HMR plugin * Expose function for localizing a plugin component * Update npm version * better docs * Plugin provides i18n instance * Expose HMR plugin on different path * Bump version (again) * Ensure HMR plugin is properly built * Bump (again) * Specify callback function * Bump package version * Improved docstrings * Stricter type hinting
This commit is contained in:
@@ -2,6 +2,32 @@
|
|||||||
|
|
||||||
This file contains historical changelog information for the InvenTree UI components library.
|
This file contains historical changelog information for the InvenTree UI components library.
|
||||||
|
|
||||||
|
### 1.4.5 - June 2026
|
||||||
|
|
||||||
|
Fixes callback signature for `<LocalizedComponent>` to allow for an optional `loadLocale` function to be passed in, which is used to dynamically load locale messages for the plugin.
|
||||||
|
|
||||||
|
### 1.4.4 - June 2026
|
||||||
|
|
||||||
|
Fixes bundling issues associated with the `InventreeHmrPlugin` plugin function.
|
||||||
|
|
||||||
|
### 1.4.3 - June 2026
|
||||||
|
|
||||||
|
Expose the `InventreeHmrPlugin` on a different path (`@inventreedb/ui/vite`) to avoid vite bundling issues.
|
||||||
|
|
||||||
|
### 1.4.2 - June 2026
|
||||||
|
|
||||||
|
Fixes a bug in the `LocalizedComponent` function
|
||||||
|
|
||||||
|
### 1.4.1 - June 2026
|
||||||
|
|
||||||
|
### HMR Support
|
||||||
|
|
||||||
|
Adds support for React Fast Refresh in plugin development. This allows for a much smoother development experience when working on UI plugins, as changes to React components will now trigger a component-level update rather than a full page reload.
|
||||||
|
|
||||||
|
### Localized Components
|
||||||
|
|
||||||
|
Exposes a new `LocalizedComponent` function, which can be used to create React components that are automatically localized using the InvenTree server's localization system.
|
||||||
|
|
||||||
### 1.4.0 - May 2026
|
### 1.4.0 - May 2026
|
||||||
|
|
||||||
#### Version Numbering
|
#### Version Numbering
|
||||||
|
|||||||
@@ -153,3 +153,9 @@ export {
|
|||||||
useStoredTableState
|
useStoredTableState
|
||||||
} from './states/StoredTableState';
|
} from './states/StoredTableState';
|
||||||
export { useLocalLibState } from './states/LocalLibState';
|
export { useLocalLibState } from './states/LocalLibState';
|
||||||
|
|
||||||
|
// Plugin development utilities and hooks
|
||||||
|
export {
|
||||||
|
default as LocalizedComponent,
|
||||||
|
type LocaleLoader
|
||||||
|
} from './plugin/LocalizedComponent';
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Plugin } from 'vite';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vite plugin which enables hot module replacement (HMR) for InvenTree plugin development.
|
||||||
|
*
|
||||||
|
* This is for use with the InvenTree plugin creator tool,
|
||||||
|
* allowing frontend plugin code to be "live reloaded" during development.
|
||||||
|
*/
|
||||||
|
export default function InventreeHmrPlugin(): Plugin {
|
||||||
|
const fileRegex = /\.(js|jsx|ts|tsx)(\?|$)/;
|
||||||
|
|
||||||
|
const hmrBlock = [
|
||||||
|
'',
|
||||||
|
'// __inventree_hmr_injected__',
|
||||||
|
'if (import.meta.hot) {',
|
||||||
|
' import.meta.hot.accept((newModule) => {',
|
||||||
|
' const key = new URL(import.meta.url).origin + new URL(import.meta.url).pathname;',
|
||||||
|
' window.__plugin_hmr_callbacks?.[key]?.forEach(callback => {',
|
||||||
|
' callback(newModule);',
|
||||||
|
' });',
|
||||||
|
' })',
|
||||||
|
'}'
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'inventree-hmr-plugin',
|
||||||
|
enforce: 'post',
|
||||||
|
|
||||||
|
transform(code, id) {
|
||||||
|
if (!fileRegex.test(id)) return;
|
||||||
|
if (id.includes('node_modules')) return;
|
||||||
|
if (code.includes('__inventree_hmr_injected__')) return;
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: code + hmrBlock.join('\n'),
|
||||||
|
map: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import type { I18n } from '@lingui/core';
|
||||||
|
import { I18nProvider } from '@lingui/react';
|
||||||
|
import { Skeleton } from '@mantine/core';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* To dynamically load locale messages from a plugin context,
|
||||||
|
* the plugin MUST supply a callback function which can be used to load the locale messages for the plugin.
|
||||||
|
* This is because the plugin frontend code is built separately from the main frontend,
|
||||||
|
* and so cannot directly import locale messages from the main frontend.
|
||||||
|
*
|
||||||
|
* Refer to the inventree-plugin-creator tool for an example of how to use this component in a plugin context.
|
||||||
|
*/
|
||||||
|
export type LocaleLoader = (locale: string) => Promise<any>;
|
||||||
|
|
||||||
|
async function tryLoadLocale(
|
||||||
|
locale: string,
|
||||||
|
loader: LocaleLoader
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
return await loader(locale);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to load locale ${locale}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param i18n - The i18n instance from the plugin context
|
||||||
|
* @param locale - The current locale to load
|
||||||
|
* @param loader - The callback function to load the locale messages for the plugin
|
||||||
|
* @returns A React component which will load the locale messages and render the children once loaded
|
||||||
|
*/
|
||||||
|
async function loadPluginLocale(
|
||||||
|
i18n: I18n,
|
||||||
|
locale: string,
|
||||||
|
loader: LocaleLoader
|
||||||
|
) {
|
||||||
|
let messages = null;
|
||||||
|
|
||||||
|
messages = await tryLoadLocale(locale, loader);
|
||||||
|
|
||||||
|
if (!messages && locale.includes('-')) {
|
||||||
|
const fallbackLocale = locale.split('-')[0];
|
||||||
|
console.debug(
|
||||||
|
`Locale ${locale} not found, trying fallback locale ${fallbackLocale}`
|
||||||
|
);
|
||||||
|
messages = await tryLoadLocale(fallbackLocale, loader);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messages && locale.includes('_')) {
|
||||||
|
const fallbackLocale = locale.split('_')[0];
|
||||||
|
console.debug(
|
||||||
|
`Locale ${locale} not found, trying fallback locale ${fallbackLocale}`
|
||||||
|
);
|
||||||
|
messages = await tryLoadLocale(fallbackLocale, loader);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messages && locale !== 'en') {
|
||||||
|
console.debug(`Locale ${locale} not found, trying fallback locale en`);
|
||||||
|
messages = await tryLoadLocale('en', loader);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages?.messages) {
|
||||||
|
i18n.load(locale, messages.messages);
|
||||||
|
i18n.activate(locale);
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to load any locale for ${locale}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A default locale loader which can be used if the plugin does not supply its own loader function
|
||||||
|
// Note: This will return null, as the plugin is expected to supply its own loader function which can load the locale messages for the plugin
|
||||||
|
const defaultLocaleLoader: LocaleLoader = async (_locale: string) => null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper function for a plugin-defined component which needs to support dynamic locale loading.
|
||||||
|
*
|
||||||
|
* This is primarily designed for usage by the InvenTree plugin creator tool
|
||||||
|
*
|
||||||
|
* @param i18n - The i18n instance from the plugin context
|
||||||
|
* @param locale - The current locale to load
|
||||||
|
* @param loadLocale - The callback function to load the locale messages for the plugin
|
||||||
|
* @param children - The child components to render once the locale is loaded
|
||||||
|
*/
|
||||||
|
export default function LocalizedComponent({
|
||||||
|
i18n,
|
||||||
|
locale,
|
||||||
|
loadLocale,
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
i18n: I18n;
|
||||||
|
locale: string;
|
||||||
|
loadLocale?: LocaleLoader;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoaded(false);
|
||||||
|
loadPluginLocale(i18n, locale, loadLocale ?? defaultLocaleLoader).then(
|
||||||
|
() => {
|
||||||
|
setLoaded(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [i18n, locale, loadLocale]);
|
||||||
|
|
||||||
|
return loaded ? (
|
||||||
|
<I18nProvider i18n={i18n}>{children}</I18nProvider>
|
||||||
|
) : (
|
||||||
|
<Skeleton w='100%' animate />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@inventreedb/ui",
|
"name": "@inventreedb/ui",
|
||||||
"description": "UI components for the InvenTree project",
|
"description": "UI components for the InvenTree project",
|
||||||
"version": "1.4.0",
|
"version": "1.4.5",
|
||||||
"private": false,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js"
|
".": "./dist/index.js",
|
||||||
|
"./vite": "./dist/plugin/InventreeHmrPlugin.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ export default defineConfig((cfg) =>
|
|||||||
},
|
},
|
||||||
lib: {
|
lib: {
|
||||||
entry: {
|
entry: {
|
||||||
index: resolve(__dirname, 'lib/index.ts')
|
index: resolve(__dirname, 'lib/index.ts'),
|
||||||
|
'plugin/InventreeHmrPlugin': resolve(
|
||||||
|
__dirname,
|
||||||
|
'lib/plugin/InventreeHmrPlugin.tsx'
|
||||||
|
)
|
||||||
},
|
},
|
||||||
name: 'InvenTree',
|
name: 'InvenTree',
|
||||||
formats: ['es']
|
formats: ['es']
|
||||||
|
|||||||
Reference in New Issue
Block a user