diff --git a/dist/core.d.ts b/dist/core.d.ts index 7e55e1d..f584a68 100644 --- a/dist/core.d.ts +++ b/dist/core.d.ts @@ -41,7 +41,6 @@ export type BiProviderProps = { permissionEnabled?: boolean; theme?: any; remoteUiEnabled?: boolean; - remoteUiUrl?: string; remoteUiChakraProviderProps?: Record; }; diff --git a/dist/core.js b/dist/core.js index 680b107..1ee169d 100644 --- a/dist/core.js +++ b/dist/core.js @@ -1,78 +1,79 @@ -import j from "axios"; -import { jsx as q, jsxs as M } from "react/jsx-runtime"; -import _ from "keycloak-js"; -import $, { createContext as V, useState as z, useRef as N, useCallback as X, useEffect as g, useMemo as x, useContext as ee } from "react"; -import { useQueryClient as te, useMutation as re, useQuery as f, QueryClient as ne, QueryClientProvider as oe } from "@tanstack/react-query"; -import { extendTheme as ie, withDefaultColorScheme as se, ChakraProvider as R, Spinner as ae, Badge as ue } from "@chakra-ui/react"; +import Q from "axios"; +import { jsx as Z, jsxs as A } from "react/jsx-runtime"; +import j from "keycloak-js"; +import _, { createContext as C, useState as X, useRef as L, useCallback as K, useEffect as z, useMemo as g, useContext as $ } from "react"; +import { useQueryClient as ee, useMutation as te, useQuery as f, QueryClient as re, QueryClientProvider as ne } from "@tanstack/react-query"; +import { createRoot as oe } from "react-dom/client"; +import { extendTheme as ie, withDefaultColorScheme as se, ChakraProvider as V, Spinner as ae, Badge as ue } from "@chakra-ui/react"; import { Global as de } from "@emotion/react"; -let W = null, o = null, O = null, v = !1; -const ce = V(null), le = ({ url: e, realm: t, clientId: r }) => (o || (o = new _({ +let W = null, o = null, Y = null, O = !1; +const ce = C(null), le = ({ url: e, realm: r, clientId: t }) => (o || (o = new j({ url: e, - realm: t, - clientId: r -})), o), he = (e) => (W = e, W), Fe = () => v, I = () => !!(o != null && o.authenticated), w = () => (o == null ? void 0 : o.token) ?? null, D = async (e = 30) => o ? o.updateToken(e) : !1, G = (e) => o == null ? void 0 : o.logout(e), Ge = (e) => o == null ? void 0 : o.login(e), J = ({ config: e, initOptions: t = {} } = {}) => { - const r = e ?? W; - if (!(r != null && r.url) || !(r != null && r.realm) || !(r != null && r.clientId)) + realm: r, + clientId: t +})), o), he = (e) => (W = e, W), Se = () => O, N = () => !!(o != null && o.authenticated), x = () => (o == null ? void 0 : o.token) ?? null, D = async (e = 30) => o ? o.updateToken(e) : !1, R = (e) => o == null ? void 0 : o.logout(e), Je = (e) => o == null ? void 0 : o.login(e), I = ({ config: e, initOptions: r = {} } = {}) => { + const t = e ?? W; + if (!(t != null && t.url) || !(t != null && t.realm) || !(t != null && t.clientId)) throw new Error("Keycloak config is required."); - he(r); - const n = le(r); - return O || (O = n.init({ + he(t); + const n = le(t); + return Y || (Y = n.init({ onLoad: "login-required", checkLoginIframe: !1, flow: "implicit", - ...t - }).then((u) => (v = !0, u)).catch((u) => { - throw v = !0, u; - })), O; -}, Se = ({ + ...r + }).then((d) => (O = !0, d)).catch((d) => { + throw O = !0, d; + })), Y; +}, Fe = ({ children: e, - config: t, - initOptions: r, + config: r, + initOptions: t, loading: n = null }) => { - const [u, h] = z(!1), [d, s] = z(!1), [a, S] = z(null), p = N(!1), i = X(() => { - o && (s(!!o.authenticated), S(o.token ?? null)); + const [d, l] = X(!1), [u, s] = X(!1), [a, F] = X(null), p = L(!1), i = K(() => { + o && (s(!!o.authenticated), F(o.token ?? null)); }, []); - g(() => (p.current = !0, J({ config: t, initOptions: r }).then((c) => { - p.current && (s(!!c), S((o == null ? void 0 : o.token) ?? null), h(!0)); + z(() => (p.current = !0, I({ config: r, initOptions: t }).then((h) => { + p.current && (s(!!h), F((o == null ? void 0 : o.token) ?? null), l(!0)); }).catch(() => { - p.current && (s(!1), S(null), h(!0)); + p.current && (s(!1), F(null), l(!0)); }), () => { p.current = !1; - }), [t, r]); - const l = X(() => o == null ? void 0 : o.login(), []), m = X((c) => o == null ? void 0 : o.logout(c), []), F = X( - async (c = 30) => { + }), [r, t]); + const c = K(() => o == null ? void 0 : o.login(), []), m = K((h) => o == null ? void 0 : o.logout(h), []), S = K( + async (h = 30) => { if (!o) return !1; - const Z = await o.updateToken(c); - return i(), Z; + const E = await o.updateToken(h); + return i(), E; }, [i] - ), E = x( + ), q = g( () => ({ - initialized: u, - authenticated: d, + initialized: d, + authenticated: u, token: a, - login: l, + login: c, logout: m, - updateToken: F + updateToken: S }), - [u, d, a, l, m, F] + [d, u, a, c, m, S] ); - return u ? /* @__PURE__ */ q(ce.Provider, { value: E, children: e }) : n; + return d ? /* @__PURE__ */ Z(ce.Provider, { value: q, children: e }) : n; }; let B = null; const pe = (e) => { - const { api: t } = e, r = j.create({ - baseURL: t.baseUrl + const { api: r } = e, t = Q.create({ + baseURL: r.baseUrl }), n = async () => { - (!Fe() || !I()) && await J(); - }, u = !!e.keycloak, h = async (s) => { - if (!u) return s; + (!Se() || !N()) && await I(); + }, d = !!e.keycloak, l = async (s) => { + if (!d) return s; await n(); - let a = w(); + let a = x(); if (a) { try { - await D(5), a = w(); + await D(5), a = x(); } catch { } s.headers = { @@ -81,271 +82,271 @@ const pe = (e) => { }; } return s; - }, d = async (s) => { - var S; + }, u = async (s) => { + var F; const a = s.config; - if (((S = s == null ? void 0 : s.response) == null ? void 0 : S.status) === 401 && !(a != null && a._retry)) { + if (((F = s == null ? void 0 : s.response) == null ? void 0 : F.status) === 401 && !(a != null && a._retry)) { a._retry = !0; try { await D(30); - const p = w(); + const p = x(); return a.headers = { ...a.headers, Authorization: `Bearer ${p}` - }, r(a); + }, t(a); } catch { - G({ redirectUri: window.location.origin }); + R({ redirectUri: window.location.origin }); } } return Promise.reject(s); }; - return r.interceptors.request.use(h), r.interceptors.response.use( + return t.interceptors.request.use(l), t.interceptors.response.use( (s) => s, - d + u ), { - api: r + api: t }; -}, me = (e) => (B = pe(e), B), Ee = () => { +}, me = (e) => (B = pe(e), B), qe = () => { if (!B) throw new Error("api is not configured. Call configureApi(config) first."); return B.api; -}, H = new Proxy( +}, G = new Proxy( {}, { - get(e, t) { - return Ee()[t]; + get(e, r) { + return qe()[r]; } } -), qe = async (e) => (await H.get(e)).data; -function P(e) { +), Ee = async (e) => (await G.get(e)).data; +function v(e) { return Array.isArray(e) ? e : [e]; } function Ze(e) { - return [...P(e), "list"]; + return [...v(e), "list"]; } -function C(e, t) { - return [...P(e), t]; +function M(e, r) { + return [...v(e), r]; } -function be(e, t) { - if (!t) return e; - const r = new URLSearchParams(); - Object.entries(t).forEach(([u, h]) => { - h != null && h !== "" && r.append(u, h); +function be(e, r) { + if (!r) return e; + const t = new URLSearchParams(); + Object.entries(r).forEach(([d, l]) => { + l != null && l !== "" && t.append(d, l); }); - const n = r.toString(); + const n = t.toString(); return n ? `${e}?${n}` : e; } -function ye(e, t) { - return typeof e == "function" ? e(t == null ? void 0 : t.id, t) : e; +function ye(e, r) { + return typeof e == "function" ? e(r == null ? void 0 : r.id, r) : e; } function Te(e = {}) { - const { id: t, ...r } = e; - return r; + const { id: r, ...t } = e; + return t; } -function Ke(e, t) { - return `${e.replace(/\/$/, "")}/${t}`; +function Ke(e, r) { + return `${e.replace(/\/$/, "")}/${r}`; } function Xe(e) { return `use${e.charAt(0).toUpperCase()}${e.slice(1)}`; } -function Je({ +function He({ key: e, - url: t, - actions: r = {}, - axiosInstance: n = H, - fetcher: u = qe + url: r, + actions: t = {}, + axiosInstance: n = G, + fetcher: d = Ee }) { - const h = P(e), d = Ze(e); - function s(i, l = {}) { + const l = v(e), u = Ze(e); + function s(i, c = {}) { return f({ - queryKey: i ? [...d, i] : d, - queryFn: () => u(be(t, i)), - ...l + queryKey: i ? [...u, i] : u, + queryFn: () => d(be(r, i)), + ...c }); } - function a(i, l = {}) { + function a(i, c = {}) { return f({ - queryKey: C(e, i), - queryFn: () => u(Ke(t, i)), + queryKey: M(e, i), + queryFn: () => d(Ke(r, i)), enabled: !!i, - ...l + ...c }); } - function S(i, l = {}) { - const m = te(), F = r[i]; - if (!F) + function F(i, c = {}) { + const m = ee(), S = t[i]; + if (!S) throw new Error(`Action "${i}" is not defined.`); - return re({ - mutationKey: [...h, i], - mutationFn: async (E = {}) => { - const c = F.method || "post", Z = ye(F.url || t, E), b = F.body ? F.body(E) : F.sendRawPayload ? E : Te(E); - return (c === "delete" ? await n.delete( - Z, + return te({ + mutationKey: [...l, i], + mutationFn: async (q = {}) => { + const h = S.method || "post", E = ye(S.url || r, q), b = S.body ? S.body(q) : S.sendRawPayload ? q : Te(q); + return (h === "delete" ? await n.delete( + E, b && Object.keys(b).length > 0 ? { data: b } : void 0 - ) : await n[c](Z, b)).data; + ) : await n[h](E, b)).data; }, - onSuccess: (...E) => { + onSuccess: (...q) => { var T; - const [c, Z] = E; + const [h, E] = q; m.invalidateQueries({ - queryKey: d + queryKey: u }); - const b = (Z == null ? void 0 : Z.id) ?? (c == null ? void 0 : c.id); + const b = (E == null ? void 0 : E.id) ?? (h == null ? void 0 : h.id); b && m.invalidateQueries({ - queryKey: C(e, b) + queryKey: M(e, b) }); - const y = typeof F.invalidateKeys == "function" ? F.invalidateKeys(c, Z) : F.invalidateKeys; - y == null || y.forEach((K) => { - m.invalidateQueries({ queryKey: K }); - }), (T = l.onSuccess) == null || T.call(l, ...E); + const y = typeof S.invalidateKeys == "function" ? S.invalidateKeys(h, E) : S.invalidateKeys; + y == null || y.forEach((k) => { + m.invalidateQueries({ queryKey: k }); + }), (T = c.onSuccess) == null || T.call(c, ...q); }, - ...l + ...c }); } - const p = Object.keys(r).reduce((i, l) => (i[Xe(l)] = (m = {}) => S(l, m), i), {}); + const p = Object.keys(t).reduce((i, c) => (i[Xe(c)] = (m = {}) => F(c, m), i), {}); return { - key: h, - listKey: d, + key: l, + listKey: u, useGet: s, useItem: a, - useAction: S, + useAction: F, ...p }; } -const k = V(null), Y = "permissions", ze = (e) => (e == null ? void 0 : e.storageKey) || Y, ge = ({ +const J = C(null), w = "permissions", ze = (e) => (e == null ? void 0 : e.storageKey) || w, xe = ({ user: e = null, - children: t, - config: r, - loading: n = /* @__PURE__ */ q("div", { children: "Loading permissions..." }), - errorFallback: u = /* @__PURE__ */ q("div", { children: "Error loading permissions" }) + children: r, + config: t, + loading: n = /* @__PURE__ */ Z("div", { children: "Loading permissions..." }), + errorFallback: d = /* @__PURE__ */ Z("div", { children: "Error loading permissions" }) }) => { - const h = ze(r), [d, s] = z( - () => we(h) - ), { data: a, isLoading: S, error: p } = fe(r), i = a ?? d; - g(() => { - a && (xe(h, a), s(a)); - }, [a, h]); - const l = x( - () => Be(i), + const l = ze(t), [u, s] = X( + () => ge(l) + ), { data: a, isLoading: F, error: p } = We(t), i = a ?? u; + z(() => { + a && (Be(l, a), s(a)); + }, [a, l]); + const c = g( + () => we(i), [i] - ), m = X( - (c = "") => Ye(l, c), - [l] - ), F = X((c = "") => !m(c), [m]), E = x( + ), m = K( + (h = "") => Ye(c, h), + [c] + ), S = K((h = "") => !m(h), [m]), q = g( () => ({ user: e, can: m, - cannot: F, + cannot: S, permissions: i, - isLoading: S, + isLoading: F, error: p }), - [e, m, F, i, S, p] + [e, m, S, i, F, p] ); - return p && !i ? u : S && !i ? n : /* @__PURE__ */ q(k.Provider, { value: E, children: t }); -}, we = (e = Y) => { + return p && !i ? d : F && !i ? n : /* @__PURE__ */ Z(J.Provider, { value: q, children: r }); +}, ge = (e = w) => { if (!(typeof window > "u")) try { - const t = localStorage.getItem(e); - return t ? JSON.parse(t) : void 0; + const r = localStorage.getItem(e); + return r ? JSON.parse(r) : void 0; } catch { return; } -}, xe = (e = Y, t) => { +}, Be = (e = w, r) => { if (!(typeof window > "u")) try { - localStorage.setItem(e, JSON.stringify(t)); + localStorage.setItem(e, JSON.stringify(r)); } catch { } -}, He = (e = Y) => { +}, ke = (e = w) => { if (!(typeof window > "u")) try { localStorage.removeItem(e); } catch { } -}, Be = (e) => e ? new Set(e.map((t) => t.name)) : /* @__PURE__ */ new Set(), Ye = (e, t = "") => { - if (!t) return !1; - if (e.has(t) || [...e].some((n) => n.startsWith(t))) return !0; - let r = t; - for (; r.includes(":"); ) - if (r = r.slice(0, r.lastIndexOf(":")), e.has(`${r}:*`)) return !0; +}, we = (e) => e ? new Set(e.map((r) => r.name)) : /* @__PURE__ */ new Set(), Ye = (e, r = "") => { + if (!r) return !1; + if (e.has(r) || [...e].some((n) => n.startsWith(r))) return !0; + let t = r; + for (; t.includes(":"); ) + if (t = t.slice(0, t.lastIndexOf(":")), e.has(`${t}:*`)) return !0; return !1; -}, Q = { +}, H = { all: ["permissions"], - list: (e) => [...Q.all, e] -}, Oe = async (e) => { + list: (e) => [...H.all, e] +}, fe = async (e) => { if (!(e != null && e.url) || !(e != null && e.clientId)) throw new Error("Permission config requires url and clientId."); - const t = w(), r = new URL(e.url); - r.searchParams.set("client_id", e.clientId); - const n = await fetch(r.toString(), { + const r = x(), t = new URL(e.url); + t.searchParams.set("client_id", e.clientId); + const n = await fetch(t.toString(), { method: "GET", headers: { - ...t ? { Authorization: `Bearer ${t}` } : {} + ...r ? { Authorization: `Bearer ${r}` } : {} } }); if (!n.ok) throw new Error("Failed to load permissions"); return n.json(); -}, A = () => { - const e = ee(k); +}, P = () => { + const e = $(J); if (!e) throw new Error("usePermission must be used within PermissionProvider"); return e; -}, fe = (e) => f({ - queryKey: Q.list(e == null ? void 0 : e.clientId), - queryFn: () => Oe(e), - enabled: !!(e != null && e.url) && !!(e != null && e.clientId) && I(), +}, We = (e) => f({ + queryKey: H.list(e == null ? void 0 : e.clientId), + queryFn: () => fe(e), + enabled: !!(e != null && e.url) && !!(e != null && e.clientId) && N(), staleTime: 1 / 0, refetchOnWindowFocus: !1, refetchOnReconnect: !1 -}), ke = ({ +}), Qe = ({ perm: e, - children: t, - fallback: r = /* @__PURE__ */ q("div", { children: "You are not allowed to do this" }) + children: r, + fallback: t = /* @__PURE__ */ Z("div", { children: "You are not allowed to do this" }) }) => { - const { can: n } = A(); - return n(e) ? t : r; -}, Qe = ({ children: e }) => { - const { can: t, permissions: r } = A(); - return r && t("admin") ? e : null; -}, je = ({ children: e, forceNoAccess: t = !1 }) => { - const { can: r, isLoading: n } = A(); - return g(() => { - n || (t || !r("panel_access")) && G(); - }, [n, r, t]), e; + const { can: n } = P(); + return n(e) ? r : t; +}, je = ({ children: e }) => { + const { can: r, permissions: t } = P(); + return t && r("admin") ? e : null; +}, _e = ({ children: e, forceNoAccess: r = !1 }) => { + const { can: t, isLoading: n } = P(); + return z(() => { + n || (r || !t("panel_access")) && R(); + }, [n, t, r]), e; }; -function We() { - const [e, t] = z(!1), r = N(null); - return g(() => { +function Oe() { + const [e, r] = X(!1), t = L(null); + return z(() => { if (process.env.NODE_ENV !== "production") return; let n; - const u = async () => { - const d = await fetch(`/version.json?t=${Date.now()}`, { + const d = async () => { + const u = await fetch(`/version.json?t=${Date.now()}`, { cache: "no-store" }); - if (!d.ok) - throw new Error(`Failed to fetch version: ${d.status}`); - const s = await d.json(); + if (!u.ok) + throw new Error(`Failed to fetch version: ${u.status}`); + const s = await u.json(); return String(s.version); }; return (async () => { try { - r.current = await u(), n = setInterval(async () => { + t.current = await d(), n = setInterval(async () => { try { - const d = await u(); - r.current && d !== r.current && (t(!0), clearInterval(n)); - } catch (d) { - console.error("Update check failed:", d); + const u = await d(); + t.current && u !== t.current && (r(!0), clearInterval(n)); + } catch (u) { + console.error("Update check failed:", u); } }, 3e4); - } catch (d) { - console.error("Initial version fetch failed:", d); + } catch (u) { + console.error("Initial version fetch failed:", u); } })(), () => { n && clearInterval(n); }; - }, []), e ? /* @__PURE__ */ q( + }, []), e ? /* @__PURE__ */ Z( "div", { style: { @@ -364,7 +365,7 @@ function We() { } ) : null; } -const ve = new ne({ +const ve = new re({ defaultOptions: { queries: { retry: 1, @@ -395,7 +396,7 @@ const ve = new ne({ } }, se({ colorScheme: "facebook" }) -), Me = () => /* @__PURE__ */ q( +), De = () => /* @__PURE__ */ Z( de, { styles: ` @@ -408,115 +409,111 @@ const ve = new ne({ } ` } -), U = "__UIKIT_REMOTE_DEPS__", L = "__UIKIT_REMOTE_CONFIG__"; -function De({ theme: e, remoteUiUrl: t, chakraProviderProps: r } = {}) { +), U = "__UIKIT_REMOTE_DEPS__"; +function Me({ theme: e, chakraProviderProps: r } = {}) { if (typeof window > "u") return; - const n = window[U] || {}, u = window[L] || {}; + const t = window[U] || {}; window[U] = { - ...n, - React: n.React || $, - ChakraProvider: n.ChakraProvider || R, - Badge: n.Badge || ue, - theme: e ?? n.theme, + ...t, + React: t.React || _, + createRoot: t.createRoot || oe, + ChakraProvider: t.ChakraProvider || V, + Badge: t.Badge || ue, + theme: e ?? t.theme, chakraProviderProps: { - ...n.chakraProviderProps || {}, + ...t.chakraProviderProps || {}, ...r || {} } - }, window[L] = { - ...u, - ...t ? { GBadgeUrl: t } : {} }; } -function _e({ +function $e({ children: e, - apiBaseUrl: t, - keycloakClientId: r, + apiBaseUrl: r, + keycloakClientId: t, permissionClientId: n, - loading: u = /* @__PURE__ */ q(ae, {}), - updateChecker: h = !1, - updateCheckerProps: d, + loading: d = /* @__PURE__ */ Z(ae, {}), + updateChecker: l = !1, + updateCheckerProps: u, keycloakUrl: s = "https://auth.ibagher.ir", keycloakRealm: a = "bi", - permissionUrl: S = "https://api.d.aiengines.ir/user_api/v1/permissions/list", + permissionUrl: F = "https://api.d.aiengines.ir/user_api/v1/permissions/list", keycloakEnabled: p, permissionEnabled: i, - theme: l = Ae, + theme: c = Ae, remoteUiEnabled: m = !0, - remoteUiUrl: F, - remoteUiChakraProviderProps: E + remoteUiChakraProviderProps: S }) { - const [c, Z] = z(!1), b = p ?? !!r, y = i ?? !!n, T = x( + const [q, h] = X(!1), E = p ?? !!t, b = i ?? !!n, y = g( () => ({ api: { - baseUrl: t || "" + baseUrl: r || "" }, - keycloak: b ? { + keycloak: E ? { url: s, realm: a, - clientId: r + clientId: t } : null, - permission: y ? { - url: S, + permission: b ? { + url: F, clientId: n } : null }), [ - t, + r, + E, b, - y, s, a, - r, - S, + t, + F, n ] ); - if (g(() => { - Z(!0); - }, []), g(() => { - c && me(T); - }, [c, T]), !c) + if (z(() => { + h(!0); + }, []), z(() => { + q && me(y); + }, [q, y]), !q) return null; - m && De({ - theme: l, - remoteUiUrl: F, - chakraProviderProps: E + m && Me({ + theme: c, + chakraProviderProps: S }); - let K = e; - return y && (K = /* @__PURE__ */ q(ge, { config: T.permission, loading: u, children: K })), b && (K = /* @__PURE__ */ q(Se, { config: T.keycloak, loading: u, children: K })), /* @__PURE__ */ M(R, { theme: l, children: [ - /* @__PURE__ */ q(Me, {}), - /* @__PURE__ */ M(oe, { client: ve, children: [ - h && /* @__PURE__ */ q(We, { ...d }), - K + let T = e; + return b && (T = /* @__PURE__ */ Z(xe, { config: y.permission, loading: d, children: T })), E && (T = /* @__PURE__ */ Z(Fe, { config: y.keycloak, loading: d, children: T })), /* @__PURE__ */ A(V, { theme: c, children: [ + /* @__PURE__ */ Z(De, {}), + /* @__PURE__ */ A(ne, { client: ve, children: [ + l && /* @__PURE__ */ Z(Oe, { ...u }), + T ] }) ] }); } export { - Qe as AdminGuard, - _e as BiProvider, - ke as Can, - Se as KeycloakProvider, - je as PanelAccessGuard, - ge as PermissionProvider, - H as api, + je as AdminGuard, + $e as BiProvider, + Qe as Can, + Fe as KeycloakProvider, + _e as PanelAccessGuard, + xe as PermissionProvider, + G as api, Ye as checkPermission, - He as clearCachedPermissions, + ke as clearCachedPermissions, me as configureApi, - Je as createApi, - Be as createPermissionSet, - qe as fetcher, - Ee as getApi, - we as getCachedPermissions, - w as getKeycloakToken, - Oe as getPermissions, - J as initKeycloak, - I as isKeycloakAuthenticated, - Fe as isKeycloakInitialized, - Ge as loginKeycloak, - G as logoutKeycloak, - Q as permissionKeys, - xe as setCachedPermissions, + He as createApi, + we as createPermissionSet, + Ee as fetcher, + qe as getApi, + ge as getCachedPermissions, + x as getKeycloakToken, + fe as getPermissions, + I as initKeycloak, + N as isKeycloakAuthenticated, + Se as isKeycloakInitialized, + Je as loginKeycloak, + R as logoutKeycloak, + H as permissionKeys, + Be as setCachedPermissions, D as updateKeycloakToken, - A as usePermission, - fe as usePermissionsQuery + P as usePermission, + We as usePermissionsQuery }; diff --git a/dist/remote-ui.d.ts b/dist/remote-ui.d.ts index dc44fd1..9f1246e 100644 --- a/dist/remote-ui.d.ts +++ b/dist/remote-ui.d.ts @@ -1,54 +1,24 @@ -import type { BadgeProps, StackProps } from "@chakra-ui/react"; -import type { ReactNode } from "react"; +export { + setupUikitRemoteDeps, +} from "./_core/setupRemoteDeps"; -export type UikitRemoteLevel = "full" | "compact" | "simple"; +export type { + UikitRemoteSetupOptions, +} from "./_core/setupRemoteDeps"; -export interface UikitRemoteSetupOptions { - React?: any; - createRoot?: any; - ChakraProvider?: any; - Badge?: any; - Button?: any; - HStack?: any; - IconButton?: any; - Input?: any; - Text?: any; - theme?: any; - chakraProviderProps?: Record; +export { + GBadge, +} from "./GBadge"; - /** Backward compatible alias for GBadgeUrl. */ - remoteUrl?: string; - GBadgeUrl?: string; - PaginationUrl?: string; - remoteUrls?: { - GBadge?: string; - Pagination?: string; - }; -} +export type { + GBadgeProps, +} from "./GBadge"; -export declare function setupUikitRemoteDeps( - options?: UikitRemoteSetupOptions, -): void; +export { + Pagination, +} from "./Pagination"; -export interface GBadgeProps extends BadgeProps { - children?: ReactNode; - remoteUrl?: string; - theme?: any; - chakraProviderProps?: Record; -} - -export declare const GBadge: (props: GBadgeProps) => JSX.Element; - -export interface PaginationProps extends Omit { - currentPage?: number; - totalCount?: number; - totalPages?: number; - pageSize?: number; - onPageChange?: (page: number) => void; - level?: UikitRemoteLevel; - remoteUrl?: string; - theme?: any; - chakraProviderProps?: Record; -} - -export declare const Pagination: (props: PaginationProps) => JSX.Element; +export type { + PaginationLevel, + PaginationProps, +} from "./Pagination"; diff --git a/dist/remote-ui.js b/dist/remote-ui.js index 5f8b339..d272dc8 100644 --- a/dist/remote-ui.js +++ b/dist/remote-ui.js @@ -1,13 +1,120 @@ -import { jsx as E } from "react/jsx-runtime"; -import p, { useRef as s, useEffect as R } from "react"; +import B, { useRef as d, useEffect as g } from "react"; import { createRoot as k } from "react-dom/client"; -import { Text as U, Input as _, IconButton as I, HStack as v, Button as G, Badge as C, ChakraProvider as T } from "@chakra-ui/react"; +import { Text as v, Input as w, IconButton as E, HStack as y, Button as I, Badge as C, ChakraProvider as T } from "@chakra-ui/react"; +import { jsx as _ } from "react/jsx-runtime"; const h = { GBadge: "https://uikit.d.aiengines.ir/GBadge.js", Pagination: "https://uikit.d.aiengines.ir/Pagination.js" -}, l = "__UIKIT_REMOTE_DEPS__", g = "__UIKIT_REMOTE_CONFIG__", u = /* @__PURE__ */ new Map(), y = { - GBadge: ["React", "createRoot", "ChakraProvider", "Badge"], - Pagination: [ +}, s = "__UIKIT_REMOTE_DEPS__"; +function D(e = {}) { + if (typeof window > "u") return; + const t = window[s] || {}; + window[s] = { + ...t, + React: e.React || t.React || B, + createRoot: e.createRoot || t.createRoot || k, + ChakraProvider: e.ChakraProvider || t.ChakraProvider || T, + Badge: e.Badge || t.Badge || C, + Button: e.Button || t.Button || I, + HStack: e.HStack || t.HStack || y, + IconButton: e.IconButton || t.IconButton || E, + Input: e.Input || t.Input || w, + Text: e.Text || t.Text || v, + ...e.deps || {}, + theme: Object.prototype.hasOwnProperty.call(e, "theme") ? e.theme : t.theme, + chakraProviderProps: { + ...t.chakraProviderProps || {}, + ...e.chakraProviderProps || {} + } + }; +} +const a = /* @__PURE__ */ new Map(); +function x(e = []) { + if (typeof window > "u") return !1; + const t = window[s]; + return e.every((r) => t == null ? void 0 : t[r]); +} +function S(e = []) { + typeof window > "u" || x(e) || D(); +} +function G(e, t) { + const r = `${e}:${t}`; + return a.has(r) || a.set( + r, + import( + /* webpackIgnore: true */ + /* @vite-ignore */ + t + ).catch((u) => { + throw a.delete(r), u; + }) + ), a.get(r); +} +function P({ + componentName: e, + tagName: t, + defaultRemoteUrl: r, + requiredDeps: u, + normalizeProps: f +}) { + return function({ + theme: m, + chakraProviderProps: l, + ...p + }) { + const i = d(null), n = d(null), c = d(null); + return c.current = { + ...f ? f(p) : p, + ...m !== void 0 ? { theme: m } : {}, + ...l ? { chakraProviderProps: l } : {} + }, g(() => { + let R = !0; + return S(u), G(e, r).then(() => { + if (!R || !i.current) return; + const o = document.createElement(t); + o.componentProps = c.current, i.current.appendChild(o), n.current = o; + }).catch((o) => { + console.error( + `[uikit/remote/ui] Failed to load ${e}:`, + o + ); + }), () => { + R = !1, n.current && (n.current.remove(), n.current = null); + }; + }, [e, r, t]), g(() => { + n.current && (n.current.componentProps = c.current); + }), /* @__PURE__ */ _( + "span", + { + ref: i, + style: { + display: "contents" + } + } + ); + }; +} +function b(e) { + if (typeof e == "string" || typeof e == "number") + return e; + e != null && console.warn( + "[uikit/remote/ui] GBadge only accepts text/number children across the remote boundary." + ); +} +const q = P({ + componentName: "GBadge", + tagName: "uikit-g-badge", + defaultRemoteUrl: h.GBadge, + requiredDeps: ["React", "createRoot", "ChakraProvider", "Badge"], + normalizeProps: ({ children: e, ...t }) => ({ + ...t, + children: b(e) + }) +}), K = P({ + componentName: "Pagination", + tagName: "uikit-pagination", + defaultRemoteUrl: h.Pagination, + requiredDeps: [ "React", "createRoot", "ChakraProvider", @@ -17,122 +124,9 @@ const h = { "Input", "Text" ] -}; -function D(e = {}) { - if (typeof window > "u") return; - const t = window[l] || {}, r = window[g] || {}; - window[l] = { - ...t, - React: e.React || t.React || p, - createRoot: e.createRoot || t.createRoot || k, - ChakraProvider: e.ChakraProvider || t.ChakraProvider || T, - Badge: e.Badge || t.Badge || C, - Button: e.Button || t.Button || G, - HStack: e.HStack || t.HStack || v, - IconButton: e.IconButton || t.IconButton || I, - Input: e.Input || t.Input || _, - Text: e.Text || t.Text || U, - theme: Object.prototype.hasOwnProperty.call(e, "theme") ? e.theme : t.theme, - chakraProviderProps: { - ...t.chakraProviderProps || {}, - ...e.chakraProviderProps || {} - } - }; - const n = e.remoteUrls || {}; - window[g] = { - ...r, - ...e.remoteUrl ? { GBadgeUrl: e.remoteUrl } : {}, - ...e.GBadgeUrl ? { GBadgeUrl: e.GBadgeUrl } : {}, - ...e.PaginationUrl ? { PaginationUrl: e.PaginationUrl } : {}, - ...n.GBadge ? { GBadgeUrl: n.GBadge } : {}, - ...n.Pagination ? { PaginationUrl: n.Pagination } : {} - }; -} -function O(e) { - if (typeof window > "u") return; - const t = window[l]; - (y[e] || []).some((o) => !(t != null && t[o])) && D(); -} -function S(e, t) { - var r; - return t || (typeof window > "u" ? h[e] : ((r = window[g]) == null ? void 0 : r[`${e}Url`]) || h[e]); -} -function M(e, t) { - const r = S(e, t), n = `${e}:${r}`; - return u.has(n) || u.set( - n, - import( - /* webpackIgnore: true */ - /* @vite-ignore */ - r - ).catch((o) => { - throw u.delete(n), o; - }) - ), u.get(n); -} -function x(e) { - if (typeof e == "string" || typeof e == "number") - return e; - e != null && console.warn( - "[uikit/remote/ui] GBadge only accepts text/number children across the remote boundary." - ); -} -function w({ - componentName: e, - tagName: t, - normalizeProps: r -}) { - return function({ - remoteUrl: o, - theme: f, - chakraProviderProps: m, - ...B - }) { - const c = s(null), a = s(null), d = s(null); - return d.current = { - ...r ? r(B) : B, - ...f !== void 0 ? { theme: f } : {}, - ...m ? { chakraProviderProps: m } : {} - }, R(() => { - let P = !0; - return O(e), M(e, o).then(() => { - if (!P || !c.current) return; - const i = document.createElement(t); - i.componentProps = d.current, c.current.appendChild(i), a.current = i; - }).catch((i) => { - console.error( - `[uikit/remote/ui] Failed to load ${e}:`, - i - ); - }), () => { - P = !1, a.current && (a.current.remove(), a.current = null); - }; - }, [e, o, t]), R(() => { - a.current && (a.current.componentProps = d.current); - }), /* @__PURE__ */ E( - "span", - { - ref: c, - style: { - display: "contents" - } - } - ); - }; -} -const F = w({ - componentName: "GBadge", - tagName: "uikit-g-badge", - normalizeProps: ({ children: e, ...t }) => ({ - ...t, - children: x(e) - }) -}), $ = w({ - componentName: "Pagination", - tagName: "uikit-pagination" }); export { - F as GBadge, - $ as Pagination, + q as GBadge, + K as Pagination, D as setupUikitRemoteDeps }; diff --git a/src/core/BiProvider.jsx b/src/core/BiProvider.jsx index 925fb5d..f7656b2 100644 --- a/src/core/BiProvider.jsx +++ b/src/core/BiProvider.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useState } from "react"; +import { createRoot } from "react-dom/client"; import { Badge, ChakraProvider, Spinner } from "@chakra-ui/react"; import { QueryClientProvider } from "@tanstack/react-query"; import { KeycloakProvider } from "./keycloak.jsx"; @@ -9,17 +10,14 @@ import { queryClient } from "./query.js"; import { Fonts, theme as defaultTheme } from "./style.jsx"; const REMOTE_DEPS_KEY = "__UIKIT_REMOTE_DEPS__"; -const REMOTE_CONFIG_KEY = "__UIKIT_REMOTE_CONFIG__"; - -function setupBiRemoteUiDeps({ theme, remoteUiUrl, chakraProviderProps } = {}) { +function setupBiRemoteUiDeps({ theme, chakraProviderProps } = {}) { if (typeof window === "undefined") return; const previousDeps = window[REMOTE_DEPS_KEY] || {}; - const previousConfig = window[REMOTE_CONFIG_KEY] || {}; - window[REMOTE_DEPS_KEY] = { ...previousDeps, React: previousDeps.React || React, + createRoot: previousDeps.createRoot || createRoot, ChakraProvider: previousDeps.ChakraProvider || ChakraProvider, Badge: previousDeps.Badge || Badge, theme: theme ?? previousDeps.theme, @@ -28,11 +26,6 @@ function setupBiRemoteUiDeps({ theme, remoteUiUrl, chakraProviderProps } = {}) { ...(chakraProviderProps || {}), }, }; - - window[REMOTE_CONFIG_KEY] = { - ...previousConfig, - ...(remoteUiUrl ? { GBadgeUrl: remoteUiUrl } : {}), - }; } export function BiProvider({ @@ -57,7 +50,6 @@ export function BiProvider({ theme = defaultTheme, remoteUiEnabled = true, - remoteUiUrl, remoteUiChakraProviderProps, }) { const [mounted, setMounted] = useState(false); @@ -115,7 +107,6 @@ export function BiProvider({ if (remoteUiEnabled) { setupBiRemoteUiDeps({ theme, - remoteUiUrl, chakraProviderProps: remoteUiChakraProviderProps, }); } diff --git a/src/core/index.d.ts b/src/core/index.d.ts index 7e55e1d..f584a68 100644 --- a/src/core/index.d.ts +++ b/src/core/index.d.ts @@ -41,7 +41,6 @@ export type BiProviderProps = { permissionEnabled?: boolean; theme?: any; remoteUiEnabled?: boolean; - remoteUiUrl?: string; remoteUiChakraProviderProps?: Record; }; diff --git a/src/remote/ui/GBadge.remote.jsx b/src/remote/ui/GBadge/GBadge.remote.jsx similarity index 100% rename from src/remote/ui/GBadge.remote.jsx rename to src/remote/ui/GBadge/GBadge.remote.jsx diff --git a/src/remote/ui/GBadge/index.d.ts b/src/remote/ui/GBadge/index.d.ts new file mode 100644 index 0000000..cba7847 --- /dev/null +++ b/src/remote/ui/GBadge/index.d.ts @@ -0,0 +1,10 @@ +import type { BadgeProps } from "@chakra-ui/react"; +import type { ReactNode } from "react"; + +export interface GBadgeProps extends BadgeProps { + children?: ReactNode; + theme?: any; + chakraProviderProps?: Record; +} + +export declare const GBadge: (props: GBadgeProps) => JSX.Element; diff --git a/src/remote/ui/GBadge/index.jsx b/src/remote/ui/GBadge/index.jsx new file mode 100644 index 0000000..afb0623 --- /dev/null +++ b/src/remote/ui/GBadge/index.jsx @@ -0,0 +1,31 @@ +"use client"; + +import { DEFAULT_REMOTE_URLS } from "../_core/constants"; +import { createRemoteElementComponent } from "../_core/createRemoteElementComponent"; + +function normalizeChildren(children) { + if (typeof children === "string" || typeof children === "number") { + return children; + } + + if (children === null || children === undefined) { + return undefined; + } + + console.warn( + "[uikit/remote/ui] GBadge only accepts text/number children across the remote boundary.", + ); + + return undefined; +} + +export const GBadge = createRemoteElementComponent({ + componentName: "GBadge", + tagName: "uikit-g-badge", + defaultRemoteUrl: DEFAULT_REMOTE_URLS.GBadge, + requiredDeps: ["React", "createRoot", "ChakraProvider", "Badge"], + normalizeProps: ({ children, ...props }) => ({ + ...props, + children: normalizeChildren(children), + }), +}); diff --git a/src/remote/ui/Pagination.remote.jsx b/src/remote/ui/Pagination/Pagination.remote.jsx similarity index 100% rename from src/remote/ui/Pagination.remote.jsx rename to src/remote/ui/Pagination/Pagination.remote.jsx diff --git a/src/remote/ui/Pagination/index.d.ts b/src/remote/ui/Pagination/index.d.ts new file mode 100644 index 0000000..bf1ec56 --- /dev/null +++ b/src/remote/ui/Pagination/index.d.ts @@ -0,0 +1,16 @@ +import type { StackProps } from "@chakra-ui/react"; + +export type PaginationLevel = "full" | "compact" | "simple"; + +export interface PaginationProps extends Omit { + currentPage?: number; + totalCount?: number; + totalPages?: number; + pageSize?: number; + onPageChange?: (page: number) => void; + level?: PaginationLevel; + theme?: any; + chakraProviderProps?: Record; +} + +export declare const Pagination: (props: PaginationProps) => JSX.Element; diff --git a/src/remote/ui/Pagination/index.jsx b/src/remote/ui/Pagination/index.jsx new file mode 100644 index 0000000..fa300b7 --- /dev/null +++ b/src/remote/ui/Pagination/index.jsx @@ -0,0 +1,20 @@ +"use client"; + +import { DEFAULT_REMOTE_URLS } from "../_core/constants"; +import { createRemoteElementComponent } from "../_core/createRemoteElementComponent"; + +export const Pagination = createRemoteElementComponent({ + componentName: "Pagination", + tagName: "uikit-pagination", + defaultRemoteUrl: DEFAULT_REMOTE_URLS.Pagination, + requiredDeps: [ + "React", + "createRoot", + "ChakraProvider", + "Button", + "HStack", + "IconButton", + "Input", + "Text", + ], +}); diff --git a/src/remote/ui/_core/constants.js b/src/remote/ui/_core/constants.js new file mode 100644 index 0000000..f15896c --- /dev/null +++ b/src/remote/ui/_core/constants.js @@ -0,0 +1,6 @@ +export const DEFAULT_REMOTE_URLS = { + GBadge: "https://uikit.d.aiengines.ir/GBadge.js", + Pagination: "https://uikit.d.aiengines.ir/Pagination.js", +}; + +export const REMOTE_DEPS_KEY = "__UIKIT_REMOTE_DEPS__"; diff --git a/src/remote/ui/_core/createRemoteElementComponent.jsx b/src/remote/ui/_core/createRemoteElementComponent.jsx new file mode 100644 index 0000000..b557911 --- /dev/null +++ b/src/remote/ui/_core/createRemoteElementComponent.jsx @@ -0,0 +1,117 @@ +"use client"; + +import React, { useEffect, useRef } from "react"; + +import { REMOTE_DEPS_KEY } from "./constants"; +import { setupUikitRemoteDeps } from "./setupRemoteDeps"; + +const remotePromises = new Map(); + +function hasRequiredDeps(requiredDeps = []) { + if (typeof window === "undefined") return false; + + const deps = window[REMOTE_DEPS_KEY]; + + return requiredDeps.every((key) => deps?.[key]); +} + +function ensureRemoteDeps(requiredDeps = []) { + if (typeof window === "undefined") return; + + if (!hasRequiredDeps(requiredDeps)) { + setupUikitRemoteDeps(); + } +} + +function loadRemote(componentName, remoteFileUrl) { + const cacheKey = `${componentName}:${remoteFileUrl}`; + + if (!remotePromises.has(cacheKey)) { + remotePromises.set( + cacheKey, + import( + /* webpackIgnore: true */ + /* @vite-ignore */ + remoteFileUrl + ).catch((error) => { + remotePromises.delete(cacheKey); + throw error; + }), + ); + } + + return remotePromises.get(cacheKey); +} + +export function createRemoteElementComponent({ + componentName, + tagName, + defaultRemoteUrl, + requiredDeps, + normalizeProps, +}) { + return function RemoteElementComponent({ + theme, + chakraProviderProps, + ...props + }) { + const containerRef = useRef(null); + const elementRef = useRef(null); + const latestPropsRef = useRef(null); + + latestPropsRef.current = { + ...(normalizeProps ? normalizeProps(props) : props), + ...(theme !== undefined ? { theme } : {}), + ...(chakraProviderProps ? { chakraProviderProps } : {}), + }; + + useEffect(() => { + let active = true; + + ensureRemoteDeps(requiredDeps); + + loadRemote(componentName, defaultRemoteUrl) + .then(() => { + if (!active) return; + if (!containerRef.current) return; + + const element = document.createElement(tagName); + + element.componentProps = latestPropsRef.current; + + containerRef.current.appendChild(element); + elementRef.current = element; + }) + .catch((error) => { + console.error( + `[uikit/remote/ui] Failed to load ${componentName}:`, + error, + ); + }); + + return () => { + active = false; + + if (elementRef.current) { + elementRef.current.remove(); + elementRef.current = null; + } + }; + }, [componentName, defaultRemoteUrl, tagName]); + + useEffect(() => { + if (!elementRef.current) return; + + elementRef.current.componentProps = latestPropsRef.current; + }); + + return ( + + ); + }; +} diff --git a/src/remote/ui/_core/setupRemoteDeps.d.ts b/src/remote/ui/_core/setupRemoteDeps.d.ts new file mode 100644 index 0000000..378246e --- /dev/null +++ b/src/remote/ui/_core/setupRemoteDeps.d.ts @@ -0,0 +1,22 @@ +export interface UikitRemoteSetupOptions { + React?: any; + createRoot?: any; + + ChakraProvider?: any; + + Badge?: any; + Button?: any; + HStack?: any; + IconButton?: any; + Input?: any; + Text?: any; + + deps?: Record; + + theme?: any; + chakraProviderProps?: Record; +} + +export declare function setupUikitRemoteDeps( + options?: UikitRemoteSetupOptions, +): void; diff --git a/src/remote/ui/_core/setupRemoteDeps.js b/src/remote/ui/_core/setupRemoteDeps.js new file mode 100644 index 0000000..7cbff4d --- /dev/null +++ b/src/remote/ui/_core/setupRemoteDeps.js @@ -0,0 +1,49 @@ +"use client"; + +import React from "react"; +import { createRoot } from "react-dom/client"; +import { + Badge, + Button, + ChakraProvider, + HStack, + IconButton, + Input, + Text, +} from "@chakra-ui/react"; + +import { REMOTE_DEPS_KEY } from "./constants"; + +export function setupUikitRemoteDeps(options = {}) { + if (typeof window === "undefined") return; + + const previousDeps = window[REMOTE_DEPS_KEY] || {}; + + window[REMOTE_DEPS_KEY] = { + ...previousDeps, + + React: options.React || previousDeps.React || React, + createRoot: options.createRoot || previousDeps.createRoot || createRoot, + + ChakraProvider: + options.ChakraProvider || previousDeps.ChakraProvider || ChakraProvider, + + Badge: options.Badge || previousDeps.Badge || Badge, + Button: options.Button || previousDeps.Button || Button, + HStack: options.HStack || previousDeps.HStack || HStack, + IconButton: options.IconButton || previousDeps.IconButton || IconButton, + Input: options.Input || previousDeps.Input || Input, + Text: options.Text || previousDeps.Text || Text, + + ...(options.deps || {}), + + theme: Object.prototype.hasOwnProperty.call(options, "theme") + ? options.theme + : previousDeps.theme, + + chakraProviderProps: { + ...(previousDeps.chakraProviderProps || {}), + ...(options.chakraProviderProps || {}), + }, + }; +} diff --git a/src/remote/ui/index.d.ts b/src/remote/ui/index.d.ts index dc44fd1..9f1246e 100644 --- a/src/remote/ui/index.d.ts +++ b/src/remote/ui/index.d.ts @@ -1,54 +1,24 @@ -import type { BadgeProps, StackProps } from "@chakra-ui/react"; -import type { ReactNode } from "react"; +export { + setupUikitRemoteDeps, +} from "./_core/setupRemoteDeps"; -export type UikitRemoteLevel = "full" | "compact" | "simple"; +export type { + UikitRemoteSetupOptions, +} from "./_core/setupRemoteDeps"; -export interface UikitRemoteSetupOptions { - React?: any; - createRoot?: any; - ChakraProvider?: any; - Badge?: any; - Button?: any; - HStack?: any; - IconButton?: any; - Input?: any; - Text?: any; - theme?: any; - chakraProviderProps?: Record; +export { + GBadge, +} from "./GBadge"; - /** Backward compatible alias for GBadgeUrl. */ - remoteUrl?: string; - GBadgeUrl?: string; - PaginationUrl?: string; - remoteUrls?: { - GBadge?: string; - Pagination?: string; - }; -} +export type { + GBadgeProps, +} from "./GBadge"; -export declare function setupUikitRemoteDeps( - options?: UikitRemoteSetupOptions, -): void; +export { + Pagination, +} from "./Pagination"; -export interface GBadgeProps extends BadgeProps { - children?: ReactNode; - remoteUrl?: string; - theme?: any; - chakraProviderProps?: Record; -} - -export declare const GBadge: (props: GBadgeProps) => JSX.Element; - -export interface PaginationProps extends Omit { - currentPage?: number; - totalCount?: number; - totalPages?: number; - pageSize?: number; - onPageChange?: (page: number) => void; - level?: UikitRemoteLevel; - remoteUrl?: string; - theme?: any; - chakraProviderProps?: Record; -} - -export declare const Pagination: (props: PaginationProps) => JSX.Element; +export type { + PaginationLevel, + PaginationProps, +} from "./Pagination"; diff --git a/src/remote/ui/index.jsx b/src/remote/ui/index.jsx index 313e432..ed686d2 100644 --- a/src/remote/ui/index.jsx +++ b/src/remote/ui/index.jsx @@ -1,225 +1,6 @@ "use client"; -import React, { useEffect, useRef } from "react"; -import { createRoot } from "react-dom/client"; -import { - Badge, - Button, - ChakraProvider, - HStack, - IconButton, - Input, - Text, -} from "@chakra-ui/react"; +export { setupUikitRemoteDeps } from "./_core/setupRemoteDeps"; -const DEFAULT_REMOTE_URLS = { - GBadge: "https://uikit.d.aiengines.ir/GBadge.js", - Pagination: "https://uikit.d.aiengines.ir/Pagination.js", -}; - -const REMOTE_DEPS_KEY = "__UIKIT_REMOTE_DEPS__"; -const REMOTE_CONFIG_KEY = "__UIKIT_REMOTE_CONFIG__"; - -const remotePromises = new Map(); - -const REQUIRED_DEPS = { - GBadge: ["React", "createRoot", "ChakraProvider", "Badge"], - Pagination: [ - "React", - "createRoot", - "ChakraProvider", - "Button", - "HStack", - "IconButton", - "Input", - "Text", - ], -}; - -export function setupUikitRemoteDeps(options = {}) { - if (typeof window === "undefined") return; - - const previousDeps = window[REMOTE_DEPS_KEY] || {}; - const previousConfig = window[REMOTE_CONFIG_KEY] || {}; - - window[REMOTE_DEPS_KEY] = { - ...previousDeps, - React: options.React || previousDeps.React || React, - createRoot: options.createRoot || previousDeps.createRoot || createRoot, - ChakraProvider: - options.ChakraProvider || previousDeps.ChakraProvider || ChakraProvider, - Badge: options.Badge || previousDeps.Badge || Badge, - Button: options.Button || previousDeps.Button || Button, - HStack: options.HStack || previousDeps.HStack || HStack, - IconButton: options.IconButton || previousDeps.IconButton || IconButton, - Input: options.Input || previousDeps.Input || Input, - Text: options.Text || previousDeps.Text || Text, - theme: Object.prototype.hasOwnProperty.call(options, "theme") - ? options.theme - : previousDeps.theme, - chakraProviderProps: { - ...(previousDeps.chakraProviderProps || {}), - ...(options.chakraProviderProps || {}), - }, - }; - - const remoteUrls = options.remoteUrls || {}; - - window[REMOTE_CONFIG_KEY] = { - ...previousConfig, - ...(options.remoteUrl ? { GBadgeUrl: options.remoteUrl } : {}), - ...(options.GBadgeUrl ? { GBadgeUrl: options.GBadgeUrl } : {}), - ...(options.PaginationUrl ? { PaginationUrl: options.PaginationUrl } : {}), - ...(remoteUrls.GBadge ? { GBadgeUrl: remoteUrls.GBadge } : {}), - ...(remoteUrls.Pagination ? { PaginationUrl: remoteUrls.Pagination } : {}), - }; -} - -function ensureRemoteDeps(componentName) { - if (typeof window === "undefined") return; - - const deps = window[REMOTE_DEPS_KEY]; - const requiredDeps = REQUIRED_DEPS[componentName] || []; - const hasMissingDeps = requiredDeps.some((key) => !deps?.[key]); - - if (hasMissingDeps) { - setupUikitRemoteDeps(); - } -} - -function getRemoteUrl(componentName, explicitRemoteUrl) { - if (explicitRemoteUrl) return explicitRemoteUrl; - - if (typeof window === "undefined") { - return DEFAULT_REMOTE_URLS[componentName]; - } - - return ( - window[REMOTE_CONFIG_KEY]?.[`${componentName}Url`] || - DEFAULT_REMOTE_URLS[componentName] - ); -} - -function loadRemote(componentName, explicitRemoteUrl) { - const remoteUrl = getRemoteUrl(componentName, explicitRemoteUrl); - const cacheKey = `${componentName}:${remoteUrl}`; - - if (!remotePromises.has(cacheKey)) { - remotePromises.set( - cacheKey, - import( - /* webpackIgnore: true */ - /* @vite-ignore */ - remoteUrl - ).catch((error) => { - remotePromises.delete(cacheKey); - throw error; - }), - ); - } - - return remotePromises.get(cacheKey); -} - -function normalizeBadgeChildren(children) { - if (typeof children === "string" || typeof children === "number") { - return children; - } - - if (children === null || children === undefined) { - return undefined; - } - - console.warn( - "[uikit/remote/ui] GBadge only accepts text/number children across the remote boundary.", - ); - - return undefined; -} - -function createRemoteElementComponent({ - componentName, - tagName, - normalizeProps, -}) { - return function RemoteElementComponent({ - remoteUrl, - theme, - chakraProviderProps, - ...props - }) { - const containerRef = useRef(null); - const elementRef = useRef(null); - const latestPropsRef = useRef(null); - - latestPropsRef.current = { - ...(normalizeProps ? normalizeProps(props) : props), - ...(theme !== undefined ? { theme } : {}), - ...(chakraProviderProps ? { chakraProviderProps } : {}), - }; - - useEffect(() => { - let active = true; - - ensureRemoteDeps(componentName); - - loadRemote(componentName, remoteUrl) - .then(() => { - if (!active) return; - if (!containerRef.current) return; - - const element = document.createElement(tagName); - - element.componentProps = latestPropsRef.current; - - containerRef.current.appendChild(element); - - elementRef.current = element; - }) - .catch((error) => { - console.error( - `[uikit/remote/ui] Failed to load ${componentName}:`, - error, - ); - }); - - return () => { - active = false; - - if (elementRef.current) { - elementRef.current.remove(); - elementRef.current = null; - } - }; - }, [componentName, remoteUrl, tagName]); - - useEffect(() => { - if (!elementRef.current) return; - - elementRef.current.componentProps = latestPropsRef.current; - }); - - return ( - - ); - }; -} - -export const GBadge = createRemoteElementComponent({ - componentName: "GBadge", - tagName: "uikit-g-badge", - normalizeProps: ({ children, ...props }) => ({ - ...props, - children: normalizeBadgeChildren(children), - }), -}); - -export const Pagination = createRemoteElementComponent({ - componentName: "Pagination", - tagName: "uikit-pagination", -}); +export { GBadge } from "./GBadge"; +export { Pagination } from "./Pagination"; diff --git a/vite.config.js b/vite.config.js index eef959b..8d21ff7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -51,11 +51,11 @@ export default defineConfig(({ mode }) => { entry: { GBadge: path.resolve( process.cwd(), - "src/remote/ui/GBadge.remote.jsx", + "src/remote/ui/GBadge/GBadge.remote.jsx", ), Pagination: path.resolve( process.cwd(), - "src/remote/ui/Pagination.remote.jsx", + "src/remote/ui/Pagination/Pagination.remote.jsx", ), },