@termix-ssh/plugin-sdk 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/PLUGIN-GUIDE.md +38 -18
- package/cli/commands/build.mjs +54 -3
- package/dist/backend.d.ts +7 -0
- package/dist/host/{chunk-LKN6OILD.js → chunk-G346FUE4.js} +3 -1
- package/dist/host/testing-host.js +1 -1
- package/dist/host/ui.js +1 -1
- package/dist/vitest-preset.d.ts +24 -0
- package/dist/vitest-preset.js +22 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.9
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Worker entries in `package.json` `termix.workers`, each bundled on its own into `dist/workers/<name>.js`
|
|
8
|
+
- A `.wasm`, `.ttf` or `.otf` import gives the file's bytes, in `termix-plugin build` and in plugin tests
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- The plugin guide listed jszip, guacamole-lite and @anthropic-ai/sdk as shared packages, but they are bundled
|
|
13
|
+
|
|
14
|
+
## 1.0.8
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `publicRateLimit` on `ctx.http.router()`, for a plugin whose public routes serve a proxied page
|
|
19
|
+
|
|
3
20
|
## 1.0.7
|
|
4
21
|
|
|
5
22
|
### Fixed
|
package/PLUGIN-GUIDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Termix plugin guide
|
|
2
2
|
|
|
3
|
-
SDK 1.0.
|
|
3
|
+
SDK 1.0.9 | Plugin API 1.2.0
|
|
4
4
|
|
|
5
5
|
This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
|
|
6
6
|
`@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
|
|
@@ -283,7 +283,8 @@ router.post("/notes", async (req, res) => {
|
|
|
283
283
|
- The `@openapi` path is the full path, starting with `/plugin-api/<id>/`. `build` turns these into
|
|
284
284
|
`dist/openapi.json` and the docs site renders them.
|
|
285
285
|
- `ctx.http.router({ public: ["/webhook/:token"], bodyLimit: "20mb", rawBody: false })`. Public routes
|
|
286
|
-
have no actor: check their token yourself
|
|
286
|
+
have no actor: check their token yourself. Core allows 120 public requests a minute per address;
|
|
287
|
+
`publicRateLimit` raises that for a proxied page that loads its own assets.
|
|
287
288
|
- `ctx.http.baseUrl(req)` is the public address, for callback URLs.
|
|
288
289
|
- `ctx.ws.route("/live", (socket, conn) => ...)`. Long lived sockets should check
|
|
289
290
|
`conn.isDataUnlocked()` per message.
|
|
@@ -333,8 +334,27 @@ ctx.schedule.every(60_000, () => refresh(), { jitterMs: 5_000 });
|
|
|
333
334
|
### Shared packages
|
|
334
335
|
|
|
335
336
|
These come from Termix and are never bundled: `@termix-ssh/plugin-sdk`, `express`, `ssh2`, `ws`, `multer`,
|
|
336
|
-
`cookie-parser`, `axios`, `
|
|
337
|
-
|
|
337
|
+
`cookie-parser`, `axios`, `drizzle-orm`, `sharp` and every Node builtin. List the ones you use in
|
|
338
|
+
`peerDependencies`. Any other npm package is bundled.
|
|
339
|
+
|
|
340
|
+
### Workers and WebAssembly
|
|
341
|
+
|
|
342
|
+
Heavy work (decoding, encoding, parsing) belongs in a worker thread so it never blocks the server. Name
|
|
343
|
+
each worker entry in `package.json`:
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
"termix": { "workers": { "engine": "src/backend/engine/worker.ts" } }
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`build` bundles it alone into `dist/workers/engine.js`; start it with
|
|
350
|
+
`new Worker(new URL("./workers/engine.js", import.meta.url))`. A worker bundles everything it imports and
|
|
351
|
+
can't use the shared packages above, so it must not import `@termix-ssh/plugin-sdk`. Talk to it with
|
|
352
|
+
`postMessage`.
|
|
353
|
+
|
|
354
|
+
Importing a `.wasm`, `.ttf` or `.otf` file gives its bytes as a `Uint8Array`, in `build` and in tests, so a module ships
|
|
355
|
+
inside the bundle with no file to find. Compile it once on the main thread with `new WebAssembly.Module`
|
|
356
|
+
and post that to each worker. Add `declare module "*.wasm" { const bytes: Uint8Array; export default bytes; }`
|
|
357
|
+
to a `.d.ts` in the plugin.
|
|
338
358
|
|
|
339
359
|
## Database
|
|
340
360
|
|
|
@@ -682,20 +702,20 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
|
|
|
682
702
|
|
|
683
703
|
## termix-plugin CLI
|
|
684
704
|
|
|
685
|
-
| Command | What it does
|
|
686
|
-
| ------------------------------ |
|
|
687
|
-
| `build` | Bundle `dist/backend.js
|
|
688
|
-
| `validate` | Check the manifest, its files, migrations for every database, the changelog and `package.json`
|
|
689
|
-
| `test` | Apply patches, run Vitest
|
|
690
|
-
| `migrations` | Write a migration per database from `tables.ts`. `--check` fails when one is missing
|
|
691
|
-
| `openapi` | Write `dist/openapi.json` from `@openapi` blocks
|
|
692
|
-
| `pack` | Validate, then write `<id>-<version>.tmxplug`. `--out <dir>`
|
|
693
|
-
| `sign <file>` | Sign a `.tmxplug` with `TERMIX_PLUGIN_SIGNING_KEY`
|
|
694
|
-
| `verify <file> --key <base64>` | Check a `.tmxplug` against its `.sig`
|
|
695
|
-
| `keygen` | Write a new signing key pair. `--out <dir>`
|
|
696
|
-
| `changelog` | Print one version's notes. `--version x.y.z`
|
|
697
|
-
| `patch` | Apply the plugin's dependency patches (`build` and `test` do this too)
|
|
698
|
-
| `dev` | Build, pack and install on a server, again on every change. `--server`, `--key`, `--once`
|
|
705
|
+
| Command | What it does |
|
|
706
|
+
| ------------------------------ | ----------------------------------------------------------------------------------------------- |
|
|
707
|
+
| `build` | Bundle `dist/backend.js`, `dist/frontend.js`, workers, CSS, locales, migrations, `openapi.json` |
|
|
708
|
+
| `validate` | Check the manifest, its files, migrations for every database, the changelog and `package.json` |
|
|
709
|
+
| `test` | Apply patches, run Vitest |
|
|
710
|
+
| `migrations` | Write a migration per database from `tables.ts`. `--check` fails when one is missing |
|
|
711
|
+
| `openapi` | Write `dist/openapi.json` from `@openapi` blocks |
|
|
712
|
+
| `pack` | Validate, then write `<id>-<version>.tmxplug`. `--out <dir>` |
|
|
713
|
+
| `sign <file>` | Sign a `.tmxplug` with `TERMIX_PLUGIN_SIGNING_KEY` |
|
|
714
|
+
| `verify <file> --key <base64>` | Check a `.tmxplug` against its `.sig` |
|
|
715
|
+
| `keygen` | Write a new signing key pair. `--out <dir>` |
|
|
716
|
+
| `changelog` | Print one version's notes. `--version x.y.z` |
|
|
717
|
+
| `patch` | Apply the plugin's dependency patches (`build` and `test` do this too) |
|
|
718
|
+
| `dev` | Build, pack and install on a server, again on every change. `--server`, `--key`, `--once` |
|
|
699
719
|
|
|
700
720
|
## SDK entry points
|
|
701
721
|
|
package/cli/commands/build.mjs
CHANGED
|
@@ -52,6 +52,43 @@ export function externalRequireInterop(externals) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// A .wasm or font import becomes its bytes (a Uint8Array) inside the bundle,
|
|
56
|
+
// so it ships in backend.js or a worker with no separate file to find.
|
|
57
|
+
export const BINARY_EXTENSIONS = [".wasm", ".ttf", ".otf"];
|
|
58
|
+
const BINARY_LOADERS = Object.fromEntries(
|
|
59
|
+
BINARY_EXTENSIONS.map((ext) => [ext, "binary"]),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const REQUIRE_BANNER =
|
|
63
|
+
"import { createRequire as __termixCreateRequire } from 'node:module'; const require = __termixCreateRequire(import.meta.url);";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* package.json "termix": { "workers": { "engine": "src/backend/engine/worker.ts" } }
|
|
67
|
+
* bundles each entry on its own into dist/workers/<name>.js, for
|
|
68
|
+
* new Worker(new URL("./workers/<name>.js", import.meta.url)). Nothing is left
|
|
69
|
+
* external: core's import fallback only runs on the main thread, so a worker
|
|
70
|
+
* can't rely on host packages and must not import the SDK.
|
|
71
|
+
*/
|
|
72
|
+
export function readWorkerEntries(cwd, pluginId) {
|
|
73
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
74
|
+
const pkg = fs.existsSync(pkgPath)
|
|
75
|
+
? JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
|
76
|
+
: {};
|
|
77
|
+
const workers = pkg.termix?.workers ?? {};
|
|
78
|
+
return Object.entries(workers).map(([name, entry]) => {
|
|
79
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
80
|
+
throw new Error(`${pluginId}: worker name "${name}" must be lowercase`);
|
|
81
|
+
}
|
|
82
|
+
const file = path.resolve(cwd, String(entry));
|
|
83
|
+
if (!fs.existsSync(file)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`${pluginId}: worker "${name}" entry ${entry} is missing`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return { name, file };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
const SDK_PATCHES = path.join(
|
|
56
93
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
57
94
|
"..",
|
|
@@ -216,13 +253,27 @@ export async function build({ cwd }) {
|
|
|
216
253
|
// node_modules import instead, resolved at runtime like a host-provided
|
|
217
254
|
// package.
|
|
218
255
|
external: [...BACKEND_EXTERNALS, ...(manifest.nativeDependencies ?? [])],
|
|
256
|
+
loader: BINARY_LOADERS,
|
|
219
257
|
// Bundled CJS deps still call require() for Node builtins at runtime.
|
|
220
258
|
// ESM has no ambient require, so give esbuild's require shim a real one.
|
|
221
|
-
banner: {
|
|
222
|
-
js: "import { createRequire as __termixCreateRequire } from 'node:module'; const require = __termixCreateRequire(import.meta.url);",
|
|
223
|
-
},
|
|
259
|
+
banner: { js: REQUIRE_BANNER },
|
|
224
260
|
});
|
|
225
261
|
|
|
262
|
+
for (const worker of readWorkerEntries(cwd, pluginId)) {
|
|
263
|
+
await esbuild.build({
|
|
264
|
+
entryPoints: [worker.file],
|
|
265
|
+
outfile: path.join(outDir, "workers", `${worker.name}.js`),
|
|
266
|
+
bundle: true,
|
|
267
|
+
format: "esm",
|
|
268
|
+
platform: "node",
|
|
269
|
+
target: "node22",
|
|
270
|
+
sourcemap: true,
|
|
271
|
+
logLevel: "warning",
|
|
272
|
+
loader: BINARY_LOADERS,
|
|
273
|
+
banner: { js: REQUIRE_BANNER },
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
226
277
|
// Without this Node finds the host's typeless package.json, tries backend.js
|
|
227
278
|
// as CommonJS first and warns before reparsing it as ESM.
|
|
228
279
|
fs.writeFileSync(
|
package/dist/backend.d.ts
CHANGED
|
@@ -303,6 +303,13 @@ export interface PluginRouterOptions {
|
|
|
303
303
|
* (multer, a raw body for signature checking, a proxied stream).
|
|
304
304
|
*/
|
|
305
305
|
rawBody?: boolean;
|
|
306
|
+
/**
|
|
307
|
+
* Requests per minute one address may make to this plugin's public routes.
|
|
308
|
+
* Defaults to 120, which a proxied page loading its own scripts and images
|
|
309
|
+
* can pass on its own. Capped at 10000, and an admin's
|
|
310
|
+
* TERMIX_PLUGIN_PUBLIC_RATE_LIMIT wins over it.
|
|
311
|
+
*/
|
|
312
|
+
publicRateLimit?: number;
|
|
306
313
|
}
|
|
307
314
|
/**
|
|
308
315
|
* HTTP routes, served at /plugin-api/<id>/ by the main backend server.
|
|
@@ -52016,6 +52016,8 @@ var en_default = {
|
|
|
52016
52016
|
pinAppRailDesc: "Keep the left sidebar app rail always expanded instead of expanding on hover",
|
|
52017
52017
|
expandAppRailOnHover: "Expand App Rail on Hover",
|
|
52018
52018
|
expandAppRailOnHoverDesc: "Allow the left sidebar app rail to expand when the pointer moves over it",
|
|
52019
|
+
appRailHoverPushesContent: "Push content when it expands",
|
|
52020
|
+
appRailHoverPushesContentDesc: "Make room for the expanded rail instead of opening it over the page. Open sessions, like remote desktop, resize each time",
|
|
52019
52021
|
showPinAppRailButton: "Show Pin App Rail Button",
|
|
52020
52022
|
showPinAppRailButtonDesc: "Show a button in the app rail to pin it open. Off by default.",
|
|
52021
52023
|
settingsNavigation: "Navigation",
|
|
@@ -53584,7 +53586,7 @@ async function pluginWsUrl(pluginId, path2, options2 = {}) {
|
|
|
53584
53586
|
}
|
|
53585
53587
|
const token = await fetchSocketTicket() ?? localStorage.getItem("jwt");
|
|
53586
53588
|
const protocols2 = websocketAuthProtocols(token);
|
|
53587
|
-
const devProxy = !define_import_meta_env_default.VITE_API_HOST
|
|
53589
|
+
const devProxy = !define_import_meta_env_default.VITE_API_HOST;
|
|
53588
53590
|
const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
|
|
53589
53591
|
if (devProxy) {
|
|
53590
53592
|
return {
|
package/dist/host/ui.js
CHANGED
package/dist/vitest-preset.d.ts
CHANGED
|
@@ -8,11 +8,25 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Two projects: tests/backend runs in node, tests/frontend in jsdom.
|
|
10
10
|
*/
|
|
11
|
+
/**
|
|
12
|
+
* termix-plugin build turns a .wasm or font import into its bytes. Tests get the same,
|
|
13
|
+
* so code that imports a module compiles it the same way under vitest.
|
|
14
|
+
*/
|
|
15
|
+
export declare function wasmBinaryPlugin(): {
|
|
16
|
+
name: string;
|
|
17
|
+
enforce: "pre";
|
|
18
|
+
load(id: string): string;
|
|
19
|
+
};
|
|
11
20
|
export interface PluginVitestOptions {
|
|
12
21
|
/** Extra aliases merged over the defaults. */
|
|
13
22
|
alias?: Record<string, string>;
|
|
14
23
|
}
|
|
15
24
|
export declare function pluginVitestConfig(configUrl: string, options?: PluginVitestOptions): {
|
|
25
|
+
plugins: {
|
|
26
|
+
name: string;
|
|
27
|
+
enforce: "pre";
|
|
28
|
+
load(id: string): string;
|
|
29
|
+
}[];
|
|
16
30
|
resolve: {
|
|
17
31
|
alias: Record<string, string>;
|
|
18
32
|
};
|
|
@@ -21,6 +35,11 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
|
|
|
21
35
|
setupFiles: string[];
|
|
22
36
|
pool: "threads";
|
|
23
37
|
projects: ({
|
|
38
|
+
plugins: {
|
|
39
|
+
name: string;
|
|
40
|
+
enforce: "pre";
|
|
41
|
+
load(id: string): string;
|
|
42
|
+
}[];
|
|
24
43
|
resolve: {
|
|
25
44
|
alias: Record<string, string>;
|
|
26
45
|
};
|
|
@@ -35,6 +54,11 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
|
|
|
35
54
|
server?: undefined;
|
|
36
55
|
};
|
|
37
56
|
} | {
|
|
57
|
+
plugins: {
|
|
58
|
+
name: string;
|
|
59
|
+
enforce: "pre";
|
|
60
|
+
load(id: string): string;
|
|
61
|
+
}[];
|
|
38
62
|
resolve: {
|
|
39
63
|
alias: Record<string, string>;
|
|
40
64
|
};
|
package/dist/vitest-preset.js
CHANGED
|
@@ -29,6 +29,24 @@ function findRepoRoot(startDir) {
|
|
|
29
29
|
dir = parent;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* termix-plugin build turns a .wasm or font import into its bytes. Tests get the same,
|
|
34
|
+
* so code that imports a module compiles it the same way under vitest.
|
|
35
|
+
*/
|
|
36
|
+
export function wasmBinaryPlugin() {
|
|
37
|
+
return {
|
|
38
|
+
name: "termix-wasm-binary",
|
|
39
|
+
enforce: "pre",
|
|
40
|
+
load(id) {
|
|
41
|
+
const file = id.split("?")[0];
|
|
42
|
+
if (![".wasm", ".ttf", ".otf"].some((ext) => file.endsWith(ext))) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const base64 = fs.readFileSync(file).toString("base64");
|
|
46
|
+
return `export default new Uint8Array(Buffer.from(${JSON.stringify(base64)}, "base64"));`;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
32
50
|
export function pluginVitestConfig(configUrl, options = {}) {
|
|
33
51
|
const pluginDir = path.dirname(fileURLToPath(configUrl));
|
|
34
52
|
const manifestPath = path.join(pluginDir, "manifest.json");
|
|
@@ -61,7 +79,9 @@ export function pluginVitestConfig(configUrl, options = {}) {
|
|
|
61
79
|
// Forks workers on Windows abort now and then with "Worker exited
|
|
62
80
|
// unexpectedly". Threads don't.
|
|
63
81
|
const pool = "threads";
|
|
82
|
+
const plugins = [wasmBinaryPlugin()];
|
|
64
83
|
return {
|
|
84
|
+
plugins,
|
|
65
85
|
resolve: { alias },
|
|
66
86
|
test: {
|
|
67
87
|
globals: true,
|
|
@@ -69,6 +89,7 @@ export function pluginVitestConfig(configUrl, options = {}) {
|
|
|
69
89
|
pool,
|
|
70
90
|
projects: [
|
|
71
91
|
{
|
|
92
|
+
plugins,
|
|
72
93
|
resolve: { alias },
|
|
73
94
|
test: {
|
|
74
95
|
name: `${pluginId}:backend`,
|
|
@@ -81,6 +102,7 @@ export function pluginVitestConfig(configUrl, options = {}) {
|
|
|
81
102
|
},
|
|
82
103
|
},
|
|
83
104
|
{
|
|
105
|
+
plugins,
|
|
84
106
|
resolve: { alias },
|
|
85
107
|
test: {
|
|
86
108
|
name: `${pluginId}:frontend`,
|
package/package.json
CHANGED