feat(*): svelte 5 panel view sharing the gateway ui, obsidian theme, preview and tests

This commit is contained in:
hh
2026-08-29 18:30:31 +02:00
parent a1bf4db905
commit 04400c5076
33 changed files with 3378 additions and 1803 deletions
+115 -34
View File
@@ -1,40 +1,121 @@
import esbuild from "esbuild";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import builtins from "builtin-modules";
import esbuild from "esbuild";
import sveltePlugin from "esbuild-svelte";
export const root = path.dirname(fileURLToPath(import.meta.url));
export const LIB = path.resolve(root, "../beaver-gateway/ui/src/lib");
const EXTERNAL = [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
];
const LIB_IMPORT = /^\$lib\//;
const APP_IMPORT = /^\$app\//;
const SONNER_IMPORT = /^svelte-sonner$/;
const OBSIDIAN_IMPORT = /^obsidian$/;
const BARE_IMPORT = /^[^./#]/;
function isExternal(specifier) {
return (
specifier.startsWith("node:") ||
EXTERNAL.some(
(name) => specifier === name || specifier.startsWith(`${name}/`)
)
);
}
// The panel is beaver-gateway's ``ui/src/lib``, compiled into this plugin.
// Two rules keep it one bundle: ``$lib/*`` points at that directory, and
// every bare import - svelte, bits-ui, the icons - resolves from this
// plugin's node_modules, never from the gateway's, so there is exactly one
// Svelte runtime. ``svelte-sonner`` becomes Obsidian notices; ``$app/*``
// is SvelteKit-only and refused.
export function sharedLib({ obsidian = "external" } = {}) {
return {
name: "beaver-shared-lib",
setup(build) {
build.onResolve({ filter: LIB_IMPORT }, (args) =>
build.resolve(`./${args.path.slice("$lib/".length)}`, {
kind: args.kind,
pluginData: { pinned: true },
resolveDir: LIB,
})
);
build.onResolve({ filter: APP_IMPORT }, (args) => ({
errors: [
{
text: `${args.path} is SvelteKit-only; the panel must not import it`,
},
],
}));
build.onResolve({ filter: SONNER_IMPORT }, () => ({
path: path.join(root, "src/shims/sonner.ts"),
}));
build.onResolve({ filter: OBSIDIAN_IMPORT }, () =>
obsidian === "external"
? { external: true, path: "obsidian" }
: { path: obsidian }
);
build.onResolve({ filter: BARE_IMPORT }, (args) => {
if (args.pluginData?.pinned) {
return;
}
if (isExternal(args.path)) {
return { external: true, path: args.path };
}
return build.resolve(args.path, {
kind: args.kind,
pluginData: { pinned: true },
resolveDir: root,
});
});
},
};
}
export const svelte = () =>
sveltePlugin({
compilerOptions: { css: "injected", runes: true },
filterWarnings: (warning) => !warning.filename?.includes("node_modules"),
});
const prod = process.argv[2] === "production";
const ctx = await esbuild.context({
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
format: "cjs",
target: "es2022",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
});
if (prod) {
await ctx.rebuild();
await ctx.dispose();
} else {
await ctx.watch();
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const ctx = await esbuild.context({
bundle: true,
conditions: ["svelte", "browser"],
entryPoints: ["src/main.ts"],
format: "cjs",
logLevel: "info",
mainFields: ["svelte", "browser", "module", "main"],
minify: prod,
outfile: "main.js",
plugins: [sharedLib(), svelte()],
sourcemap: prod ? false : "inline",
target: "es2022",
treeShaking: true,
});
if (prod) {
await ctx.rebuild();
await ctx.dispose();
} else {
await ctx.watch();
}
}