diff --git a/.dockerignore b/.dockerignore index f67b580..02c0045 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,4 +12,3 @@ data tmp **/dist packages/bridge/main.js -apps/ignis-server/server/plugins/*/obsidian/main.js diff --git a/.gitignore b/.gitignore index 3c8f672..0bf12f3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,5 @@ investigation/ vaults/ packages/*/dist/ packages/bridge/main.js -apps/ignis-server/server/plugins/*/obsidian/main.js demo-vaults/ data/ diff --git a/apps/ignis-server/Dockerfile b/apps/ignis-server/Dockerfile index 6f83376..123747f 100644 --- a/apps/ignis-server/Dockerfile +++ b/apps/ignis-server/Dockerfile @@ -58,7 +58,7 @@ COPY packages/bridge/styles.css ./packages/bridge/ COPY --from=build /app/packages/shim/dist/shim-loader.js ./packages/shim/dist/shim-loader.js COPY --from=build /app/packages/ui/dist/ignis-ui.js ./packages/ui/dist/ignis-ui.js COPY --from=build /app/packages/bridge/main.js ./packages/bridge/main.js -COPY --from=build /app/apps/ignis-server/server/plugins/headless-sync/obsidian/main.js ./apps/ignis-server/server/plugins/headless-sync/obsidian/main.js +COPY --from=build /app/apps/ignis-server/server/plugins/headless-sync/obsidian/dist/ ./apps/ignis-server/server/plugins/headless-sync/obsidian/dist/ RUN chmod +x /app/apps/ignis-server/scripts/entrypoint.sh diff --git a/apps/ignis-server/server/index.js b/apps/ignis-server/server/index.js index b4e436e..71313e7 100644 --- a/apps/ignis-server/server/index.js +++ b/apps/ignis-server/server/index.js @@ -13,7 +13,11 @@ const { BRIDGE_PLUGIN_ID, migratePluginsFromAllVaults, } = require("./bridge-plugin"); -const { initPlugins, shutdownPlugins } = require("./plugin-system/manager"); +const { + initPlugins, + shutdownPlugins, + getBundledPluginDirs, +} = require("./plugin-system/manager"); const pluginRoutes = require("./routes/plugins"); writeCoalescer.configure({ writeCoalesceMs: config.writeCoalesceMs }); const { flushAll } = writeCoalescer; @@ -173,8 +177,19 @@ const server = app.listen(config.port, async () => { console.log(`[ignis] Vault root: ${config.vaultRoot}`); console.log(`[ignis] Vaults: ${Object.keys(config.vaults).join(", ")}`); - await migratePluginsFromAllVaults(config.vaultRoot, [BRIDGE_PLUGIN_ID]); await initPlugins({ app, config, wss, watcher }); + + const bundledPluginDirs = getBundledPluginDirs(); + + for (const { distDir } of bundledPluginDirs) { + app.use(express.static(distDir)); + } + + await migratePluginsFromAllVaults(config.vaultRoot, [ + BRIDGE_PLUGIN_ID, + ...bundledPluginDirs.map((d) => d.bundledPluginId), + ]); + bootstrapRoutes .warmUp() .catch((e) => console.warn("[bootstrap] warm-up error:", e.message)); diff --git a/apps/ignis-server/server/plugin-system/discovery.js b/apps/ignis-server/server/plugin-system/discovery.js index ed710e9..bd73fdd 100644 --- a/apps/ignis-server/server/plugin-system/discovery.js +++ b/apps/ignis-server/server/plugin-system/discovery.js @@ -40,17 +40,16 @@ function discoverPlugins(pluginsDir) { continue; } - let bundledPluginId = null; + let bundledManifest = null; if (plugin.obsidianPlugin) { try { - const manifest = JSON.parse( + bundledManifest = JSON.parse( fs.readFileSync( path.join(plugin.obsidianPlugin, "manifest.json"), "utf-8", ), ); - bundledPluginId = manifest.id; } catch { // No valid bundled plugin manifest } @@ -61,7 +60,8 @@ function discoverPlugins(pluginsDir) { name: plugin.name, description: plugin.description || "", obsidianPlugin: plugin.obsidianPlugin || null, - bundledPluginId, + bundledPluginId: bundledManifest ? bundledManifest.id : null, + bundledManifest, module: plugin, }); diff --git a/apps/ignis-server/server/plugin-system/manager.js b/apps/ignis-server/server/plugin-system/manager.js index e1a84c2..ec5bd3d 100644 --- a/apps/ignis-server/server/plugin-system/manager.js +++ b/apps/ignis-server/server/plugin-system/manager.js @@ -3,10 +3,6 @@ const path = require("path"); const express = require("express"); const { discoverPlugins } = require("./discovery"); const configStore = require("./config-store"); -const { - installObsidianPlugin, - removeObsidianPlugin, -} = require("./obsidian-plugin"); let discoveredPlugins = new Map(); const loadedPlugins = new Map(); @@ -50,18 +46,6 @@ async function initPlugins(ctx) { continue; } - const discovered = discoveredPlugins.get(pluginId); - - if (discovered.obsidianPlugin) { - try { - await installObsidianPlugin(discovered.obsidianPlugin, vaultPath); - } catch (e) { - console.error( - `[plugins] Failed to verify bundled plugin for ${pluginId} in ${vaultId}: ${e.message}`, - ); - } - } - const loaded = loadedPlugins.get(pluginId); if (loaded?.module?.onVaultEnabled) { @@ -182,25 +166,6 @@ async function enablePluginForVault(pluginId, vaultId) { await loadPlugin(pluginId); } - if (discovered.obsidianPlugin) { - try { - const result = await installObsidianPlugin( - discovered.obsidianPlugin, - vaultPath, - ); - - if (result.installed) { - console.log( - `[plugins] Installed bundled Obsidian plugin for ${pluginId} in vault: ${vaultId}`, - ); - } - } catch (e) { - console.error( - `[plugins] Failed to install bundled plugin for ${pluginId}: ${e.message}`, - ); - } - } - const loaded = loadedPlugins.get(pluginId); if (loaded?.module?.onVaultEnabled) { @@ -227,25 +192,6 @@ async function disablePluginForVault(pluginId, vaultId) { await loaded.module.onVaultDisabled(vaultId, vaultPath); } - if (discovered.obsidianPlugin) { - try { - const result = await removeObsidianPlugin( - discovered.obsidianPlugin, - vaultPath, - ); - - if (result.removed) { - console.log( - `[plugins] Removed bundled Obsidian plugin for ${pluginId} from vault: ${vaultId}`, - ); - } - } catch (e) { - console.error( - `[plugins] Failed to remove bundled plugin for ${pluginId}: ${e.message}`, - ); - } - } - const enabledVaults = configStore.getEnabledVaults(pluginConfig, pluginId); const updated = enabledVaults.filter((id) => id !== vaultId); configStore.setEnabledVaults(pluginConfig, pluginId, updated); @@ -256,6 +202,47 @@ async function disablePluginForVault(pluginId, vaultId) { } } +function getBundledPluginDirs() { + const dirs = []; + + for (const [, discovered] of discoveredPlugins) { + if (discovered.obsidianPlugin && discovered.bundledPluginId) { + dirs.push({ + bundledPluginId: discovered.bundledPluginId, + distDir: path.join(discovered.obsidianPlugin, "dist"), + }); + } + } + + return dirs; +} + +function getVirtualPluginsForVault(vaultId, version) { + const v = version ? `?v=${version}` : ""; + const result = []; + + for (const [pluginId, discovered] of discoveredPlugins) { + if (!discovered.obsidianPlugin || !discovered.bundledPluginId) { + continue; + } + + const enabledVaults = configStore.getEnabledVaults(pluginConfig, pluginId); + + if (!enabledVaults.includes(vaultId)) { + continue; + } + + result.push({ + id: discovered.bundledPluginId, + scriptUrl: `/${discovered.bundledPluginId}.js${v}`, + cssUrl: `/${discovered.bundledPluginId}.css${v}`, + manifest: discovered.bundledManifest, + }); + } + + return result; +} + function getDiscoveredPlugins() { const result = []; @@ -280,4 +267,6 @@ module.exports = { enablePluginForVault, disablePluginForVault, getDiscoveredPlugins, + getBundledPluginDirs, + getVirtualPluginsForVault, }; diff --git a/apps/ignis-server/server/routes/bootstrap.js b/apps/ignis-server/server/routes/bootstrap.js index 2f40b2d..04fdc99 100644 --- a/apps/ignis-server/server/routes/bootstrap.js +++ b/apps/ignis-server/server/routes/bootstrap.js @@ -9,7 +9,11 @@ const fsp = fs.promises; const path = require("path"); const zlib = require("zlib"); const config = require("../config"); -const { getDiscoveredPlugins } = require("../plugin-system/manager"); +const { + getDiscoveredPlugins, + getVirtualPluginsForVault, +} = require("../plugin-system/manager"); +const { getVersion } = require("../version"); const router = express.Router(); @@ -135,6 +139,7 @@ async function buildEntry(vaultId) { tree, // In demo mode, hide server-side plugins from the client. plugins: config.demoMode ? [] : getDiscoveredPlugins(), + virtualPlugins: getVirtualPluginsForVault(vaultId, getVersion()), }; const jsonBuf = Buffer.from(JSON.stringify(response)); diff --git a/build.js b/build.js index ab62834..5425726 100644 --- a/build.js +++ b/build.js @@ -1,6 +1,17 @@ const esbuild = require("esbuild"); +const fs = require("fs"); const path = require("path"); +const headlessSyncDir = path.join( + __dirname, + "apps", + "ignis-server", + "server", + "plugins", + "headless-sync", + "obsidian", +); + Promise.all([ // Build shim-loader.js (delegated to packages/shim) require("./packages/shim/build.js"), @@ -9,35 +20,21 @@ Promise.all([ require("./packages/ui/build.js"), // Build headless-sync bundled plugin - esbuild.build({ - entryPoints: [ - path.join( - __dirname, - "apps", - "ignis-server", - "server", - "plugins", - "headless-sync", - "obsidian", - "src", - "main.js", - ), - ], - bundle: true, - outfile: path.join( - __dirname, - "apps", - "ignis-server", - "server", - "plugins", - "headless-sync", - "obsidian", - "main.js", - ), - format: "cjs", - platform: "browser", - target: ["chrome90"], - external: ["obsidian", "fs"], //using fs shim - logLevel: "info", - }), + esbuild + .build({ + entryPoints: [path.join(headlessSyncDir, "src", "main.js")], + bundle: true, + outfile: path.join(headlessSyncDir, "dist", "ignis-headless-sync.js"), + format: "cjs", + platform: "browser", + target: ["chrome90"], + external: ["obsidian", "fs"], + logLevel: "info", + }) + .then(() => { + fs.copyFileSync( + path.join(headlessSyncDir, "styles.css"), + path.join(headlessSyncDir, "dist", "ignis-headless-sync.css"), + ); + }), ]).catch(() => process.exit(1)); diff --git a/packages/shim/src/init.js b/packages/shim/src/init.js index 0d25dda..9cd64d9 100644 --- a/packages/shim/src/init.js +++ b/packages/shim/src/init.js @@ -232,6 +232,7 @@ export function initialize() { autoTrustDemoVaults(bootstrap.vaultList); applyTree(bootstrap.tree); applyCoreSyncGuard(bootstrap.plugins); + window.__ignisVirtualPlugins = bootstrap.virtualPlugins || []; // Race the indexer: batch-fetch text content into ContentCache so // Obsidian's startup indexing reads hit the cache instead of the network. diff --git a/packages/shim/src/loader.js b/packages/shim/src/loader.js index 20e8599..6b24d22 100644 --- a/packages/shim/src/loader.js +++ b/packages/shim/src/loader.js @@ -4,7 +4,10 @@ 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"; +import { + extractObsidianModule, + loadVirtualPlugin, +} from "./virtual-plugin-loader.js"; // __IGNIS_VERSION__ is replaced at build time from package.json. window.__ignis = { version: __IGNIS_VERSION__ }; @@ -48,6 +51,15 @@ extractObsidianModule() const bridge = new IgnisBridgePlugin(window.app, BRIDGE_MANIFEST); await bridge.onload(); console.log("[ignis] bridge loaded"); + + for (const vp of window.__ignisVirtualPlugins || []) { + try { + await loadVirtualPlugin(vp); + console.log(`[ignis] virtual plugin loaded: ${vp.id}`); + } catch (e) { + console.error(`[ignis] virtual plugin load failed: ${vp.id}`, e); + } + } }) .catch((e) => console.error("[ignis] bridge load failed:", e)); diff --git a/packages/shim/src/virtual-plugin-loader.js b/packages/shim/src/virtual-plugin-loader.js index 8bc18ee..5503e20 100644 --- a/packages/shim/src/virtual-plugin-loader.js +++ b/packages/shim/src/virtual-plugin-loader.js @@ -103,3 +103,42 @@ export async function extractObsidianModule() { console.log("[ignis] obsidian module captured"); return captured; } + +export async function loadVirtualPlugin(entry) { + if (entry.cssUrl) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = entry.cssUrl; + link.setAttribute("data-ignis-virtual-plugin", entry.id); + document.head.appendChild(link); + } + + const res = await fetch(entry.scriptUrl); + + if (!res.ok) { + throw new Error( + `fetch ${entry.scriptUrl} -> ${res.status} ${res.statusText}`, + ); + } + + const src = + (await res.text()) + `\n//# sourceURL=ignis-virtual/${entry.id}.js`; + + const module = { exports: {} }; + const localRequire = (name) => + name === "obsidian" ? window.__obsidian : window.require(name); + + new Function("module", "exports", "require", src)( + module, + module.exports, + localRequire, + ); + + const PluginClass = module.exports.default || module.exports; + const instance = new PluginClass(window.app, entry.manifest); + + await instance.onload(); + + window.__ignis.plugins = window.__ignis.plugins || {}; + window.__ignis.plugins[entry.id] = { instance, manifest: entry.manifest }; +}