diff --git a/.gitignore b/.gitignore index dd87e2d..3c3629e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ node_modules -build diff --git a/build/ModuleFederation.d.ts b/build/ModuleFederation.d.ts new file mode 100644 index 0000000..0502158 --- /dev/null +++ b/build/ModuleFederation.d.ts @@ -0,0 +1,73 @@ +import React, { FunctionComponent, ReactNode } from "react"; +type Module = any; +export declare function registerAndLoadModule(scope: string, module: string, url: string): () => Promise; +export declare function loadModule(scope: string, module: string): () => Promise; +export declare const useCurrentApp: () => SolutionUI; +export type FederatedComponentProps = { + url: string; + scope: string; + module: string; +}; +export type SolutionUI = { + kind: string; + url: string; + name: string; + version: string; + appHistoryBasePath: string; +}; +export declare function FederatedComponent({ url, scope, module, app, renderOnLoading, props, }: FederatedComponentProps & { + props: any; + app: SolutionUI; + renderOnLoading?: ReactNode; +}): React.JSX.Element; +export declare const lazyWithModules: (functionComponent: FunctionComponent>, ...modules: { + module: string; + url: string; + scope: string; +}[]) => React.LazyExoticComponent<(originalProps: Props) => React.ReactNode>; +export declare const ComponentWithFederatedImports: ({ renderOnError, renderOnLoading, componentWithInjectedImports, componentProps, federatedImports, }: { + renderOnError?: ReactNode; + renderOnLoading?: ReactNode; + componentWithInjectedImports: FunctionComponent & { + moduleExports: Record; + }>; + componentProps: Props; + federatedImports: { + remoteEntryUrl: string; + scope: string; + module: string; + }[]; +}) => React.JSX.Element; +type ShellHooks = T["shellHooks"]; +type ShellAlerts = T["shellAlerts"]; +type Listener = () => void; +export declare const shellHooksStore: { + getShellHooks: () => any; + subscribe: (listener: Listener) => () => void; + setShellHooks: (newHooks: any) => void; +}; +export declare const shellAlertsStore: { + getShellAlerts: () => any; + subscribe: (listener: Listener) => () => void; + setShellAlerts: (newAlerts: any) => void; +}; +export declare const useShellHooks: () => ShellHooks; +export declare const useShellAlerts: () => ShellAlerts; +export declare const ShellHooksProvider: ({ shellHooks, shellAlerts, children, }: { + shellHooks: ShellHooks; + shellAlerts: ShellAlerts; + children: ReactNode; +}) => React.JSX.Element; +export {}; diff --git a/build/ModuleFederation.js b/build/ModuleFederation.js new file mode 100644 index 0000000..670d8e0 --- /dev/null +++ b/build/ModuleFederation.js @@ -0,0 +1,163 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ShellHooksProvider = exports.useShellAlerts = exports.useShellHooks = exports.shellAlertsStore = exports.shellHooksStore = exports.ComponentWithFederatedImports = exports.lazyWithModules = exports.FederatedComponent = exports.useCurrentApp = exports.loadModule = exports.registerAndLoadModule = void 0; +const runtime_1 = require("@module-federation/enhanced/runtime"); +const react_1 = __importStar(require("react")); +const registeredApps = []; +function registerAndLoadModule(scope, module, url) { + if (registeredApps.includes(scope)) { + return loadModule(scope, module); + } + registeredApps.push(scope); + (0, runtime_1.registerRemotes)([ + { + name: scope, + entry: url, + }, + ]); + return loadModule(scope, module); +} +exports.registerAndLoadModule = registerAndLoadModule; +function loadModule(scope, module) { + return async () => { + const moduleAbsolutePath = module.substring(1); + const remoteUrl = `${scope}${moduleAbsolutePath}`; + return (0, runtime_1.loadRemote)(remoteUrl); + }; +} +exports.loadModule = loadModule; +const CurrentAppContext = (0, react_1.createContext)(null); +const useCurrentApp = () => { + const contextValue = (0, react_1.useContext)(CurrentAppContext); + if (contextValue === null) { + throw new Error("useCurrentApp can't be used outside of CurrentAppContext.Provider"); + } + return contextValue; +}; +exports.useCurrentApp = useCurrentApp; +function FederatedComponent({ url, scope, module, app, renderOnLoading, props, }) { + const Component = (0, react_1.useMemo)(() => { + return (0, react_1.lazy)(registerAndLoadModule(scope, module, url)); + }, [scope, module, url]); + if (!url || !scope || !module) { + throw new Error("Can't federate a component without url, scope and module"); + } + return (react_1.default.createElement(react_1.Suspense, { fallback: renderOnLoading !== null && renderOnLoading !== void 0 ? renderOnLoading : react_1.default.createElement(react_1.default.Fragment, null, "Loading...") }, + react_1.default.createElement(exports.ShellHooksProvider, { shellHooks: props.shellHooks, shellAlerts: props.shellAlerts }, + react_1.default.createElement(CurrentAppContext.Provider, { value: app }, + react_1.default.createElement(Component, { ...props }))))); +} +exports.FederatedComponent = FederatedComponent; +const lazyWithModules = (functionComponent, ...modules) => { + return react_1.default.lazy(async () => { + const loadedModules = await Promise.all(modules.map((mod) => { + return registerAndLoadModule(mod.scope, mod.module, mod.url)(); + })); + const moduleExports = loadedModules.reduce((current, loadedModule, index) => ({ + ...current, + [modules[index].module]: loadedModule, + }), {}); + return { + __esModule: true, + default: (originalProps) => functionComponent({ moduleExports: moduleExports, ...originalProps }), + }; + }); +}; +exports.lazyWithModules = lazyWithModules; +const ComponentWithFederatedImports = ({ renderOnError, renderOnLoading, componentWithInjectedImports, componentProps, federatedImports, }) => { + const Component = (0, react_1.useMemo)(() => (0, exports.lazyWithModules)(componentWithInjectedImports, ...federatedImports.map((federatedImport) => ({ + scope: federatedImport.scope, + module: federatedImport.module, + url: federatedImport.remoteEntryUrl, + }))), [JSON.stringify(federatedImports)]); + return (react_1.default.createElement(react_1.Suspense, { fallback: renderOnLoading !== null && renderOnLoading !== void 0 ? renderOnLoading : react_1.default.createElement(react_1.default.Fragment, null, "Loading...") }, + react_1.default.createElement(Component, { ...componentProps }))); +}; +exports.ComponentWithFederatedImports = ComponentWithFederatedImports; +const createShellHooksStore = () => { + let shellHooks = null; + const listeners = new Set(); + return { + getShellHooks: () => shellHooks, + subscribe: (listener) => { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, + setShellHooks: (newHooks) => { + if (shellHooks !== newHooks) { + shellHooks = newHooks; + listeners.forEach((listener) => listener()); + } + }, + }; +}; +const createShellAlertsStore = () => { + let shellAlerts = null; + const listeners = new Set(); + return { + getShellAlerts: () => shellAlerts, + subscribe: (listener) => { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, + setShellAlerts: (newAlerts) => { + if (shellAlerts !== newAlerts) { + shellAlerts = newAlerts; + listeners.forEach((listener) => listener()); + } + }, + }; +}; +exports.shellHooksStore = createShellHooksStore(); +exports.shellAlertsStore = createShellAlertsStore(); +const useShellHooks = () => { + const hooks = (0, react_1.useSyncExternalStore)(exports.shellHooksStore.subscribe, exports.shellHooksStore.getShellHooks); + if (!hooks) { + throw new Error("useShellHooks must be used within a ShellHooksProvider and initialized with valid hooks."); + } + return hooks; +}; +exports.useShellHooks = useShellHooks; +const useShellAlerts = () => { + const alerts = (0, react_1.useSyncExternalStore)(exports.shellAlertsStore.subscribe, exports.shellAlertsStore.getShellAlerts); + if (!alerts) { + throw new Error("useShellAlerts must be used within a ShellHooksProvider and initialized with valid alerts."); + } + return alerts; +}; +exports.useShellAlerts = useShellAlerts; +const ShellHooksProvider = ({ shellHooks, shellAlerts, children, }) => { + (0, react_1.useMemo)(() => { + exports.shellHooksStore.setShellHooks(shellHooks); + exports.shellAlertsStore.setShellAlerts(shellAlerts); + }, [shellHooks, shellAlerts]); + return react_1.default.createElement(react_1.default.Fragment, null, children); +}; +exports.ShellHooksProvider = ShellHooksProvider; diff --git a/package-lock.json b/package-lock.json index 7453456..aa2138a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,16 +11,16 @@ "devDependencies": { "@module-federation/enhanced": "^0.2.2", "@types/jest": "^26.0.23", - "@types/react": "^17.0.11", - "@types/react-dom": "^17.0.8", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", "jest": "^27.0.5", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", "typescript": "^5.3.3" }, "peerDependencies": { - "react": "^17.0.2", - "react-dom": "^17.0.2" + "react": "^18.3.1", + "react-dom": "^18.3.1" } }, "node_modules/@ampproject/remapping": { @@ -2090,31 +2090,24 @@ "dev": true }, "node_modules/@types/react": { - "version": "17.0.47", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.47.tgz", - "integrity": "sha512-mk0BL8zBinf2ozNr3qPnlu1oyVTYq+4V7WA76RgxUAtf0Em/Wbid38KN6n4abEkvO4xMTBWmnP1FtQzgkEiJoA==", + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "dev": true, "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "17.0.17", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.17.tgz", - "integrity": "sha512-VjnqEmqGnasQKV0CWLevqMTXBYG9GbwuE6x3VetERLh0cq2LTptFE73MrQi2S7GkKXCf2GgwItB/melLnxfnsg==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "dev": true, "dependencies": { - "@types/react": "^17" + "@types/react": "*" } }, - "node_modules/@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true - }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -7459,15 +7452,6 @@ "integrity": "sha512-JYOWTeFoS0Z93587vRJgASD5Ut11fYl5NyihP3KrYBvMe1FRRs6RN7m20SA/16GM4P6hTnZjT+UmDOt38UeXNg==", "dev": true }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -7781,30 +7765,28 @@ "dev": true }, "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "17.0.2" + "react": "^18.3.1" } }, "node_modules/require-directory": { @@ -7936,13 +7918,12 @@ } }, "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dev": true, "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "node_modules/semver": { @@ -10343,31 +10324,24 @@ "dev": true }, "@types/react": { - "version": "17.0.47", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.47.tgz", - "integrity": "sha512-mk0BL8zBinf2ozNr3qPnlu1oyVTYq+4V7WA76RgxUAtf0Em/Wbid38KN6n4abEkvO4xMTBWmnP1FtQzgkEiJoA==", + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "dev": true, "requires": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, "@types/react-dom": { - "version": "17.0.17", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.17.tgz", - "integrity": "sha512-VjnqEmqGnasQKV0CWLevqMTXBYG9GbwuE6x3VetERLh0cq2LTptFE73MrQi2S7GkKXCf2GgwItB/melLnxfnsg==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "dev": true, "requires": { - "@types/react": "^17" + "@types/react": "*" } }, - "@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true - }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -14467,12 +14441,6 @@ "integrity": "sha512-JYOWTeFoS0Z93587vRJgASD5Ut11fYl5NyihP3KrYBvMe1FRRs6RN7m20SA/16GM4P6hTnZjT+UmDOt38UeXNg==", "dev": true }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true - }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -14710,24 +14678,22 @@ "dev": true }, "react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "requires": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "scheduler": "^0.23.2" } }, "require-directory": { @@ -14815,13 +14781,12 @@ } }, "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dev": true, "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "semver": { diff --git a/package.json b/package.json index 1e4eced..c3856a9 100644 --- a/package.json +++ b/package.json @@ -21,16 +21,16 @@ "devDependencies": { "@module-federation/enhanced": "^0.2.2", "@types/jest": "^26.0.23", - "@types/react": "^17.0.11", - "@types/react-dom": "^17.0.8", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", "jest": "^27.0.5", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", "typescript": "^5.3.3" }, "peerDependencies": { - "react": "^17.0.2", - "react-dom": "^17.0.2" + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "publishConfig": { "access": "public" diff --git a/src/ModuleFederation.tsx b/src/ModuleFederation.tsx index 064b9aa..0c2d547 100644 --- a/src/ModuleFederation.tsx +++ b/src/ModuleFederation.tsx @@ -3,6 +3,7 @@ import { registerRemotes, } from "@module-federation/enhanced/runtime"; import React, { + FC, FunctionComponent, ReactNode, Suspense, @@ -10,6 +11,7 @@ import React, { lazy, useContext, useMemo, + useSyncExternalStore, } from "react"; type Module = any; @@ -97,15 +99,20 @@ export function FederatedComponent({ return ( Loading...}> - - - + + + + + ); } export const lazyWithModules = ( - functionComponent: FunctionComponent, + functionComponent: FunctionComponent>, ...modules: { module: string; url: string; scope: string }[] ) => { return React.lazy(async () => { @@ -139,7 +146,7 @@ export const ComponentWithFederatedImports = ({ renderOnError?: ReactNode; renderOnLoading?: ReactNode; componentWithInjectedImports: FunctionComponent< - Props & { moduleExports: Record } + React.PropsWithChildren & { moduleExports: Record } >; componentProps: Props; federatedImports: { @@ -168,3 +175,115 @@ export const ComponentWithFederatedImports = ({ ); }; + +type ShellHooks = T["shellHooks"]; +type ShellAlerts = T["shellAlerts"]; + +type Listener = () => void; + +const createShellHooksStore = () => { + let shellHooks: ShellHooks | null = null; + + const listeners: Set = new Set(); + + return { + getShellHooks: () => shellHooks, + + subscribe: (listener: Listener) => { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, + + setShellHooks: (newHooks: ShellHooks) => { + if (shellHooks !== newHooks) { + shellHooks = newHooks; + listeners.forEach((listener) => listener()); + } + }, + }; +}; + +const createShellAlertsStore = () => { + let shellAlerts: ShellAlerts | null = null; + const listeners: Set = new Set(); + + return { + getShellAlerts: () => shellAlerts, + + subscribe: (listener: Listener) => { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, + + setShellAlerts: (newAlerts: ShellAlerts) => { + if (shellAlerts !== newAlerts) { + shellAlerts = newAlerts; + listeners.forEach((listener) => listener()); + } + }, + }; +}; + +export const shellHooksStore = createShellHooksStore(); +export const shellAlertsStore = createShellAlertsStore(); + +export const useShellHooks = < + T extends { shellHooks: any } +>(): ShellHooks => { + const hooks = useSyncExternalStore( + shellHooksStore.subscribe, + shellHooksStore.getShellHooks + ); + + if (!hooks) { + throw new Error( + "useShellHooks must be used within a ShellHooksProvider and initialized with valid hooks." + ); + } + + return hooks; +}; + +export const useShellAlerts = < + T extends { shellAlerts: any } +>(): ShellAlerts => { + const alerts = useSyncExternalStore( + shellAlertsStore.subscribe, + shellAlertsStore.getShellAlerts + ); + + if (!alerts) { + throw new Error( + "useShellAlerts must be used within a ShellHooksProvider and initialized with valid alerts." + ); + } + + return alerts; +}; + +export const ShellHooksProvider = < + T extends { shellHooks: any }, + K extends { shellAlerts: any } +>({ + shellHooks, + shellAlerts, + children, +}: { + shellHooks: ShellHooks; + shellAlerts: ShellAlerts; + children: ReactNode; +}) => { + useMemo(() => { + if (shellHooks) { + shellHooksStore.setShellHooks(shellHooks); + } + if (shellAlerts) { + shellAlertsStore.setShellAlerts(shellAlerts); + } + }, [shellHooks, shellAlerts]); + return <>{children}; +};