From 8b43493d870903bd6285ab464ef51289a092dda5 Mon Sep 17 00:00:00 2001 From: Nystik <236107-Nystik@users.noreply.gitlab.com> Date: Sat, 7 Mar 2026 12:23:08 +0100 Subject: [PATCH] add shim, basic stubs --- shims/electron/index.js | 33 +++++++ shims/electron/ipc-renderer.js | 107 +++++++++++++++++++++ shims/electron/web-frame.js | 27 ++++++ shims/loader.js | 171 +++++++++++++++++++++++++++++++++ shims/process.js | 22 +++++ 5 files changed, 360 insertions(+) create mode 100644 shims/electron/index.js create mode 100644 shims/electron/ipc-renderer.js create mode 100644 shims/electron/web-frame.js create mode 100644 shims/loader.js create mode 100644 shims/process.js diff --git a/shims/electron/index.js b/shims/electron/index.js new file mode 100644 index 0000000..feda1d8 --- /dev/null +++ b/shims/electron/index.js @@ -0,0 +1,33 @@ +// Electron module shim +// Returned when Obsidian calls: window.require('electron') + +import { ipcRenderer } from "./ipc-renderer.js"; +import { webFrame } from "./web-frame.js"; +import { remoteShim } from "./remote/index.js"; + +export const electronShim = { + ipcRenderer, + webFrame, + remote: remoteShim, + + // electron.deprecate - used by Obsidian to mark deprecated APIs + deprecate: { + function(fn, name) { + return fn; + }, + event(emitter, name) {}, + removeFunction(fn, name) { + return fn; + }, + log(message) { + console.log("[electron:deprecate]", message); + }, + warn(oldName, newName) {}, + promisify(fn) { + return fn; + }, + renameFunction(fn, newName) { + return fn; + }, + }, +}; diff --git a/shims/electron/ipc-renderer.js b/shims/electron/ipc-renderer.js new file mode 100644 index 0000000..ee6e095 --- /dev/null +++ b/shims/electron/ipc-renderer.js @@ -0,0 +1,107 @@ +// Shim for electron.ipcRenderer +// Obsidian uses: .send(), .sendSync(), .on(), .once() +// +// sendSync channels discovered in app.js: +// vault → {id, path} - critical for startup +// version → string - app version +// is-dev → boolean - dev mode flag +// file-url → string - base URL prefix for vault assets +// disable-update → boolean - whether updates are disabled +// update → string - update status +// disable-gpu → boolean - GPU acceleration toggle +// frame → void - window frame style +// set-icon → void - custom vault icon +// get-icon → null|object - get custom vault icon +// relaunch → void - restart app +// starter → void - open vault chooser +// help → void - open help +// sandbox → void - open sandbox vault +// copy-asar → boolean - install update + +const listeners = new Map(); + +// Sync channel handlers - must return values synchronously +const syncHandlers = { + vault: () => window.__vaultConfig || { id: "default-vault", path: "/" }, + version: () => "1.8.9", + "is-dev": () => false, + "file-url": () => "", + "disable-update": () => true, + update: () => "", + "disable-gpu": () => false, + frame: () => null, + "set-icon": () => null, + "get-icon": () => null, + relaunch: () => { + window.location.reload(); + return null; + }, + starter: () => null, + help: () => { + window.open("https://help.obsidian.md/", "_blank"); + return null; + }, + sandbox: () => null, + "copy-asar": () => false, + "check-update": () => null, +}; + +export const ipcRenderer = { + send(channel, ...args) { + console.log("[shim:ipcRenderer] send:", channel, args); + // TODO: route to server via chosen sync mechanism if needed + }, + + sendSync(channel, ...args) { + console.log("[shim:ipcRenderer] sendSync:", channel, args); + if (syncHandlers[channel]) { + return syncHandlers[channel](...args); + } + console.warn("[shim:ipcRenderer] Unhandled sendSync channel:", channel); + return null; + }, + + on(channel, listener) { + if (!listeners.has(channel)) { + listeners.set(channel, []); + } + listeners.get(channel).push(listener); + return ipcRenderer; + }, + + once(channel, listener) { + const wrapped = (...args) => { + ipcRenderer.removeListener(channel, wrapped); + listener(...args); + }; + return ipcRenderer.on(channel, wrapped); + }, + + removeListener(channel, listener) { + const arr = listeners.get(channel); + if (arr) { + const idx = arr.indexOf(listener); + if (idx >= 0) arr.splice(idx, 1); + } + return ipcRenderer; + }, + + removeAllListeners(channel) { + if (channel) { + listeners.delete(channel); + } else { + listeners.clear(); + } + return ipcRenderer; + }, + + // Internal: emit an event to registered listeners (used by ws bridge) + _emit(channel, ...args) { + const arr = listeners.get(channel); + if (arr) { + for (const fn of arr) { + fn({}, ...args); + } + } + }, +}; diff --git a/shims/electron/web-frame.js b/shims/electron/web-frame.js new file mode 100644 index 0000000..0767ea7 --- /dev/null +++ b/shims/electron/web-frame.js @@ -0,0 +1,27 @@ +// Shim for electron.webFrame +// Obsidian uses: getZoomLevel(), setZoomLevel() + +let currentZoom = 0; + +export const webFrame = { + getZoomLevel() { + return currentZoom; + }, + + setZoomLevel(level) { + currentZoom = level; + // Approximate Electron's zoom behavior via CSS zoom + // Electron zoom level 0 = 100%, each step is ~20% + const scale = Math.pow(1.2, level); + document.body.style.zoom = scale; + }, + + getZoomFactor() { + return Math.pow(1.2, currentZoom); + }, + + setZoomFactor(factor) { + currentZoom = Math.log(factor) / Math.log(1.2); + document.body.style.zoom = factor; + }, +}; diff --git a/shims/loader.js b/shims/loader.js new file mode 100644 index 0000000..879bced --- /dev/null +++ b/shims/loader.js @@ -0,0 +1,171 @@ +// shim-loader.js +// Loaded before app.js. Defines window.require() and window.process +// to intercept all Electron/Node API calls from Obsidian's renderer code. + +import { electronShim } from "./electron/index.js"; +import { remoteShim } from "./electron/remote/index.js"; +import { fsShim } from "./fs/index.js"; +import { pathShim } from "./path.js"; +import { urlShim } from "./url.js"; +import { cryptoShim } from "./crypto/index.js"; +import { btimeShim } from "./btime.js"; +import { processShim } from "./process.js"; + +// Debug mode: wrap shims in Proxy to log all property accesses +const DEBUG = true; +const _accessLog = new Map(); // "module.property" -> count + +function wrapWithProxy(obj, name) { + if (!DEBUG || !obj || typeof obj !== "object") return obj; + return new Proxy(obj, { + get(target, prop) { + if ( + typeof prop === "string" && + prop !== "then" && + prop !== "toJSON" && + !prop.startsWith("_") + ) { + const key = `${name}.${prop}`; + _accessLog.set(key, (_accessLog.get(key) || 0) + 1); + if (!(prop in target)) { + console.warn(`[shim:MISS] ${key} - property not found on shim`); + } + } + return target[prop]; + }, + }); +} + +// Expose access log for debugging in console: window.__shimLog() +window.__shimLog = function () { + const sorted = [..._accessLog.entries()].sort((a, b) => b[1] - a[1]); + console.table(sorted.map(([k, v]) => ({ api: k, calls: v }))); +}; +window.__shimMisses = function () { + const sorted = [..._accessLog.entries()] + .filter(([k]) => { + const [mod, prop] = k.split("."); + const shim = rawRegistry[mod]; + return shim && !(prop in shim); + }) + .sort((a, b) => b[1] - a[1]); + console.table(sorted.map(([k, v]) => ({ api: k, calls: v }))); +}; + +const rawRegistry = { + electron: electronShim, + "@electron/remote": remoteShim, + "original-fs": fsShim, + fs: fsShim, + path: pathShim, + url: urlShim, + crypto: cryptoShim, + btime: btimeShim, +}; + +const shimRegistry = {}; +for (const [name, shim] of Object.entries(rawRegistry)) { + shimRegistry[name] = wrapWithProxy(shim, name); +} + +// Modules that should throw on require (native modules that don't exist in browser) +const throwOnRequire = new Set(["btime", "get-fonts", "vibrancy-win"]); + +window.require = function (moduleName) { + if (throwOnRequire.has(moduleName)) { + throw new Error(`Cannot find module '${moduleName}'`); + } + if (shimRegistry[moduleName]) { + return shimRegistry[moduleName]; + } + console.warn("[obsidian-bridge] Unshimmed require:", moduleName); + return wrapWithProxy({}, `UNKNOWN(${moduleName})`); +}; + +window.process = processShim; + +// Provide a global Buffer if needed +if (typeof window.Buffer === "undefined") { + // TODO: evaluate if a full Buffer polyfill is needed or if Uint8Array suffices + window.Buffer = { + from: function (data, encoding) { + if (typeof data === "string") { + return new TextEncoder().encode(data); + } + if (data instanceof ArrayBuffer) { + return new Uint8Array(data); + } + return new Uint8Array(data); + }, + concat: function (arrays) { + const total = arrays.reduce((sum, a) => sum + a.length, 0); + const result = new Uint8Array(total); + let offset = 0; + for (const arr of arrays) { + result.set(arr, offset); + offset += arr.length; + } + return result; + }, + isBuffer: function (obj) { + return obj instanceof Uint8Array; + }, + }; +} + +// Prevent app.js from closing the window (browser blocks this anyway, but suppress the error) +const _origClose = window.close; +window.close = function () { + console.log("[obsidian-bridge] window.close() blocked"); +}; + +// Pre-populate fs metadata cache synchronously before app.js runs. +// This ensures existsSync() works for the vault path during startup. +(function initMetadataCache() { + try { + const xhr = new XMLHttpRequest(); + xhr.open("GET", "/api/fs/tree", false); // synchronous + xhr.send(); + if (xhr.status === 200) { + const tree = JSON.parse(xhr.responseText); + fsShim._metadataCache.populate(tree); + // Also add the root path itself + fsShim._metadataCache.set("", { type: "directory" }); + fsShim._metadataCache.set("/", { type: "directory" }); + console.log( + "[obsidian-bridge] Metadata cache populated:", + fsShim._metadataCache.size, + "entries", + ); + } else { + console.error( + "[obsidian-bridge] Failed to fetch metadata tree:", + xhr.status, + ); + } + } catch (e) { + console.error("[obsidian-bridge] Failed to init metadata cache:", e); + } +})(); + +// Fetch vault config from server synchronously +(function initVaultConfig() { + try { + const xhr = new XMLHttpRequest(); + xhr.open("GET", "/api/vault/info", false); // synchronous + xhr.send(); + if (xhr.status === 200) { + const info = JSON.parse(xhr.responseText); + // Set the vault config that sendSync('vault') will return + window.__vaultConfig = { + id: info.name || "default-vault", + path: "/", + }; + console.log("[obsidian-bridge] Vault config:", window.__vaultConfig); + } + } catch (e) { + console.error("[obsidian-bridge] Failed to fetch vault config:", e); + } +})(); + +console.log("[obsidian-bridge] Shim loader initialized"); diff --git a/shims/process.js b/shims/process.js new file mode 100644 index 0000000..e2d1e9c --- /dev/null +++ b/shims/process.js @@ -0,0 +1,22 @@ +// Shim for window.process +// Obsidian checks process.platform, process.versions.electron, etc. + +export const processShim = { + platform: 'linux', + versions: { + electron: '28.0.0', + node: '18.18.0', + chrome: '120.0.0.0', + }, + env: {}, + cwd: () => '/', + nextTick: (fn, ...args) => setTimeout(() => fn(...args), 0), + argv: [], + type: 'renderer', + resourcesPath: '/', + stdout: { write: (s) => console.log(s) }, + stderr: { write: (s) => console.error(s) }, + on: () => {}, + once: () => {}, + removeListener: () => {}, +};