plugins 2.0 update
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# Sample TS Fullstack Plugin
|
||||
|
||||
This sample intentionally ships TypeScript source instead of generated JavaScript.
|
||||
|
||||
The server extracts the bundle, compiles `src/ui.ts` to `assets/sample-ts-fullstack.js`, and compiles `src/server.ts` to `server.js`. Both entrypoints import `src/shared.ts`, demonstrating that plugin authors can split logic across multiple TypeScript files.
|
||||
|
||||
No package install is performed during plugin extraction; use relative imports or APIs already available in the browser/server runtime.
|
||||
|
||||
On Windows, run this sample's local `build.bat` to create `sample-ts-fullstack.zip`. The batch file packages the TypeScript sources cleanly; compilation still happens on the server during extraction.
|
||||
@@ -0,0 +1,60 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "PLUGIN_DIR=%~dp0"
|
||||
set "PLUGIN_NAME=sample-ts-fullstack"
|
||||
|
||||
set "NATIVE_DIR=%PLUGIN_DIR%\native"
|
||||
set "WASM_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.wasm"
|
||||
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
|
||||
|
||||
if not exist "%PLUGIN_DIR%\config.json" (
|
||||
echo [error] config.json not found in %PLUGIN_DIR%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "WASM_SOURCE="
|
||||
if exist "%NATIVE_DIR%" (
|
||||
for %%S in ("%NATIVE_DIR%\*.c") do (
|
||||
if not defined WASM_SOURCE set "WASM_SOURCE=%%~fS"
|
||||
)
|
||||
)
|
||||
|
||||
if defined WASM_SOURCE (
|
||||
where clang >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [error] clang was not found. Install a WASI-capable clang or provide %WASM_OUT% yourself.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [build] clang --target=wasm32-wasi -O2 "!WASM_SOURCE!" ^> "%WASM_OUT%"
|
||||
clang --target=wasm32-wasi -O2 -Wl,--no-entry -Wl,--allow-undefined -Wl,--export=overlord_alloc -Wl,--export=overlord_free -Wl,--export=overlord_on_load -Wl,--export=overlord_on_event -Wl,--export=overlord_on_unload -o "%WASM_OUT%" "!WASM_SOURCE!"
|
||||
if errorlevel 1 (
|
||||
echo [error] WASM build failed
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
|
||||
|
||||
pushd "%PLUGIN_DIR%"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$zip = '%ZIP_OUT%'; $items = @(); foreach ($file in @('config.json', '%PLUGIN_NAME%.html', '%PLUGIN_NAME%.css', '%PLUGIN_NAME%.js', '%PLUGIN_NAME%.wasm', 'server.js')) { if (Test-Path -LiteralPath $file -PathType Leaf) { $items += $file } }; foreach ($dir in @('src', 'assets')) { if (Test-Path -LiteralPath $dir -PathType Container) { $items += $dir } }; if ($items.Count -eq 0) { throw 'No plugin files found to package' }; Compress-Archive -Path $items -DestinationPath $zip -Force"
|
||||
set "ZIP_STATUS=%ERRORLEVEL%"
|
||||
popd
|
||||
if not "%ZIP_STATUS%"=="0" exit /b %ZIP_STATUS%
|
||||
if errorlevel 1 (
|
||||
echo [error] zip failed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if defined PLUGIN_SIGN_KEY (
|
||||
where bun >nul 2>&1
|
||||
if not errorlevel 1 (
|
||||
echo [sign] Signing plugin with key: %PLUGIN_SIGN_KEY%
|
||||
bun run "%~dp0..\..\Overlord-Server\scripts\plugin-sign.ts" --key "%PLUGIN_SIGN_KEY%" "%ZIP_OUT%"
|
||||
) else (
|
||||
echo [warn] bun not found, skipping plugin signing
|
||||
)
|
||||
)
|
||||
|
||||
echo [ok] %ZIP_OUT%
|
||||
@@ -0,0 +1,3 @@
|
||||
#sample-ts-output {
|
||||
min-height: 9rem;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-slate-100">Sample TS Fullstack</h2>
|
||||
<p class="text-sm text-slate-400">Multiple TypeScript files compiled by the server at plugin extraction time.</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button id="sample-ts-ping" class="px-3 py-2 rounded-lg border border-cyan-700 bg-cyan-950/40 text-cyan-100">Ping backend</button>
|
||||
<button id="sample-ts-count" class="px-3 py-2 rounded-lg border border-emerald-700 bg-emerald-950/40 text-emerald-100">Increment</button>
|
||||
</div>
|
||||
<pre id="sample-ts-output" class="text-xs text-slate-300 whitespace-pre-wrap"></pre>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { RpcResponse } from "./shared";
|
||||
|
||||
type PluginContext = {
|
||||
db: {
|
||||
exec(sql: string): void;
|
||||
prepare(sql: string): {
|
||||
get(...args: unknown[]): any;
|
||||
run(...args: unknown[]): any;
|
||||
};
|
||||
};
|
||||
log: { info(message: string): void };
|
||||
};
|
||||
|
||||
function now(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function response(message: string, count?: number): RpcResponse {
|
||||
return { message, count, at: now() };
|
||||
}
|
||||
|
||||
export default {
|
||||
setup(ctx: PluginContext) {
|
||||
ctx.db.exec("CREATE TABLE IF NOT EXISTS counter (id INTEGER PRIMARY KEY CHECK (id = 1), value INTEGER NOT NULL)");
|
||||
ctx.db.prepare("INSERT OR IGNORE INTO counter (id, value) VALUES (1, 0)").run();
|
||||
ctx.log.info("sample-ts-fullstack ready");
|
||||
},
|
||||
|
||||
rpc: {
|
||||
ping(): RpcResponse {
|
||||
return response("hello from TypeScript server");
|
||||
},
|
||||
|
||||
increment(ctx: PluginContext, params: unknown): RpcResponse {
|
||||
const by = typeof (params as any)?.by === "number" ? Math.trunc((params as any).by) : 1;
|
||||
ctx.db.prepare("UPDATE counter SET value = value + ? WHERE id = 1").run(by);
|
||||
const row = ctx.db.prepare("SELECT value FROM counter WHERE id = 1").get();
|
||||
return response("counter updated", Number(row?.value || 0));
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export type RpcResponse = {
|
||||
message: string;
|
||||
count?: number;
|
||||
at: string;
|
||||
};
|
||||
|
||||
export function formatLine(label: string, value: RpcResponse): string {
|
||||
const count = typeof value.count === "number" ? ` count=${value.count}` : "";
|
||||
return `${label}: ${value.message}${count} at ${value.at}`;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { formatLine, type RpcResponse } from "./shared";
|
||||
|
||||
const output = document.getElementById("sample-ts-output");
|
||||
const pingButton = document.getElementById("sample-ts-ping");
|
||||
const countButton = document.getElementById("sample-ts-count");
|
||||
|
||||
function append(message: string) {
|
||||
if (!output) return;
|
||||
output.textContent = `${message}\n${output.textContent || ""}`;
|
||||
}
|
||||
|
||||
async function rpc(method: string, params?: unknown): Promise<RpcResponse> {
|
||||
const res = await fetch("/api/plugins/sample-ts-fullstack/rpc", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ method, params }),
|
||||
});
|
||||
const body = await res.json();
|
||||
if (!res.ok || body.ok === false) {
|
||||
throw new Error(body.error || res.statusText);
|
||||
}
|
||||
return body.result as RpcResponse;
|
||||
}
|
||||
|
||||
pingButton?.addEventListener("click", async () => {
|
||||
try {
|
||||
append(formatLine("ping", await rpc("ping")));
|
||||
} catch (err) {
|
||||
append(`ping failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
});
|
||||
|
||||
countButton?.addEventListener("click", async () => {
|
||||
try {
|
||||
append(formatLine("increment", await rpc("increment", { by: 1 })));
|
||||
} catch (err) {
|
||||
append(`increment failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user