diff --git a/apps/ignis-server/server/bridge-plugin.js b/apps/ignis-server/server/bridge-plugin.js index b82011d..014b1a8 100644 --- a/apps/ignis-server/server/bridge-plugin.js +++ b/apps/ignis-server/server/bridge-plugin.js @@ -1,46 +1,42 @@ const fs = require("fs"); const path = require("path"); -const { - installObsidianPlugin, - isObsidianPluginInstalled, -} = require("./plugin-system/obsidian-plugin"); const BRIDGE_PLUGIN_ID = "ignis-bridge"; -const BRIDGE_PLUGIN_DIR = path.join(__dirname, "..", "..", "..", "packages", "bridge"); -// .ignis metadata helpers +// Old vaults still have bridge in .obsidian/plugins from before it became virtual. +async function migratePluginFromVault(vaultPath, vaultName, pluginId) { + let didWork = false; -async function getIgnisMeta(vaultPath) { - const metaFile = path.join(vaultPath, ".ignis", "meta.json"); + const pluginDir = path.join(vaultPath, ".obsidian", "plugins", pluginId); + + if (await fs.promises.stat(pluginDir).catch(() => null)) { + await fs.promises.rm(pluginDir, { recursive: true, force: true }); + didWork = true; + } + + const cpFile = path.join(vaultPath, ".obsidian", "community-plugins.json"); try { - const content = await fs.promises.readFile(metaFile, "utf-8"); - return JSON.parse(content); - } catch { - return {}; + const list = JSON.parse(await fs.promises.readFile(cpFile, "utf-8")); + + if (Array.isArray(list)) { + const filtered = list.filter((id) => id !== pluginId); + + if (filtered.length !== list.length) { + await fs.promises.writeFile(cpFile, JSON.stringify(filtered)); + didWork = true; + } + } + } catch {} + + if (didWork) { + console.log(`[ignis] Migrated ${pluginId} out of vault: ${vaultName}`); } + + return didWork; } -async function setIgnisMeta(vaultPath, data) { - const ignisDir = path.join(vaultPath, ".ignis"); - const metaFile = path.join(ignisDir, "meta.json"); - - await fs.promises.mkdir(ignisDir, { recursive: true }); - await fs.promises.writeFile(metaFile, JSON.stringify(data, null, 2)); -} - -// Bridge plugin install/check - -async function isBridgePluginInstalled(vaultPath) { - return isObsidianPluginInstalled(BRIDGE_PLUGIN_ID, vaultPath); -} - -async function installBridgePlugin(vaultPath) { - const result = await installObsidianPlugin(BRIDGE_PLUGIN_DIR, vaultPath); - return result.installed; -} - -async function updateBridgePluginInAllVaults(vaultRoot) { +async function migratePluginsFromAllVaults(vaultRoot, pluginIds) { if (!(await fs.promises.stat(vaultRoot).catch(() => null))) { return; } @@ -53,18 +49,14 @@ async function updateBridgePluginInAllVaults(vaultRoot) { } const vaultPath = path.join(vaultRoot, entry.name); - const installed = await installBridgePlugin(vaultPath); - if (installed) { - console.log(`[ignis] Installed bridge plugin in vault: ${entry.name}`); + for (const pluginId of pluginIds) { + await migratePluginFromVault(vaultPath, entry.name, pluginId); } } } module.exports = { - installBridgePlugin, - updateBridgePluginInAllVaults, - isBridgePluginInstalled, - getIgnisMeta, - setIgnisMeta, + BRIDGE_PLUGIN_ID, + migratePluginsFromAllVaults, }; diff --git a/apps/ignis-server/server/demo/demo-provision.js b/apps/ignis-server/server/demo/demo-provision.js index 1d616be..a4840d8 100644 --- a/apps/ignis-server/server/demo/demo-provision.js +++ b/apps/ignis-server/server/demo/demo-provision.js @@ -1,6 +1,6 @@ // Vault provisioning for demo sessions. // -// Copies the template into a session-prefixed dir, installs the bridge plugin, and registers the vault on the session. +// Copies the template into a session-prefixed dir and registers the vault on the session. // Re-provisions if disk was wiped under an existing session. const fs = require("fs"); @@ -8,7 +8,6 @@ const fsp = fs.promises; const path = require("path"); const config = require("../config"); -const { installBridgePlugin } = require("../bridge-plugin"); const bootstrapRoutes = require("../routes/bootstrap"); const { sessions, makeStorageName } = require("./demo-sessions"); @@ -96,9 +95,6 @@ async function provisionVault(sessionId, userVaultName) { // Copy template (default: Welcome.md, Getting Started.md, .obsidian/*). await fsp.cp(config.demoTemplateDir, vaultPath, { recursive: true }); - // Install bridge plugin - await installBridgePlugin(vaultPath); - config.refreshVaults(); bootstrapRoutes.invalidateVault(storageName); diff --git a/apps/ignis-server/server/index.js b/apps/ignis-server/server/index.js index 1fa3353..b4e436e 100644 --- a/apps/ignis-server/server/index.js +++ b/apps/ignis-server/server/index.js @@ -9,7 +9,10 @@ const { watcher, writeCoalescer, } = require("@ignis/server-core"); -const { updateBridgePluginInAllVaults } = require("./bridge-plugin"); +const { + BRIDGE_PLUGIN_ID, + migratePluginsFromAllVaults, +} = require("./bridge-plugin"); const { initPlugins, shutdownPlugins } = require("./plugin-system/manager"); const pluginRoutes = require("./routes/plugins"); writeCoalescer.configure({ writeCoalesceMs: config.writeCoalesceMs }); @@ -170,7 +173,7 @@ const server = app.listen(config.port, async () => { console.log(`[ignis] Vault root: ${config.vaultRoot}`); console.log(`[ignis] Vaults: ${Object.keys(config.vaults).join(", ")}`); - await updateBridgePluginInAllVaults(config.vaultRoot); + await migratePluginsFromAllVaults(config.vaultRoot, [BRIDGE_PLUGIN_ID]); await initPlugins({ app, config, wss, watcher }); bootstrapRoutes .warmUp() diff --git a/apps/ignis-server/server/routes/bootstrap.js b/apps/ignis-server/server/routes/bootstrap.js index 3a09eba..2f40b2d 100644 --- a/apps/ignis-server/server/routes/bootstrap.js +++ b/apps/ignis-server/server/routes/bootstrap.js @@ -9,7 +9,6 @@ const fsp = fs.promises; const path = require("path"); const zlib = require("zlib"); const config = require("../config"); -const { isBridgePluginInstalled, getIgnisMeta } = require("../bridge-plugin"); const { getDiscoveredPlugins } = require("../plugin-system/manager"); const router = express.Router(); @@ -76,20 +75,13 @@ async function walkTree(rootPath) { return { tree, dirMtimes }; } -async function buildVaultInfo(vaultId, vaultPath) { - const pluginInstalled = await isBridgePluginInstalled(vaultPath); - const ignisMeta = await getIgnisMeta(vaultPath); - +function buildVaultInfo(vaultId, vaultPath) { return { id: vaultId, name: vaultId, path: vaultPath, platform: process.platform, version: config.obsidianVersion, - ignisPlugin: { - installed: pluginInstalled, - prompted: ignisMeta.pluginPrompted || false, - }, }; } @@ -134,10 +126,8 @@ async function buildEntry(vaultId) { } const t0 = Date.now(); - const [vault, { tree, dirMtimes }] = await Promise.all([ - buildVaultInfo(vaultId, vaultPath), - walkTree(vaultPath), - ]); + const vault = buildVaultInfo(vaultId, vaultPath); + const { tree, dirMtimes } = await walkTree(vaultPath); const response = { vault, diff --git a/apps/ignis-server/server/routes/vault.js b/apps/ignis-server/server/routes/vault.js index befccb1..438298b 100644 --- a/apps/ignis-server/server/routes/vault.js +++ b/apps/ignis-server/server/routes/vault.js @@ -2,12 +2,6 @@ const express = require("express"); const fs = require("fs"); const config = require("../config"); const path = require("path"); -const { - isBridgePluginInstalled, - getIgnisMeta, - setIgnisMeta, - installBridgePlugin, -} = require("../bridge-plugin"); const bootstrapRoutes = require("./bootstrap"); const router = express.Router(); @@ -34,19 +28,12 @@ router.get("/info", async (req, res) => { return res.status(404).json({ error: "Vault not found", id: vaultId }); } - const pluginInstalled = await isBridgePluginInstalled(vaultPath); - const ignisMeta = await getIgnisMeta(vaultPath); - res.json({ id: vaultId, name: vaultId, path: vaultPath, platform: process.platform, version: config.obsidianVersion, - ignisPlugin: { - installed: pluginInstalled, - prompted: ignisMeta.pluginPrompted || false, - }, }); }); @@ -66,8 +53,6 @@ router.post("/create", async (req, res) => { recursive: false, }); - await installBridgePlugin(vaultPath); - config.refreshVaults(); bootstrapRoutes.invalidateVault(name); @@ -138,42 +123,4 @@ router.delete("/remove", async (req, res) => { } }); -// POST /api/vault/install-plugin { vault, dismiss } - install plugin or mark as prompted -router.post("/install-plugin", async (req, res) => { - const vaultId = req.body?.vault; - const dismiss = req.body?.dismiss || false; - - if (!vaultId) { - return res.status(400).json({ error: "Missing vault ID" }); - } - - const vaultPath = config.getVaultPath(vaultId); - - if (!vaultPath) { - return res.status(404).json({ error: "Vault not found" }); - } - - try { - const meta = await getIgnisMeta(vaultPath); - - if (dismiss) { - // User clicked "Don't Ask Again" or "Not Now" - meta.pluginPrompted = true; - await setIgnisMeta(vaultPath, meta); - - return res.json({ ok: true, prompted: true }); - } else { - // User wants to install the plugin - const installed = await installBridgePlugin(vaultPath); - - meta.pluginPrompted = true; - await setIgnisMeta(vaultPath, meta); - - return res.json({ ok: true, installed, prompted: true }); - } - } catch (e) { - res.status(500).json({ error: e.message, code: e.code }); - } -}); - module.exports = router; diff --git a/build.js b/build.js index 737e849..a75727a 100644 --- a/build.js +++ b/build.js @@ -8,9 +8,6 @@ Promise.all([ // Build ignis-ui.js (delegated to packages/ui) require("./packages/ui/build.js"), - // Build ignis-bridge plugin (delegated to packages/bridge) - require("./packages/bridge/build.js"), - // Build headless-sync bundled plugin esbuild.build({ entryPoints: [ diff --git a/packages/bridge/build.js b/packages/bridge/build.js deleted file mode 100644 index 37f6a9d..0000000 --- a/packages/bridge/build.js +++ /dev/null @@ -1,13 +0,0 @@ -const esbuild = require("esbuild"); -const path = require("path"); - -module.exports = esbuild.build({ - entryPoints: [path.join(__dirname, "src", "main.js")], - bundle: true, - outfile: path.join(__dirname, "main.js"), - format: "cjs", - platform: "browser", - target: ["chrome90"], - external: ["obsidian", "fs"], - logLevel: "info", -}); diff --git a/packages/bridge/manifest.json b/packages/bridge/manifest.json deleted file mode 100644 index 4ef8f86..0000000 --- a/packages/bridge/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "ignis-bridge", - "name": "Ignis Bridge", - "version": "0.8.1", - "minAppVersion": "1.12.4", - "description": "Additional Ignis specific functionality and ignis plugin management.", - "author": "Nystik", - "authorUrl": "https://github.com/Nystik-gh/ignis", - "isDesktopOnly": false -} diff --git a/packages/bridge/package.json b/packages/bridge/package.json index c98f2c4..7dadf12 100644 --- a/packages/bridge/package.json +++ b/packages/bridge/package.json @@ -2,10 +2,5 @@ "name": "@ignis/bridge", "version": "0.0.0-internal", "private": true, - "scripts": { - "build": "node build.js" - }, - "devDependencies": { - "esbuild": "^0.20.0" - } + "main": "src/main.js" } diff --git a/packages/bridge/src/main.js b/packages/bridge/src/main.js index 8d46554..3dae017 100644 --- a/packages/bridge/src/main.js +++ b/packages/bridge/src/main.js @@ -13,8 +13,6 @@ const { initStatusBar } = require("./status-bar"); const { WorkspacePickerModal } = require("./workspace-picker"); const { startDemoGuards, stopDemoGuards } = require("./demo-guards"); -window.__obsidianAPI = require("obsidian"); - class IgnisBridgePlugin extends Plugin { async onload() { if (!window.__ignis) { diff --git a/packages/shim/build.js b/packages/shim/build.js index 265023c..31f0a75 100644 --- a/packages/shim/build.js +++ b/packages/shim/build.js @@ -13,6 +13,10 @@ module.exports = esbuild.build({ alias: { path: "path-browserify", }, + loader: { + ".css": "text", + }, + external: ["obsidian", "fs"], define: { __IGNIS_VERSION__: JSON.stringify(ignisVersion), }, diff --git a/packages/shim/src/css-overrides.js b/packages/shim/src/css-overrides.js index c87ada4..2fc71b3 100644 --- a/packages/shim/src/css-overrides.js +++ b/packages/shim/src/css-overrides.js @@ -1,4 +1,4 @@ -// Injects a link to the CSS overrides stylesheet served from /assets/overrides.css. +import bridgeCss from "@ignis/bridge/styles.css"; export function installCssOverrides() { const link = document.createElement("link"); @@ -6,4 +6,9 @@ export function installCssOverrides() { link.href = "/assets/overrides.css"; link.setAttribute("data-ignis", "css-overrides"); document.head.appendChild(link); + + const bridgeStyle = document.createElement("style"); + bridgeStyle.textContent = bridgeCss; + bridgeStyle.setAttribute("data-ignis", "bridge-css"); + document.head.appendChild(bridgeStyle); } diff --git a/packages/shim/src/fs/promises.js b/packages/shim/src/fs/promises.js index 3b4d908..565ecb9 100644 --- a/packages/shim/src/fs/promises.js +++ b/packages/shim/src/fs/promises.js @@ -1,6 +1,7 @@ import { markLocalOp } from "./echo-guard.js"; import { isInputCachePath, inputCacheGet } from "./input-cache.js"; import { applyReadTransform, applyWriteTransform, resolvePath } from "./transforms.js"; +import { hasVirtualFile, getVirtualFile } from "./virtual-files.js"; export function createFsPromises(metadataCache, contentCache, transport) { return { @@ -49,6 +50,21 @@ export function createFsPromises(metadataCache, contentCache, transport) { const wantText = encoding === "utf8" || encoding === "utf-8"; const resolved = resolvePath(path); + // Virtual plugin source overrides any cache/transport version. + if (hasVirtualFile(resolved)) { + const content = getVirtualFile(resolved); + + if (wantText) { + return typeof content === "string" + ? content + : new TextDecoder().decode(content); + } + + return typeof content === "string" + ? new TextEncoder().encode(content) + : content; + } + let result = null; // Check input cache for files picked via browser file dialogs. diff --git a/packages/shim/src/fs/virtual-files.js b/packages/shim/src/fs/virtual-files.js new file mode 100644 index 0000000..65078f0 --- /dev/null +++ b/packages/shim/src/fs/virtual-files.js @@ -0,0 +1,23 @@ +// Virtual plugin source served from memory; the fs shim's read path checks here before disk. + +function normalize(p) { + return (p || "").replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, ""); +} + +const virtualFiles = new Map(); + +export function setVirtualFile(path, content) { + virtualFiles.set(normalize(path), content); +} + +export function removeVirtualFile(path) { + virtualFiles.delete(normalize(path)); +} + +export function getVirtualFile(path) { + return virtualFiles.get(normalize(path)); +} + +export function hasVirtualFile(path) { + return virtualFiles.has(normalize(path)); +} diff --git a/packages/shim/src/loader.js b/packages/shim/src/loader.js index 0319de4..20e8599 100644 --- a/packages/shim/src/loader.js +++ b/packages/shim/src/loader.js @@ -4,11 +4,24 @@ import { installCssOverrides } from "./css-overrides.js"; import { initialize } from "./init.js"; import { fsShim } from "./fs/index.js"; import { registerUI } from "./ui-registry.js"; +import { extractObsidianModule } from "./virtual-plugin-loader.js"; // __IGNIS_VERSION__ is replaced at build time from package.json. window.__ignis = { version: __IGNIS_VERSION__ }; window.__ignis_registerUI = registerUI; +const BRIDGE_MANIFEST = { + id: "ignis-bridge", + name: "Ignis Bridge", + version: __IGNIS_VERSION__, + minAppVersion: "1.12.4", + description: + "Additional Ignis specific functionality and ignis plugin management.", + author: "Nystik", + authorUrl: "https://github.com/Nystik-gh/ignis", + isDesktopOnly: false, +}; + installGlobals(); // process, Buffer, window overrides (before require so Buffer is available) installRequire(); // shim registry, window.require installCssOverrides(); // browser-specific CSS fixes @@ -27,4 +40,15 @@ if (window.__currentVaultId) { fsShim._watcherClient.connect(window.__currentVaultId); } +extractObsidianModule() + .then(async () => { + // Dynamic import so bridge's top-level require("obsidian") fires after installRequire + extractObsidianModule. + const mod = await import("@ignis/bridge"); + const IgnisBridgePlugin = mod.default || mod; + const bridge = new IgnisBridgePlugin(window.app, BRIDGE_MANIFEST); + await bridge.onload(); + console.log("[ignis] bridge loaded"); + }) + .catch((e) => console.error("[ignis] bridge load failed:", e)); + console.log("[ignis] Shim loader initialized"); diff --git a/packages/shim/src/require.js b/packages/shim/src/require.js index d2a9a55..1444460 100644 --- a/packages/shim/src/require.js +++ b/packages/shim/src/require.js @@ -67,3 +67,8 @@ export function installRequire() { installDebugHelpers(rawRegistry); } + +// For modules captured at runtime, e.g. the obsidian module via the virtual-plugin loader. +export function registerShim(name, mod) { + shimRegistry[name] = mod; +} diff --git a/packages/shim/src/virtual-plugin-loader.js b/packages/shim/src/virtual-plugin-loader.js new file mode 100644 index 0000000..8bc18ee --- /dev/null +++ b/packages/shim/src/virtual-plugin-loader.js @@ -0,0 +1,105 @@ +// Capture the obsidian module via a one-shot synthetic plugin so virtual plugins (bridge, future bundled) can require("obsidian"). + +import { setVirtualFile, removeVirtualFile } from "./fs/virtual-files.js"; +import { registerShim } from "./require.js"; + +const EXTRACTOR_ID = "ignis-obsidian-extractor"; +const EXTRACTOR_DIR = ".ignis/virtual/" + EXTRACTOR_ID; +const EXTRACTOR_PATH = EXTRACTOR_DIR + "/main.js"; + +const EXTRACTOR_SRC = ` +const obsidian = require("obsidian"); +window.__ignisCapturedObsidian = obsidian; +module.exports = class extends obsidian.Plugin { + onload() {} +}; +`; + +const EXTRACTOR_MANIFEST = { + id: EXTRACTOR_ID, + name: "Ignis Obsidian Module Extractor", + version: "0.0.0", + minAppVersion: "1.0.0", + description: "Internal: captures the obsidian module for virtual plugins.", + author: "ignis", + authorUrl: "", + isDesktopOnly: false, + dir: EXTRACTOR_DIR, +}; + +function waitForApp() { + return new Promise((resolve) => { + if (window.app && window.app.plugins && window.app.workspace) { + return resolve(); + } + + const interval = setInterval(() => { + if (window.app && window.app.plugins && window.app.workspace) { + clearInterval(interval); + resolve(); + } + }, 20); + }); +} + +export async function extractObsidianModule() { + if (window.__obsidian) { + return window.__obsidian; + } + + await waitForApp(); + + const plugins = window.app.plugins; + + // loadPlugin gates on isEnabled(). Force-enable, restore on cleanup. + const wasEnabled = plugins.isEnabled(); + let toggledOn = false; + + if (!wasEnabled) { + try { + await plugins.setEnable(true); + toggledOn = true; + } catch (e) { + console.warn( + "[ignis] could not enable community plugins for extractor:", + e, + ); + } + } + + setVirtualFile(EXTRACTOR_PATH, EXTRACTOR_SRC); + plugins.manifests[EXTRACTOR_ID] = EXTRACTOR_MANIFEST; + + try { + await plugins.loadPlugin(EXTRACTOR_ID); + } catch (e) { + console.error("[ignis] extractor load failed:", e); + } + + const captured = window.__ignisCapturedObsidian; + + try { + await plugins.unloadPlugin(EXTRACTOR_ID); + } catch {} + + delete plugins.manifests[EXTRACTOR_ID]; + removeVirtualFile(EXTRACTOR_PATH); + delete window.__ignisCapturedObsidian; + + if (toggledOn) { + try { + await plugins.setEnable(false); + } catch {} + } + + if (!captured) { + console.error("[ignis] obsidian module extraction failed"); + return null; + } + + window.__obsidian = captured; + registerShim("obsidian", captured); + + console.log("[ignis] obsidian module captured"); + return captured; +}