mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: dev:all runner for web, electron and docker with plugin hot reload, termix-plugin dev
This commit is contained in:
1 parent
49fc241e06
commit
ec80a8ef8d
24 files changed
+1848
-44
No files matched your search
@@ -35,8 +35,14 @@ npx termix-plugin pack # write <id>-<version>.tmxplug
|
||||
npx termix-plugin sign <file.tmxplug> # sign it, needs TERMIX_PLUGIN_SIGNING_KEY
|
||||
npx termix-plugin verify <file.tmxplug> --key <base64> # check a signature
|
||||
npx termix-plugin keygen --out <dir> # make a new signing key pair
|
||||
npx termix-plugin dev --server <url> --key <tmx_...> # install on a server and reinstall on every change
|
||||
```
|
||||
|
||||
`dev` needs developer mode on (Admin settings, Plugins) and an API key from an
|
||||
admin account. It installs the plugin from a file, marked unverified, and
|
||||
reinstalls it each time you save. `TERMIX_SERVER_URL` and `TERMIX_API_KEY` work
|
||||
in place of the flags. Reload the page to pick up frontend changes.
|
||||
|
||||
<br />
|
||||
|
||||
## Support
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { build } from "./build.mjs";
|
||||
import { pack } from "./pack.mjs";
|
||||
import { readManifest } from "../lib/plugin-dir.mjs";
|
||||
|
||||
const SOURCE_DIRS = new Set(["src", "locales", "migrations"]);
|
||||
const SOURCE_FILES = new Set(["manifest.json", "package.json", "README.md"]);
|
||||
|
||||
function readOption(args, name) {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1) return null;
|
||||
const value = args[index + 1];
|
||||
if (!value) throw new Error(`${name} needs a value`);
|
||||
return value;
|
||||
}
|
||||
|
||||
/** True when a changed path (relative to the plugin) feeds its build. */
|
||||
export function isSourceChange(filename) {
|
||||
if (!filename) return false;
|
||||
const parts = String(filename).replaceAll("\\", "/").split("/");
|
||||
if (parts.length === 1) return SOURCE_FILES.has(parts[0]);
|
||||
return SOURCE_DIRS.has(parts[0]);
|
||||
}
|
||||
|
||||
export function resolveDevOptions(args, env = process.env) {
|
||||
const server = (
|
||||
readOption(args, "--server") ??
|
||||
env.TERMIX_SERVER_URL ??
|
||||
"http://localhost:30001"
|
||||
).replace(/\/+$/, "");
|
||||
const key = readOption(args, "--key") ?? env.TERMIX_API_KEY ?? null;
|
||||
if (!key) {
|
||||
throw new Error(
|
||||
"An admin API key is needed. Pass --key tmx_... or set TERMIX_API_KEY.",
|
||||
);
|
||||
}
|
||||
return { server, key, once: args.includes("--once") };
|
||||
}
|
||||
|
||||
const HINTS = {
|
||||
DEVELOPER_MODE_OFF:
|
||||
"Turn on developer mode in Admin settings, under Plugins, then save again.",
|
||||
SIGNED_ONLY:
|
||||
"The server sets TERMIX_REQUIRE_SIGNED_PLUGINS, so it never installs unsigned files.",
|
||||
BUNDLED_ID: "That id ships with Termix. Use a different id while developing.",
|
||||
ALREADY_INSTALLED:
|
||||
"Uninstall the copy installed from the registry first, then save again.",
|
||||
};
|
||||
|
||||
async function request(fetchImpl, url, init) {
|
||||
const response = await fetchImpl(url, init);
|
||||
const text = await response.text();
|
||||
let body = null;
|
||||
try {
|
||||
body = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
body = { error: text };
|
||||
}
|
||||
if (!response.ok) {
|
||||
const code = body?.code;
|
||||
const hint = code && HINTS[code] ? `\n ${HINTS[code]}` : "";
|
||||
throw new Error(
|
||||
`${response.status} ${body?.error ?? response.statusText}${hint}`,
|
||||
);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/** Uploads a packed .tmxplug and installs it, agreeing to what it asks for. */
|
||||
export async function pushPlugin(
|
||||
file,
|
||||
{ server, key },
|
||||
fetchImpl = globalThis.fetch,
|
||||
) {
|
||||
const auth = { Authorization: `Bearer ${key}` };
|
||||
const preview = await request(fetchImpl, `${server}/plugins/upload`, {
|
||||
method: "POST",
|
||||
headers: { ...auth, "Content-Type": "application/octet-stream" },
|
||||
body: fs.readFileSync(file),
|
||||
});
|
||||
const result = await request(
|
||||
fetchImpl,
|
||||
`${server}/plugins/upload/${preview.token}/install`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...auth, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ capabilities: preview.capabilities }),
|
||||
},
|
||||
);
|
||||
return { preview, result };
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds, packs and installs the plugin on a running Termix server, then
|
||||
* does it again on every source change. The server needs developer mode on.
|
||||
*/
|
||||
export async function dev({ cwd, args = [] }) {
|
||||
const options = resolveDevOptions(args);
|
||||
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-plugin-dev-"));
|
||||
let lastCapabilities = null;
|
||||
|
||||
const cycle = async () => {
|
||||
const started = Date.now();
|
||||
await build({ cwd });
|
||||
const file = await pack({ cwd, args: ["--out", outDir] });
|
||||
const { preview, result } = await pushPlugin(file, options);
|
||||
fs.rmSync(file, { force: true });
|
||||
const capabilities = preview.capabilities.join(", ") || "none";
|
||||
if (capabilities !== lastCapabilities) {
|
||||
console.log(` capabilities ${capabilities}`);
|
||||
lastCapabilities = capabilities;
|
||||
}
|
||||
console.log(
|
||||
`installed ${result.id}@${result.version} on ${options.server} (${result.state}, ${Date.now() - started} ms)`,
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
await cycle();
|
||||
} catch (error) {
|
||||
if (options.once) throw error;
|
||||
console.error(error?.message ?? error);
|
||||
}
|
||||
if (options.once) return;
|
||||
|
||||
const { id } = readManifest(cwd);
|
||||
console.log(`watching ${id} for changes, Ctrl+C to stop`);
|
||||
let timer = null;
|
||||
let running = false;
|
||||
let again = false;
|
||||
const fire = async () => {
|
||||
if (running) {
|
||||
again = true;
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
try {
|
||||
await cycle();
|
||||
} catch (error) {
|
||||
console.error(error?.message ?? error);
|
||||
} finally {
|
||||
running = false;
|
||||
if (again) {
|
||||
again = false;
|
||||
void fire();
|
||||
}
|
||||
}
|
||||
};
|
||||
fs.watch(cwd, { recursive: true }, (_event, filename) => {
|
||||
if (!isSourceChange(filename)) return;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => void fire(), 300);
|
||||
});
|
||||
await new Promise(() => {});
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { test } from "./commands/test.mjs";
|
||||
import { pack } from "./commands/pack.mjs";
|
||||
import { sign, verify, keygen } from "./commands/sign.mjs";
|
||||
import { migrations } from "./commands/migrations.mjs";
|
||||
import { dev } from "./commands/dev.mjs";
|
||||
|
||||
const COMMANDS = {
|
||||
build,
|
||||
@@ -28,6 +29,7 @@ const COMMANDS = {
|
||||
verify,
|
||||
keygen,
|
||||
migrations,
|
||||
dev,
|
||||
};
|
||||
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
@@ -46,6 +48,9 @@ if (!command || command === "--help" || command === "-h") {
|
||||
" verify Check a .tmxplug's .sig <file> --key <base64>[,...]",
|
||||
" keygen Write a new signing key pair [--out dir]",
|
||||
" migrations Generate migrations from the plugin's table definitions",
|
||||
" dev Build, pack and install on a running server, again on every change",
|
||||
" [--server url] [--key tmx_...] [--once]",
|
||||
" (or TERMIX_SERVER_URL and TERMIX_API_KEY; needs developer mode)",
|
||||
].join("\n"),
|
||||
);
|
||||
process.exit(command ? 0 : 1);
|
||||
|
||||
Reference in new issue
Block a user