diff --git a/src/shims/debug.js b/src/shims/debug.js new file mode 100644 index 0000000..a3192e0 --- /dev/null +++ b/src/shims/debug.js @@ -0,0 +1,47 @@ +const DEBUG = true; +const _accessLog = new Map(); // "module.property" -> count + +export 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]; + }, + }); +} + +export function installDebugHelpers(rawRegistry) { + 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 }))); + }; +} diff --git a/src/shims/globals.js b/src/shims/globals.js new file mode 100644 index 0000000..f3c4c4e --- /dev/null +++ b/src/shims/globals.js @@ -0,0 +1,110 @@ +import { processShim } from "./process.js"; +import { + registerPopupWindow, + unregisterPopupWindow, +} from "./electron/remote/window.js"; +import { showVaultManager } from "../ui/bootstrap.js"; + +function installProcess() { + window.process = processShim; +} + +function installBuffer() { + if (typeof window.Buffer !== "undefined") return; + + 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; + }, + }; +} + +function installWindowClose() { + window.close = function () { + console.log("[ignis] window.close() blocked"); + if (!window.__vaultConfig) { + showVaultManager(); + } + }; +} + +function installWindowOpen() { + window.__popupIframe = null; + const _originalOpen = window.open; + + window.open = function (url, target, features) { + if (url === "about:blank" || (features && features.includes("popup"))) { + console.log("[ignis] intercepted popup:", url, features); + + registerPopupWindow(); + + const iframe = document.createElement("iframe"); + iframe.style.cssText = + "position:fixed;left:-9999px;width:0;height:0;border:none;"; + + document.body.appendChild(iframe); + window.__popupIframe = iframe; + + const iframeWin = iframe.contentWindow; + + iframeWin.require = window.require; + iframeWin.module = window.module; + iframeWin.Buffer = window.Buffer; + iframeWin.process = window.process; + iframeWin.global = iframeWin; + iframeWin.globalEnhance = window.globalEnhance; + + iframeWin.close = function () { + unregisterPopupWindow(); + iframe.remove(); + window.__popupIframe = null; + }; + + return iframeWin; + } + return _originalOpen.call(window, url, target, features); + }; +} + +function installContextMenuFix() { + // hacky fix to prevent browser from showing context menu while allowing obsidian context menu + window.addEventListener( + "contextmenu", + (e) => { + e.preventDefault(); + Object.defineProperty(e, "defaultPrevented", { get: () => false }); + }, + true, + ); +} + +export function installGlobals() { + installProcess(); + installBuffer(); + installWindowClose(); + installWindowOpen(); + installContextMenuFix(); +} diff --git a/src/shims/init.js b/src/shims/init.js new file mode 100644 index 0000000..cca9422 --- /dev/null +++ b/src/shims/init.js @@ -0,0 +1,117 @@ +import { fsShim } from "./fs/index.js"; +import { installRequestUrlShim } from "./request-url.js"; +import { vaultService } from "../services/vault-service.js"; +import { showPluginInstallDialog } from "../ui/bootstrap.js"; + +function resolveVaultId() { + const urlParams = new URLSearchParams(window.location.search); + window.__currentVaultId = + urlParams.get("vault") || localStorage.getItem("last-vault") || ""; +} + +function initVaultConfig() { + try { + const vaultParam = window.__currentVaultId + ? "?vault=" + encodeURIComponent(window.__currentVaultId) + : ""; + + const xhr = new XMLHttpRequest(); + + xhr.open("GET", "/api/vault/info" + vaultParam, false); + xhr.send(); + + if (xhr.status === 200) { + const info = JSON.parse(xhr.responseText); + + window.__currentVaultId = info.id; + localStorage.setItem("last-vault", info.id); + window.__obsidianVersion = info.version || "0.0.0"; + + window.__vaultConfig = { + id: info.id, + path: "/", + }; + + window.__ignisPlugin = info.ignisPlugin || null; + + console.log("[ignis] Vault:", window.__vaultConfig); + console.log("[ignis] Obsidian version:", window.__obsidianVersion); + } else { + console.warn("[ignis] No vault found, will show manager"); + } + } catch (e) { + console.error("[ignis] Failed to fetch vault config:", e); + } +} + +function initVaultList() { + try { + vaultService.listVaultsSync(); + } catch (e) { + window.__vaultList = []; + } +} + +function initMetadataCache() { + try { + const vaultParam = window.__currentVaultId + ? "?vault=" + encodeURIComponent(window.__currentVaultId) + : ""; + + const xhr = new XMLHttpRequest(); + + xhr.open("GET", "/api/fs/tree" + vaultParam, false); + xhr.send(); + + if (xhr.status === 200) { + const tree = JSON.parse(xhr.responseText); + + fsShim._metadataCache.populate(tree); + fsShim._metadataCache.set("", { type: "directory" }); + fsShim._metadataCache.set("/", { type: "directory" }); + + console.log( + "[ignis] Metadata cache populated:", + fsShim._metadataCache.size, + "entries", + ); + } else { + console.error("[ignis] Failed to fetch metadata tree:", xhr.status); + } + } catch (e) { + console.error("[ignis] Failed to init metadata cache:", e); + } +} + +function initPluginPrompt() { + if ( + !window.__ignisPlugin || + window.__ignisPlugin.installed || + window.__ignisPlugin.prompted + ) { + return; + } + + const vaultId = window.__currentVaultId; + + const observer = new MutationObserver(() => { + if (document.querySelector(".workspace")) { + observer.disconnect(); + showPluginInstallDialog(vaultId); + } + }); + + observer.observe(document.documentElement, { + childList: true, + subtree: true, + }); +} + +export function initialize() { + resolveVaultId(); + initVaultConfig(); + initVaultList(); + initMetadataCache(); + installRequestUrlShim(); + initPluginPrompt(); +} diff --git a/src/shims/loader.js b/src/shims/loader.js index cc52bef..c94d7ac 100644 --- a/src/shims/loader.js +++ b/src/shims/loader.js @@ -1,294 +1,9 @@ -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 { processShim } from "./process.js"; -import { installRequestUrlShim } from "./request-url.js"; -import { - registerPopupWindow, - unregisterPopupWindow, -} from "./electron/remote/window.js"; -import * as childProcessShim from "./node/child_process.js"; -import * as eventsShim from "./node/events.js"; -import * as osShim from "./node/os.js"; -import * as netShim from "./node/net.js"; -import * as httpShim from "./node/http.js"; -import { vaultService } from "../services/vault-service.js"; -import { showPluginInstallDialog, showVaultManager } from "../ui/bootstrap.js"; +import { installRequire } from "./require.js"; +import { installGlobals } from "./globals.js"; +import { initialize } from "./init.js"; -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]; - }, - }); -} - -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, - child_process: childProcessShim, - events: eventsShim, - os: osShim, - net: netShim, - http: httpShim, - https: httpShim, -}; - -const shimRegistry = {}; -for (const [name, shim] of Object.entries(rawRegistry)) { - shimRegistry[name] = wrapWithProxy(shim, name); -} - -const throwOnRequire = new Set(["btime", "get-fonts", "vibrancy-win"]); - -window.require = function (moduleName) { - // Strip node: prefix if present - const normalizedName = moduleName.startsWith("node:") - ? moduleName.slice(5) - : moduleName; - - if (throwOnRequire.has(normalizedName)) { - throw new Error(`Cannot find module '${moduleName}'`); - } - - if (shimRegistry[normalizedName]) { - return shimRegistry[normalizedName]; - } - - console.warn("[ignis] Unshimmed require:", moduleName); - return wrapWithProxy({}, `UNKNOWN(${moduleName})`); -}; - -window.process = processShim; - -if (typeof window.Buffer === "undefined") { - 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; - }, - }; -} - -window.close = function () { - console.log("[ignis] window.close() blocked"); - if (!window.__vaultConfig) { - showVaultManager(); - } -}; - -window.__popupIframe = null; -const _originalOpen = window.open; -window.open = function (url, target, features) { - if (url === "about:blank" || (features && features.includes("popup"))) { - console.log("[ignis] intercepted popup:", url, features); - - registerPopupWindow(); - - const iframe = document.createElement("iframe"); - iframe.style.cssText = - "position:fixed;left:-9999px;width:0;height:0;border:none;"; - - document.body.appendChild(iframe); - window.__popupIframe = iframe; - - const iframeWin = iframe.contentWindow; - - iframeWin.require = window.require; - iframeWin.module = window.module; - iframeWin.Buffer = window.Buffer; - iframeWin.process = window.process; - iframeWin.global = iframeWin; - iframeWin.globalEnhance = window.globalEnhance; - - iframeWin.close = function () { - unregisterPopupWindow(); - iframe.remove(); - window.__popupIframe = null; - }; - - return iframeWin; - } - return _originalOpen.call(window, url, target, features); -}; - -// hacky fix to prevent browser from showing context menu while allowing obsidian context menu -window.addEventListener( - "contextmenu", - (e) => { - e.preventDefault(); - Object.defineProperty(e, "defaultPrevented", { get: () => false }); - }, - true, -); - -const _urlParams = new URLSearchParams(window.location.search); -window.__currentVaultId = - _urlParams.get("vault") || localStorage.getItem("last-vault") || ""; - -(function initVaultConfig() { - try { - const vaultParam = window.__currentVaultId - ? "?vault=" + encodeURIComponent(window.__currentVaultId) - : ""; - - const xhr = new XMLHttpRequest(); - - xhr.open("GET", "/api/vault/info" + vaultParam, false); - xhr.send(); - - if (xhr.status === 200) { - const info = JSON.parse(xhr.responseText); - - window.__currentVaultId = info.id; - localStorage.setItem("last-vault", info.id); - window.__obsidianVersion = info.version || "0.0.0"; - - window.__vaultConfig = { - id: info.id, - path: "/", - }; - - window.__ignisPlugin = info.ignisPlugin || null; - - console.log("[ignis] Vault:", window.__vaultConfig); - console.log("[ignis] Obsidian version:", window.__obsidianVersion); - } else { - console.warn("[ignis] No vault found, will show manager"); - } - } catch (e) { - console.error("[ignis] Failed to fetch vault config:", e); - } -})(); - -(function initVaultList() { - try { - vaultService.listVaultsSync(); - } catch (e) { - window.__vaultList = []; - } -})(); - -(function initMetadataCache() { - try { - const vaultParam = window.__currentVaultId - ? "?vault=" + encodeURIComponent(window.__currentVaultId) - : ""; - - const xhr = new XMLHttpRequest(); - - xhr.open("GET", "/api/fs/tree" + vaultParam, false); - xhr.send(); - - if (xhr.status === 200) { - const tree = JSON.parse(xhr.responseText); - - fsShim._metadataCache.populate(tree); - fsShim._metadataCache.set("", { type: "directory" }); - fsShim._metadataCache.set("/", { type: "directory" }); - - console.log( - "[ignis] Metadata cache populated:", - fsShim._metadataCache.size, - "entries", - ); - } else { - console.error("[ignis] Failed to fetch metadata tree:", xhr.status); - } - } catch (e) { - console.error("[ignis] Failed to init metadata cache:", e); - } -})(); - -installRequestUrlShim(); - -// Check if plugin install prompt is needed (once per session, after workspace loads) -if ( - window.__ignisPlugin && - !window.__ignisPlugin.installed && - !window.__ignisPlugin.prompted -) { - const vaultId = window.__currentVaultId; - - const observer = new MutationObserver(() => { - if (document.querySelector(".workspace")) { - observer.disconnect(); - showPluginInstallDialog(vaultId); - } - }); - - observer.observe(document.documentElement, { - childList: true, - subtree: true, - }); -} +installGlobals(); // process, Buffer, window overrides (before require so Buffer is available) +installRequire(); // shim registry, window.require +initialize(); // vault config, metadata cache, plugin prompt console.log("[ignis] Shim loader initialized"); diff --git a/src/shims/require.js b/src/shims/require.js new file mode 100644 index 0000000..e414b87 --- /dev/null +++ b/src/shims/require.js @@ -0,0 +1,65 @@ +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 * as childProcessShim from "./node/child_process.js"; +import * as eventsShim from "./node/events.js"; +import * as osShim from "./node/os.js"; +import * as netShim from "./node/net.js"; +import * as httpShim from "./node/http.js"; +import { wrapWithProxy, installDebugHelpers } from "./debug.js"; + +const rawRegistry = { + electron: electronShim, + "@electron/remote": remoteShim, + "original-fs": fsShim, + fs: fsShim, + path: pathShim, + url: urlShim, + crypto: cryptoShim, + child_process: childProcessShim, + events: eventsShim, + os: osShim, + net: netShim, + http: httpShim, + https: httpShim, +}; + +const shimRegistry = {}; +const throwOnRequire = new Set(["btime", "get-fonts", "vibrancy-win"]); + +export function installRequire() { + for (const [name, shim] of Object.entries(rawRegistry)) { + shimRegistry[name] = wrapWithProxy(shim, name); + } + + // Add buffer shim (protobufjs inquire() checks for this) + if (typeof window.Buffer !== "undefined") { + shimRegistry.buffer = window.Buffer; + } + + // Add empty long shim (optional protobufjs dependency, gracefully handled) + shimRegistry.long = undefined; + + window.require = function (moduleName) { + // Strip node: prefix if present + const normalizedName = moduleName.startsWith("node:") + ? moduleName.slice(5) + : moduleName; + + if (throwOnRequire.has(normalizedName)) { + throw new Error(`Cannot find module '${moduleName}'`); + } + + if (shimRegistry[normalizedName]) { + return shimRegistry[normalizedName]; + } + + console.warn("[ignis] Unshimmed require:", moduleName); + return wrapWithProxy({}, `UNKNOWN(${moduleName})`); + }; + + installDebugHelpers(rawRegistry); +}