@termix-ssh/plugin-sdk 1.0.8 → 1.0.10

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.10
4
+
5
+ ### Changed
6
+
7
+ - `termix-plugin pack` includes the plugin's `LICENSE` and `NOTICE` files, so third-party notices ship with it
8
+
9
+ ## 1.0.9
10
+
11
+ ### Added
12
+
13
+ - Worker entries in `package.json` `termix.workers`, each bundled on its own into `dist/workers/<name>.js`
14
+ - A `.wasm`, `.ttf` or `.otf` import gives the file's bytes, in `termix-plugin build` and in plugin tests
15
+
16
+ ### Fixed
17
+
18
+ - The plugin guide listed jszip, guacamole-lite and @anthropic-ai/sdk as shared packages, but they are bundled
19
+
3
20
  ## 1.0.8
4
21
 
5
22
  ### Added
package/PLUGIN-GUIDE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Termix plugin guide
2
2
 
3
- SDK 1.0.8 | Plugin API 1.2.0
3
+ SDK 1.0.10 | 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.
@@ -90,7 +90,7 @@ npm test # termix-plugin test: Vitest
90
90
  npm run typecheck # tsc
91
91
  npm run validate # termix-plugin validate: manifest, files, migrations, changelog, docs
92
92
  npm run migrations # termix-plugin migrations: write migrations after changing tables.ts
93
- npm run pack # termix-plugin pack: write <id>-<version>.tmxplug
93
+ npm run pack # termix-plugin pack: write <id>-<version>.tmxplug (with LICENSE and NOTICE)
94
94
  npm run format # Prettier
95
95
  npm run format:check
96
96
  ```
@@ -334,8 +334,27 @@ ctx.schedule.every(60_000, () => refresh(), { jitterMs: 5_000 });
334
334
  ### Shared packages
335
335
 
336
336
  These come from Termix and are never bundled: `@termix-ssh/plugin-sdk`, `express`, `ssh2`, `ws`, `multer`,
337
- `cookie-parser`, `axios`, `jszip`, `guacamole-lite`, `@anthropic-ai/sdk`, `drizzle-orm`, `sharp` and every
338
- Node builtin. List the ones you use in `peerDependencies`. Any other npm package is bundled.
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.
339
358
 
340
359
  ## Database
341
360
 
@@ -683,20 +702,20 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
683
702
 
684
703
  ## termix-plugin CLI
685
704
 
686
- | Command | What it does |
687
- | ------------------------------ | ---------------------------------------------------------------------------------------------- |
688
- | `build` | Bundle `dist/backend.js` and `dist/frontend.js`, CSS, locales, migrations, `dist/openapi.json` |
689
- | `validate` | Check the manifest, its files, migrations for every database, the changelog and `package.json` |
690
- | `test` | Apply patches, run Vitest |
691
- | `migrations` | Write a migration per database from `tables.ts`. `--check` fails when one is missing |
692
- | `openapi` | Write `dist/openapi.json` from `@openapi` blocks |
693
- | `pack` | Validate, then write `<id>-<version>.tmxplug`. `--out <dir>` |
694
- | `sign <file>` | Sign a `.tmxplug` with `TERMIX_PLUGIN_SIGNING_KEY` |
695
- | `verify <file> --key <base64>` | Check a `.tmxplug` against its `.sig` |
696
- | `keygen` | Write a new signing key pair. `--out <dir>` |
697
- | `changelog` | Print one version's notes. `--version x.y.z` |
698
- | `patch` | Apply the plugin's dependency patches (`build` and `test` do this too) |
699
- | `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` |
700
719
 
701
720
  ## SDK entry points
702
721
 
@@ -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(
@@ -11,6 +11,9 @@ export const TMXPLUG_CONTENTS = [
11
11
  "README.md",
12
12
  "CHANGELOG.md",
13
13
  "icon.svg",
14
+ // Licence texts, so third-party notices travel with the plugin.
15
+ "LICENSE",
16
+ "NOTICE",
14
17
  ];
15
18
 
16
19
  const BLOCK = 512;
@@ -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
  };
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termix-ssh/plugin-sdk",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {