load bundled plugins via virtual-plugin loader

This commit is contained in:
Nystik
2026-05-24 17:41:13 +02:00
parent f05ee9e856
commit 9eeff3c1b3
11 changed files with 152 additions and 96 deletions

View File

@@ -12,4 +12,3 @@ data
tmp
**/dist
packages/bridge/main.js
apps/ignis-server/server/plugins/*/obsidian/main.js

1
.gitignore vendored
View File

@@ -4,6 +4,5 @@ investigation/
vaults/
packages/*/dist/
packages/bridge/main.js
apps/ignis-server/server/plugins/*/obsidian/main.js
demo-vaults/
data/

View File

@@ -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

View File

@@ -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));

View File

@@ -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,
});

View File

@@ -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,
};

View File

@@ -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));

View File

@@ -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));

View File

@@ -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.

View File

@@ -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));

View File

@@ -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 };
}