fix: plugin sdk 1.0.5 with optional ui peers, safer timeouts and validate checks

This commit is contained in:
LukeGus committed 2026-10-09 07:37:42 -05:00
1 parent af09aa1028
commit b0a0ee3e00
10 files changed
+113 -12

No files matched your search

+13 -1
View File
@@ -15015,7 +15015,7 @@
},
"packages/plugin-sdk": {
"name": "@termix-ssh/plugin-sdk",
"version": "1.1.0",
"version": "1.0.5",
"license": "Apache-2.0",
"dependencies": {
"@deadendjs/swagger-jsdoc": "^8.1.2",
@@ -15054,9 +15054,21 @@
"drizzle-orm": {
"optional": true
},
"i18next": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-i18next": {
"optional": true
},
"sonner": {
"optional": true
},
"ssh2": {
"optional": true
},
+9
View File
@@ -1,5 +1,14 @@
# Changelog
## 1.0.5
### Fixed
- `execCommand` timeout errors no longer include the command, which could hold a sudo password
- `termix-plugin validate` fails when a plugin ships migrations for some database engines but not all three
- `termix-plugin migrations` loads the table definitions from a path with a `#` or other URL characters in it
- `i18next`, `react-dom`, `react-i18next` and `sonner` are optional peer dependencies, so npm stops installing them into backend-only plugins
## 1.0.4
### Fixed
+1
View File
@@ -28,6 +28,7 @@ npm install --save-dev @termix-ssh/plugin-sdk
```bash
npx termix-plugin build # bundle dist/backend.js and dist/frontend.js
npx termix-plugin patch # apply the plugin's dependency patches (build does this too)
npx termix-plugin test # run the plugin's Vitest suite
npx termix-plugin validate # check manifest.json and the files it names
npx termix-plugin openapi # write dist/openapi.json from @openapi comments
+7 -7
View File
@@ -108,13 +108,6 @@ const HOST_DIR = path.join(
const THEME_CSS = path.join(HOST_DIR, "theme.css");
const CORE_CLASSES = path.join(HOST_DIR, "core-classes.json");
/**
* Compiles the Tailwind classes the plugin's frontend uses into
* dist/frontend.css, against the theme core ships in the SDK. Core only
* compiles its own classes, so a class no core file happens to use would
* otherwise have no CSS at all. Appended to whatever CSS the bundle already
* produced (a library's stylesheet the plugin imports).
*/
/**
* Moves a plugin's Tailwind layers under their own names. Core orders
* termix-plugin-utilities above its base styles and below its own utilities,
@@ -136,6 +129,13 @@ function readCoreClasses() {
return new Set(JSON.parse(fs.readFileSync(CORE_CLASSES, "utf8")));
}
/**
* Compiles the Tailwind classes the plugin's frontend uses into
* dist/frontend.css, against the theme core ships in the SDK. Core only
* compiles its own classes, so a class no core file happens to use would
* otherwise have no CSS at all. Appended to whatever CSS the bundle already
* produced (a library's stylesheet the plugin imports).
*/
export async function buildTailwind(cwd, outDir) {
const { compile, optimize } = await import("@tailwindcss/node");
const { Scanner } = await import("@tailwindcss/oxide");
@@ -17,7 +17,7 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import * as esbuild from "esbuild";
import { BACKEND_EXTERNALS } from "../lib/externals.mjs";
import { readManifest, resolveEntry } from "../lib/plugin-dir.mjs";
@@ -53,7 +53,7 @@ async function loadDefinitions(cwd) {
external: BACKEND_EXTERNALS,
});
const module = await import(`file://${outfile.split(path.sep).join("/")}`);
const module = await import(pathToFileURL(outfile).href);
const tables = module.tables ?? module.default;
if (!Array.isArray(tables)) {
throw new Error(
@@ -218,6 +218,11 @@ async function validateMigrations(cwd, pluginId) {
const present = DIALECTS.filter((dialect) =>
fs.existsSync(path.join(root, dialect)),
);
for (const dialect of DIALECTS) {
if (present.length > 0 && !present.includes(dialect)) {
problems.push(`migrations/${dialect} is missing`);
}
}
const byDialect = new Map();
for (const dialect of present) {
+13 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@termix-ssh/plugin-sdk",
"version": "1.0.4",
"version": "1.0.5",
"description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
"license": "Apache-2.0",
"repository": {
@@ -148,6 +148,18 @@
},
"@testing-library/react": {
"optional": true
},
"i18next": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-i18next": {
"optional": true
},
"sonner": {
"optional": true
}
},
"publishConfig": {
+1 -1
View File
@@ -32,7 +32,7 @@ export function execCommand(
if (!settled) {
settled = true;
cleanup();
reject(new Error(`Command timeout after ${timeoutMs}ms: ${command}`));
reject(new Error(`Command timeout after ${timeoutMs}ms`));
}
}, timeoutMs);
+51
View File
@@ -0,0 +1,51 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { validate } from "../packages/plugin-sdk/cli/commands/validate.mjs";
const roots: string[] = [];
afterEach(() => {
vi.restoreAllMocks();
for (const root of roots.splice(0))
fs.rmSync(root, { recursive: true, force: true });
});
function plugin(dialects: string[]) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "termix-validate-"));
roots.push(root);
fs.writeFileSync(
path.join(root, "manifest.json"),
JSON.stringify({ id: "fixture", version: "1.0.0" }),
);
for (const dialect of dialects) {
const dir = path.join(root, "migrations", dialect);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(
path.join(dir, "0001_init.sql"),
"CREATE TABLE p_fixture_items (id INTEGER PRIMARY KEY);\n",
);
}
return root;
}
async function problems(cwd: string) {
const lines: string[] = [];
vi.spyOn(console, "error").mockImplementation((line) => lines.push(line));
vi.spyOn(console, "warn").mockImplementation(() => {});
await expect(validate({ cwd })).rejects.toThrow();
return lines.join("\n");
}
describe("termix-plugin validate migrations", () => {
it("flags an engine with no migrations folder", async () => {
const output = await problems(plugin(["sqlite", "postgres"]));
expect(output).toContain("migrations/mysql is missing");
expect(output).not.toContain("migrations/postgres is missing");
});
it("accepts all three engines", async () => {
const output = await problems(plugin(["sqlite", "postgres", "mysql"]));
expect(output).not.toMatch(/migrations\/\w+ is missing/);
});
});
@@ -6,6 +6,7 @@ import {
isValidPackageName,
buildSudoCommand,
parseUpgradable,
execCommand,
} from "@termix-ssh/plugin-sdk/host-commands";
describe("parsePlatformProbe", () => {
@@ -114,3 +115,13 @@ describe("parseUpgradable", () => {
expect(parseUpgradable(null, "anything")).toEqual([]);
});
});
describe("execCommand", () => {
it("leaves the command out of the timeout error", async () => {
const client = { exec: () => {} } as never;
const command = buildSudoCommand("apt update", "hunter2");
await expect(execCommand(client, command, 5)).rejects.toThrow(
/^Command timeout after 5ms$/,
);
});
});