UNPKG

@zubridge/electron

Version:

A streamlined state management library for Electron applications using Zustand.

356 lines (350 loc) 13.9 kB
// Renderer-safe build with polyfilled Node.js modules const require_chunk = require('./chunk-CPpYY54z.cjs'); let zustand = require("zustand"); let zustand_vanilla = require("zustand/vanilla"); let __oxc_project_runtime_helpers_defineProperty = require("@oxc-project/runtime/helpers/defineProperty"); __oxc_project_runtime_helpers_defineProperty = require_chunk.__toESM(__oxc_project_runtime_helpers_defineProperty); //#region ../core/dist/utils/debug.js /** * Debug logging utility for Zubridge packages * Supports optional weald and @wdio/logger dependencies that are loaded dynamically * Falls back to console logging in sandbox or when dependencies are unavailable */ const isWDIOTest = () => { if (typeof process === "undefined") return false; return typeof globalThis.browser !== "undefined" || process.env.WDIO === "true" || !!process.env.WDIO_LOG_LEVEL; }; const isDebugEnabled = () => { if (typeof process !== "undefined" && process.env?.DEBUG) { const debugPattern = process.env.DEBUG; return debugPattern === "*" || debugPattern.includes("zubridge"); } if (typeof localStorage !== "undefined") try { const debug = localStorage.getItem("debug"); return debug === "*" || debug?.includes("zubridge") || false; } catch { return false; } return false; }; let wdioLogger = null; let wealdDebug = null; let loggersInitialized = false; /** * Attempt to load optional logging dependencies dynamically */ async function initializeOptionalLoggers() { if (loggersInitialized) return; loggersInitialized = true; if (isWDIOTest()) try { const wdioModule = await Promise.resolve().then(() => require("./build-BEosSXzI.cjs")); wdioLogger = (wdioModule.default || wdioModule)("zubridge"); } catch {} if (!isWDIOTest()) try { const wealdModule = await Promise.resolve().then(() => require("./src-BCHVPArp.cjs")); wealdDebug = wealdModule.default || wealdModule; } catch {} } initializeOptionalLoggers(); /** * Create a debug function for a specific namespace */ function createDebugger(namespace) { return (...args) => { if (!isDebugEnabled()) return; if (wdioLogger) { if (namespace.endsWith(":error")) wdioLogger.error(namespace, ...args); else if (namespace.endsWith(":warn")) wdioLogger.warn(namespace, ...args); else if (namespace.endsWith(":info")) wdioLogger.info(namespace, ...args); else wdioLogger.debug(namespace, ...args); return; } if (wealdDebug) { wealdDebug(namespace)(...args); return; } console.log(`[${namespace}]`, ...args); }; } const debuggers = { core: createDebugger("zubridge:core"), ipc: createDebugger("zubridge:ipc"), store: createDebugger("zubridge:store"), adapters: createDebugger("zubridge:adapters"), windows: createDebugger("zubridge:windows"), serialization: createDebugger("zubridge:serialization") }; const dynamicDebuggers = /* @__PURE__ */ new Map(); /** * Get or create a debugger for the specified area */ function getDebugger(area) { if (area in debuggers) return debuggers[area]; if (!dynamicDebuggers.has(area)) dynamicDebuggers.set(area, createDebugger(`zubridge:${area}`)); const debugFn = dynamicDebuggers.get(area); if (!debugFn) throw new Error(`Failed to create debugger for area: ${area}`); return debugFn; } /** * Log a debug message */ function debugLog(area, ...args) { getDebugger(area)(...args); } //#endregion //#region src/renderer/subscriptionValidator.ts let cachedSubscriptions = []; let lastSubscriptionFetchTime = 0; const SUBSCRIPTION_CACHE_TTL = 1e3; const getSubscriptionAPI = () => { if (typeof window !== "undefined" && window.__zubridge_subscriptionValidator) return window.__zubridge_subscriptionValidator; return null; }; /** * Gets the current window's subscriptions from the main process * @returns Array of state keys this window is subscribed to, or empty array if none */ async function getWindowSubscriptions() { try { const now = Date.now(); if (cachedSubscriptions.length > 0 && now - lastSubscriptionFetchTime < SUBSCRIPTION_CACHE_TTL) return cachedSubscriptions; const api = getSubscriptionAPI(); if (api) { const result = await api.getWindowSubscriptions(); cachedSubscriptions = Array.isArray(result) ? result : []; lastSubscriptionFetchTime = now; return cachedSubscriptions; } debugLog("subscription:error", "Subscription validator API not available"); return []; } catch (error) { debugLog("subscription:error", "Error getting window subscriptions:", error); return []; } } /** * Determines if the window is subscribed to a particular state key * @param key The state key to check * @returns True if subscribed, false otherwise */ async function isSubscribedToKey(key) { const api = getSubscriptionAPI(); if (api) return api.isSubscribedToKey(key); const subscriptions = await getWindowSubscriptions(); if (subscriptions.includes("*")) return true; if (subscriptions.includes(key)) return true; if (key.includes(".")) { const keyParts = key.split("."); for (let i = 1; i <= keyParts.length; i++) { const parentKey = keyParts.slice(0, i).join("."); if (subscriptions.includes(parentKey)) return true; } } for (const subscription of subscriptions) if (key.startsWith(`${subscription}.`)) return true; return false; } /** * Validates that the window has access to the specified state key * Throws an error if the window is not subscribed to the key * @param key The state key to validate * @param action Optional action to check for bypass flags * @throws Error if window is not subscribed to the key and not bypassing */ async function validateStateAccess(key, action) { if (!key) return; if (action && action.__bypassAccessControl === true) { debugLog("subscription", `Access control bypass set on action ${action.type}, bypassing subscription validation for key: ${key}`); return; } if (!await isSubscribedToKey(key)) { const subscriptions = await getWindowSubscriptions(); throw new Error(`Access denied: This window is not subscribed to state key '${key}'. Current subscriptions: ${subscriptions.join(", ") || "none"}`); } } /** * Checks if a state key exists in the provided state object * @param state The state object to check * @param key The key to look for (can use dot notation) * @returns True if the key exists, false otherwise */ function stateKeyExists(state, key) { const api = getSubscriptionAPI(); if (api) return api.stateKeyExists(state, key); if (!key || !state) return false; const parts = key.split("."); let current = state; for (const part of parts) { if (current === void 0 || current === null || typeof current !== "object") return false; if (!(part in current)) return false; current = current[part]; } return true; } /** * Validates state access with additional check for key existence * @param state The state object * @param key The key to validate * @param action Optional action to check for bypass flags * @throws Error if the key doesn't exist or the window isn't subscribed */ async function validateStateAccessWithExistence(state, key, action) { if (!stateKeyExists(state, key)) throw new Error(`State key '${key}' does not exist in the store`); await validateStateAccess(key, action); } //#endregion //#region src/renderer/actionValidator.ts const actionToStateKeyMap = /* @__PURE__ */ new Map(); /** * Register an action type with the state keys it affects * @param actionType The action type string * @param stateKeys Array of state keys this action affects */ function registerActionMapping(actionType, stateKeys) { actionToStateKeyMap.set(actionType, stateKeys); debugLog("action-validator", `Registered action mapping: ${actionType} -> ${stateKeys.join(", ")}`); } /** * Register multiple action mappings at once * @param mappings Object mapping action types to arrays of state keys */ function registerActionMappings(mappings) { Object.entries(mappings).forEach(([actionType, stateKeys]) => { registerActionMapping(actionType, stateKeys); }); const mappingCount = Object.keys(mappings).length; debugLog("action-validator", `Registered ${mappingCount} action mappings`); } /** * Get the state keys affected by an action * @param actionType The action type string * @returns Array of state keys this action affects, or empty array if unknown */ function getAffectedStateKeys(actionType) { return actionToStateKeyMap.get(actionType) || []; } /** * Check if a window can dispatch an action based on its subscriptions * @param action The action to validate * @returns Promise resolving to boolean indicating if dispatch is allowed */ async function canDispatchAction(action) { if (action.__bypassAccessControl === true) { debugLog("action-validator", `Access control bypass set on action ${action.type}, allowing dispatch`); return true; } const actionType = action.type; const affectedKeys = getAffectedStateKeys(actionType); if (affectedKeys.length === 0) { debugLog("action-validator", `No mapping for action ${actionType}, allowing by default`); return true; } if ((await getWindowSubscriptions()).includes("*")) return true; const currentState = typeof window !== "undefined" && window.zubridge ? await window.zubridge.getState() : null; for (const key of affectedKeys) { if (!currentState || !stateKeyExists(currentState, key)) { debugLog("action-validator", `State key ${key} does not exist in the store`); return false; } if (!await isSubscribedToKey(key)) { debugLog("action-validator", `Window lacks permission to affect key ${key} with action ${actionType}`); return false; } } return true; } /** * Validate that a window can dispatch an action, throwing if not allowed * @param action The action to validate * @throws Error if the window cannot dispatch this action */ async function validateActionDispatch(action) { if (action.__bypassAccessControl === true) { debugLog("action-validator", `Access control bypass set on action ${action.type}, bypassing action dispatch validation`); return; } const actionType = action.type; const affectedKeys = getAffectedStateKeys(actionType); if (affectedKeys.length === 0) { debugLog("action-validator", `No mapping for action ${actionType}, allowing by default`); return; } const currentState = typeof window !== "undefined" && window.zubridge ? await window.zubridge.getState() : null; for (const key of affectedKeys) if (!currentState || !stateKeyExists(currentState, key)) throw new Error(`State key '${key}' does not exist in the store`); if (!await canDispatchAction(action)) { const subscriptions = await getWindowSubscriptions(); throw new Error(`Unauthorized action dispatch: This window cannot dispatch action '${action.type}' which affects state keys: ${affectedKeys.join(", ")}. Current subscriptions: ${subscriptions.join(", ") || "none"}`); } } //#endregion //#region src/utils/environment.ts /** * Determines if the application is running in development mode * * Uses a combination of checks to ensure consistent behavior: * 1. Checks if app is packaged (production builds are packaged) * 2. Checks NODE_ENV environment variable * 3. Checks ELECTRON_IS_DEV environment variable (set by electron-is-dev or similar utilities) * * @returns {boolean} True if running in development mode, false otherwise */ const isDev = async () => { if (process.type !== "browser") { if (process.env.NODE_ENV === "production" || process.env.ELECTRON_IS_DEV === "0") return false; return process.env.NODE_ENV === "development" || process.env.ELECTRON_IS_DEV === "1" || !process.env.VITE_DEV_SERVER_URL; } const { app } = await import("electron"); return !app.isPackaged || process.env.NODE_ENV === "development" || process.env.ELECTRON_IS_DEV === "1"; }; //#endregion //#region src/index.ts const storeRegistry = /* @__PURE__ */ new WeakMap(); const createStore = (bridge) => { if (storeRegistry.has(bridge)) return storeRegistry.get(bridge); const newStore = (0, zustand_vanilla.createStore)((setState) => { bridge.subscribe((state) => setState(state, true)); bridge.getState().then((state) => setState(state, true)); return {}; }); storeRegistry.set(bridge, newStore); return newStore; }; const createHandlers = () => { if (typeof window === "undefined" || !window.zubridge) throw new Error("Zubridge handlers not found in window. Make sure the preload script is properly set up."); return window.zubridge; }; /** * Creates a hook for accessing the store state in renderer components */ const createUseStore = (customHandlers) => { const vanillaStore = createStore(customHandlers || createHandlers()); const useBoundStore = (selector) => (0, zustand.useStore)(vanillaStore, selector); Object.assign(useBoundStore, vanillaStore); return useBoundStore; }; /** * Creates a dispatch function for use in renderer components */ function useDispatch(customHandlers) { const handlers = customHandlers || createHandlers(); const dispatch = (action, payloadOrOptions, optionsIfString) => { if (typeof action === "string") return handlers.dispatch(action, payloadOrOptions, optionsIfString); if (typeof action === "function") return handlers.dispatch(action, payloadOrOptions); return handlers.dispatch(action, payloadOrOptions); }; return dispatch; } //#endregion exports.canDispatchAction = canDispatchAction; exports.createHandlers = createHandlers; exports.createUseStore = createUseStore; exports.getAffectedStateKeys = getAffectedStateKeys; exports.getWindowSubscriptions = getWindowSubscriptions; exports.isDev = isDev; exports.isSubscribedToKey = isSubscribedToKey; exports.registerActionMapping = registerActionMapping; exports.registerActionMappings = registerActionMappings; exports.stateKeyExists = stateKeyExists; exports.useDispatch = useDispatch; exports.validateActionDispatch = validateActionDispatch; exports.validateStateAccess = validateStateAccess; exports.validateStateAccessWithExistence = validateStateAccessWithExistence;