@zubridge/electron
Version:
A streamlined state management library for Electron applications using Zustand.
356 lines (350 loc) • 13.9 kB
JavaScript
// 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;