mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
fix: plugin sdk 1.0.5 with optional ui peers, safer timeouts and validate checks
This commit is contained in:
1 parent
af09aa1028
commit
b0a0ee3e00
10 files changed
+113
-12
No files matched your search
Generated
+13
-1
@@ -15015,7 +15015,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin-sdk": {
|
"packages/plugin-sdk": {
|
||||||
"name": "@termix-ssh/plugin-sdk",
|
"name": "@termix-ssh/plugin-sdk",
|
||||||
"version": "1.1.0",
|
"version": "1.0.5",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@deadendjs/swagger-jsdoc": "^8.1.2",
|
"@deadendjs/swagger-jsdoc": "^8.1.2",
|
||||||
@@ -15054,9 +15054,21 @@
|
|||||||
"drizzle-orm": {
|
"drizzle-orm": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"i18next": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"react": {
|
"react": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-i18next": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sonner": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"ssh2": {
|
"ssh2": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# 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
|
## 1.0.4
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ npm install --save-dev @termix-ssh/plugin-sdk
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx termix-plugin build # bundle dist/backend.js and dist/frontend.js
|
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 test # run the plugin's Vitest suite
|
||||||
npx termix-plugin validate # check manifest.json and the files it names
|
npx termix-plugin validate # check manifest.json and the files it names
|
||||||
npx termix-plugin openapi # write dist/openapi.json from @openapi comments
|
npx termix-plugin openapi # write dist/openapi.json from @openapi comments
|
||||||
|
|||||||
@@ -108,13 +108,6 @@ const HOST_DIR = path.join(
|
|||||||
const THEME_CSS = path.join(HOST_DIR, "theme.css");
|
const THEME_CSS = path.join(HOST_DIR, "theme.css");
|
||||||
const CORE_CLASSES = path.join(HOST_DIR, "core-classes.json");
|
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
|
* Moves a plugin's Tailwind layers under their own names. Core orders
|
||||||
* termix-plugin-utilities above its base styles and below its own utilities,
|
* 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")));
|
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) {
|
export async function buildTailwind(cwd, outDir) {
|
||||||
const { compile, optimize } = await import("@tailwindcss/node");
|
const { compile, optimize } = await import("@tailwindcss/node");
|
||||||
const { Scanner } = await import("@tailwindcss/oxide");
|
const { Scanner } = await import("@tailwindcss/oxide");
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
import * as esbuild from "esbuild";
|
import * as esbuild from "esbuild";
|
||||||
import { BACKEND_EXTERNALS } from "../lib/externals.mjs";
|
import { BACKEND_EXTERNALS } from "../lib/externals.mjs";
|
||||||
import { readManifest, resolveEntry } from "../lib/plugin-dir.mjs";
|
import { readManifest, resolveEntry } from "../lib/plugin-dir.mjs";
|
||||||
@@ -53,7 +53,7 @@ async function loadDefinitions(cwd) {
|
|||||||
external: BACKEND_EXTERNALS,
|
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;
|
const tables = module.tables ?? module.default;
|
||||||
if (!Array.isArray(tables)) {
|
if (!Array.isArray(tables)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -218,6 +218,11 @@ async function validateMigrations(cwd, pluginId) {
|
|||||||
const present = DIALECTS.filter((dialect) =>
|
const present = DIALECTS.filter((dialect) =>
|
||||||
fs.existsSync(path.join(root, 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();
|
const byDialect = new Map();
|
||||||
for (const dialect of present) {
|
for (const dialect of present) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@termix-ssh/plugin-sdk",
|
"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.",
|
"description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -148,6 +148,18 @@
|
|||||||
},
|
},
|
||||||
"@testing-library/react": {
|
"@testing-library/react": {
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"i18next": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-i18next": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sonner": {
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function execCommand(
|
|||||||
if (!settled) {
|
if (!settled) {
|
||||||
settled = true;
|
settled = true;
|
||||||
cleanup();
|
cleanup();
|
||||||
reject(new Error(`Command timeout after ${timeoutMs}ms: ${command}`));
|
reject(new Error(`Command timeout after ${timeoutMs}ms`));
|
||||||
}
|
}
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
isValidPackageName,
|
||||||
buildSudoCommand,
|
buildSudoCommand,
|
||||||
parseUpgradable,
|
parseUpgradable,
|
||||||
|
execCommand,
|
||||||
} from "@termix-ssh/plugin-sdk/host-commands";
|
} from "@termix-ssh/plugin-sdk/host-commands";
|
||||||
|
|
||||||
describe("parsePlatformProbe", () => {
|
describe("parsePlatformProbe", () => {
|
||||||
@@ -114,3 +115,13 @@ describe("parseUpgradable", () => {
|
|||||||
expect(parseUpgradable(null, "anything")).toEqual([]);
|
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$/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user