@snaptrude/plugin-client 0.0.0-dev-20260708130115
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/AGENTS.md +86 -0
- package/CHANGELOG.md +106 -0
- package/CLAUDE.md +11 -0
- package/dist/api/index.d.ts +18 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/host-api.d.ts +11 -0
- package/dist/host-api.d.ts.map +1 -0
- package/dist/index.cjs +178 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +139 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin-worker.d.ts +54 -0
- package/dist/plugin-worker.d.ts.map +1 -0
- package/dist/rpc-proxy.d.ts +28 -0
- package/dist/rpc-proxy.d.ts.map +1 -0
- package/package.json +33 -0
- package/src/api/index.ts +42 -0
- package/src/host-api.ts +47 -0
- package/src/index.ts +12 -0
- package/src/plugin-worker.ts +99 -0
- package/src/rpc-proxy.ts +56 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +12 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Plugin Client — Agent Guide
|
|
2
|
+
|
|
3
|
+
This package implements the plugin-core API for the **worker side** (plugin runtime).
|
|
4
|
+
Plugins import `snaptrude` from this package to interact with the Snaptrude host.
|
|
5
|
+
|
|
6
|
+
## Relationship to plugin-core
|
|
7
|
+
|
|
8
|
+
> Source of truth: [`packages/plugin-core/AGENTS.md`](../plugin-core/AGENTS.md)
|
|
9
|
+
|
|
10
|
+
This package implements the API surface defined in `@snaptrude/plugin-core`. Only the
|
|
11
|
+
root `PluginApi` class is extended; every namespace under it is satisfied structurally
|
|
12
|
+
by a generic RPC proxy (see below). When plugin-core changes its top-level shape,
|
|
13
|
+
this package must be updated to match.
|
|
14
|
+
|
|
15
|
+
## What this package contains
|
|
16
|
+
|
|
17
|
+
- `src/api/index.ts` — `ClientPluginApi`: wires one generic RPC proxy per top-level namespace
|
|
18
|
+
- `src/rpc-proxy.ts` — `createRpcNamespace()`: Proxy mapping dotted property paths to host RPC calls
|
|
19
|
+
- `src/host-api.ts` — Comlink RPC wrapper (`HostApi.call()`); unwraps success/error results
|
|
20
|
+
- `src/plugin-worker.ts` — `PluginWorker` base class (lifecycle, UI messaging, Comlink exposure)
|
|
21
|
+
- `src/index.ts` — Exports the `snaptrude` singleton (`ClientPluginApi` instance)
|
|
22
|
+
|
|
23
|
+
## Quick commands
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm check-types # typecheck (no emit)
|
|
27
|
+
pnpm build # tsup build (ESM + CJS)
|
|
28
|
+
pnpm dev # tsup watch mode
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Implementation patterns
|
|
32
|
+
|
|
33
|
+
### Everything is a generic RPC proxy (async, remote)
|
|
34
|
+
|
|
35
|
+
Under the all-handle model there is no in-worker compute: `core.math.*` and
|
|
36
|
+
`core.geom.*` values are opaque handles, so **every** namespace — `core.*`
|
|
37
|
+
(including `core.units`), `design.*`, `entity.*`, `tools.*` — is a
|
|
38
|
+
`createRpcNamespace<PluginXApi>("x")` Proxy. There are no hand-written
|
|
39
|
+
per-method wrappers. Nested property access builds the dot-separated method
|
|
40
|
+
path; the call marshals `{ method, args }` to the host via Comlink
|
|
41
|
+
(`getHostApi().call()`):
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// src/api/index.ts — one proxy per top-level namespace
|
|
45
|
+
this.core = createRpcNamespace<PluginCoreApi>("core")
|
|
46
|
+
|
|
47
|
+
// Plugin code — positional args (whole tuple crosses the wire), values are handles:
|
|
48
|
+
const v = await snaptrude.core.math.vec3.new(1, 2, 3)
|
|
49
|
+
await snaptrude.entity.space.createRectangular(position, dimensions)
|
|
50
|
+
// → getHostApi().call({ method: "entity.space.createRectangular", args: [position, dimensions] })
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The proxy is structurally cast to the abstract API type, so argument and return
|
|
54
|
+
types are enforced by plugin-core while dispatch stays dynamic. The `method`
|
|
55
|
+
string is exactly the property path and must match `PluginApiMethod`
|
|
56
|
+
(auto-inferred from the class hierarchy in `plugin-core/src/host-utils.ts`).
|
|
57
|
+
|
|
58
|
+
## When to update this package
|
|
59
|
+
|
|
60
|
+
- **plugin-core adds/removes/changes a method**: Usually nothing to do here — the
|
|
61
|
+
proxy dispatches dynamically and types flow from plugin-core. Rebuild plugin-core,
|
|
62
|
+
then run `pnpm check-types` here.
|
|
63
|
+
- **plugin-core adds a new top-level namespace on `PluginApi`**: Add a matching
|
|
64
|
+
`createRpcNamespace<PluginNewApi>("new")` property in `ClientPluginApi`
|
|
65
|
+
(`src/api/index.ts`). Nested modules under an existing namespace need no wiring.
|
|
66
|
+
|
|
67
|
+
After making changes, always run `pnpm check-types` to verify everything compiles.
|
|
68
|
+
|
|
69
|
+
## File mapping (plugin-core -> plugin-client)
|
|
70
|
+
|
|
71
|
+
There are no per-namespace files here anymore — the whole plugin-core API tree is
|
|
72
|
+
served by two files:
|
|
73
|
+
|
|
74
|
+
| plugin-core | plugin-client |
|
|
75
|
+
| --------------------------------------------- | -------------------------------------------------------------------- |
|
|
76
|
+
| `src/api/**` (all namespaces, e.g. `core.math.vec3`) | `src/api/index.ts` (`ClientPluginApi`, one `createRpcNamespace` each) |
|
|
77
|
+
| `src/host-utils.ts` (`PluginApiMethod` types) | `src/rpc-proxy.ts` + `src/host-api.ts` (dispatch + Comlink transport) |
|
|
78
|
+
|
|
79
|
+
## Downstream consumers
|
|
80
|
+
|
|
81
|
+
Changes to this package's **public exports** affect:
|
|
82
|
+
|
|
83
|
+
- Internal plugins under `plugins/`
|
|
84
|
+
- Example plugins under `examples/`
|
|
85
|
+
|
|
86
|
+
If you change or remove an export, search these directories for usage.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @snaptrude/plugin-client
|
|
2
|
+
|
|
3
|
+
## 0.0.0-dev-20260708130115
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [8f22cb7]
|
|
8
|
+
- @snaptrude/plugin-core@0.0.0-dev-20260708130115
|
|
9
|
+
|
|
10
|
+
## 0.5.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 7750aa3: Exposed more snaptrude APIs
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [7750aa3]
|
|
19
|
+
- @snaptrude/plugin-core@0.5.0
|
|
20
|
+
|
|
21
|
+
## 0.2.9
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 94842d0: added new space areaClass and innerProfiles APIs
|
|
26
|
+
- Updated dependencies [94842d0]
|
|
27
|
+
- Updated dependencies [ddf05fa]
|
|
28
|
+
- @snaptrude/plugin-core@0.2.9
|
|
29
|
+
|
|
30
|
+
## 0.2.8
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- 445e0b8: Add Present-mode AI Inspiration plugin APIs under `snaptrude.documentation.aiInspiration`.
|
|
35
|
+
- Updated dependencies [445e0b8]
|
|
36
|
+
- @snaptrude/plugin-core@0.2.8
|
|
37
|
+
|
|
38
|
+
## 0.2.7
|
|
39
|
+
|
|
40
|
+
### Patch Changes
|
|
41
|
+
|
|
42
|
+
- c5afd9a: added adjacecny
|
|
43
|
+
- Updated dependencies [c5afd9a]
|
|
44
|
+
- @snaptrude/plugin-core@0.2.7
|
|
45
|
+
|
|
46
|
+
## 0.2.6
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- c17d287: Add bulk space APIs: `space.bulkCreate`, `space.bulkUpdate`, and `space.bulkDelete`. Each batches its work into a single undo/redo step. All items are validated up front and the call throws if any item is invalid (nothing is applied), consistent with the single-item space methods. `bulkCreate` supports `spaceType`/`massType`/`departmentId` per item and returns `{ spaceIds }`; `bulkUpdate` takes geometry (`profile`+`extrudeHeight`) and/or nested `properties` per item and returns `{ spaces }` (one echoed result per item, like `update`); `bulkDelete` returns `{ deletedSpaceIds }`.
|
|
51
|
+
- 0683194: bulk apis
|
|
52
|
+
- Updated dependencies [c17d287]
|
|
53
|
+
- Updated dependencies [0683194]
|
|
54
|
+
- @snaptrude/plugin-core@0.2.6
|
|
55
|
+
|
|
56
|
+
## 0.2.5
|
|
57
|
+
|
|
58
|
+
### Patch Changes
|
|
59
|
+
|
|
60
|
+
- Restore declaration files in published plugin packages.
|
|
61
|
+
- Updated dependencies
|
|
62
|
+
- @snaptrude/plugin-core@0.2.5
|
|
63
|
+
|
|
64
|
+
## 0.2.4
|
|
65
|
+
|
|
66
|
+
### Patch Changes
|
|
67
|
+
|
|
68
|
+
- Add inner profile hole support to space creation from profiles.
|
|
69
|
+
|
|
70
|
+
- Updated dependencies
|
|
71
|
+
- @snaptrude/plugin-core@0.2.4
|
|
72
|
+
|
|
73
|
+
## 0.2.3
|
|
74
|
+
|
|
75
|
+
### Patch Changes
|
|
76
|
+
|
|
77
|
+
- 647466b: fixed merge issues
|
|
78
|
+
- Updated dependencies [647466b]
|
|
79
|
+
- @snaptrude/plugin-core@0.2.3
|
|
80
|
+
|
|
81
|
+
## 0.2.2
|
|
82
|
+
|
|
83
|
+
### Patch Changes
|
|
84
|
+
|
|
85
|
+
- 064777a: buildable envelope apis
|
|
86
|
+
- Updated dependencies [064777a]
|
|
87
|
+
- @snaptrude/plugin-core@0.2.2
|
|
88
|
+
|
|
89
|
+
## 0.2.1
|
|
90
|
+
|
|
91
|
+
### Patch Changes
|
|
92
|
+
|
|
93
|
+
- 3b37f4a: added buildable envelop api
|
|
94
|
+
- Updated dependencies [3b37f4a]
|
|
95
|
+
- @snaptrude/plugin-core@0.2.1
|
|
96
|
+
|
|
97
|
+
## 0.2.0
|
|
98
|
+
|
|
99
|
+
### Minor Changes
|
|
100
|
+
|
|
101
|
+
- 4d959a4: Exposed new offset and copy APIs for tools
|
|
102
|
+
|
|
103
|
+
### Patch Changes
|
|
104
|
+
|
|
105
|
+
- Updated dependencies [4d959a4]
|
|
106
|
+
- @snaptrude/plugin-core@0.2.0
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
> **All coding standards, checks, and guidance live in [`AGENTS.md`](AGENTS.md).**
|
|
4
|
+
> `CLAUDE.md` is a pointer only — add all new rules, patterns, and instructions to `AGENTS.md`.
|
|
5
|
+
|
|
6
|
+
See [AGENTS.md](AGENTS.md) for:
|
|
7
|
+
|
|
8
|
+
- Implementation pattern (generic RPC proxy — every namespace is async host RPC)
|
|
9
|
+
- File mapping from plugin-core to plugin-client
|
|
10
|
+
- When and how to update this package in response to plugin-core changes
|
|
11
|
+
- Downstream consumer notes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PluginApi, PluginCoreApi, PluginDesignApi, PluginEntityApi, PluginProgramApi, PluginPresentationApi } from "@snaptrude/plugin-core";
|
|
2
|
+
export declare class ClientPluginApi extends PluginApi {
|
|
3
|
+
private static instance;
|
|
4
|
+
/**
|
|
5
|
+
* Every namespace is fully remote under the all-handle model: math/geom now
|
|
6
|
+
* cross to the host (values are opaque handles), so there is no in-worker
|
|
7
|
+
* compute left. All dispatch through a single generic RPC Proxy. Units live
|
|
8
|
+
* under `core.units`, so they ride the `core` proxy.
|
|
9
|
+
*/
|
|
10
|
+
core: PluginCoreApi;
|
|
11
|
+
design: PluginDesignApi;
|
|
12
|
+
entity: PluginEntityApi;
|
|
13
|
+
program: PluginProgramApi;
|
|
14
|
+
presentation: PluginPresentationApi;
|
|
15
|
+
private constructor();
|
|
16
|
+
static getInstance(): ClientPluginApi;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,aAAa,EACb,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACtB,MAAM,wBAAwB,CAAA;AAG/B,qBAAa,eAAgB,SAAQ,SAAS;IAC5C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAiB;IAExC;;;;;OAKG;IACI,IAAI,EAAE,aAAa,CAAA;IACnB,MAAM,EAAE,eAAe,CAAA;IACvB,MAAM,EAAE,eAAe,CAAA;IACvB,OAAO,EAAE,gBAAgB,CAAA;IACzB,YAAY,EAAE,qBAAqB,CAAA;IAE1C,OAAO;IAUP,MAAM,CAAC,WAAW,IAAI,eAAe;CAMtC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Comlink from "comlink";
|
|
2
|
+
import type { PluginApiMethod, PluginApiCallPayload, PluginApiCallWrappedResult, PluginApiCallResult } from "@snaptrude/plugin-core";
|
|
3
|
+
export interface HostApi {
|
|
4
|
+
call<M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallWrappedResult<M>>;
|
|
5
|
+
}
|
|
6
|
+
export interface HostApiWrapped {
|
|
7
|
+
call<M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>>;
|
|
8
|
+
}
|
|
9
|
+
export declare function createHostApi(endpoint?: Comlink.Endpoint): HostApi;
|
|
10
|
+
export declare function getHostApi(): HostApiWrapped;
|
|
11
|
+
//# sourceMappingURL=host-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-api.d.ts","sourceRoot":"","sources":["../src/host-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAA;AAClC,OAAO,KAAK,EACV,eAAe,EACf,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,CAAC,SAAS,eAAe,EAC5B,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC/B,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAA;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,CAAC,SAAS,eAAe,EAC5B,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC/B,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAA;CACnC;AAED,wBAAgB,aAAa,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,OAAO,CAIlE;AAID,wBAAgB,UAAU,IAAI,cAAc,CAkB3C"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ClientPluginApi: () => ClientPluginApi,
|
|
34
|
+
PluginWorker: () => PluginWorker,
|
|
35
|
+
createHostApi: () => createHostApi,
|
|
36
|
+
getHostApi: () => getHostApi,
|
|
37
|
+
snaptrude: () => snaptrude
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(index_exports);
|
|
40
|
+
|
|
41
|
+
// src/api/index.ts
|
|
42
|
+
var import_plugin_core = require("@snaptrude/plugin-core");
|
|
43
|
+
|
|
44
|
+
// src/host-api.ts
|
|
45
|
+
var Comlink = __toESM(require("comlink"), 1);
|
|
46
|
+
function createHostApi(endpoint) {
|
|
47
|
+
return Comlink.wrap(
|
|
48
|
+
endpoint ?? globalThis
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
var _instance = null;
|
|
52
|
+
function getHostApi() {
|
|
53
|
+
if (!_instance) {
|
|
54
|
+
_instance = createHostApi();
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
call: async (payload) => {
|
|
58
|
+
if (!_instance) {
|
|
59
|
+
throw new Error("Host API not initialized");
|
|
60
|
+
}
|
|
61
|
+
return _instance.call(payload).then((result) => {
|
|
62
|
+
if (result.success) {
|
|
63
|
+
return result.data;
|
|
64
|
+
} else {
|
|
65
|
+
throw new Error(result.error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/rpc-proxy.ts
|
|
73
|
+
function createRpcNamespace(basePath) {
|
|
74
|
+
const build = (path) => new Proxy(NOOP, {
|
|
75
|
+
get(_target, prop) {
|
|
76
|
+
if (typeof prop !== "string" || prop === "then") return void 0;
|
|
77
|
+
return build(`${path}.${prop}`);
|
|
78
|
+
},
|
|
79
|
+
apply(_target, _thisArg, argArray) {
|
|
80
|
+
const payload = {
|
|
81
|
+
method: path,
|
|
82
|
+
args: argArray
|
|
83
|
+
};
|
|
84
|
+
return getHostApi().call(payload);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return build(basePath);
|
|
88
|
+
}
|
|
89
|
+
var NOOP = () => {
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// src/api/index.ts
|
|
93
|
+
var ClientPluginApi = class _ClientPluginApi extends import_plugin_core.PluginApi {
|
|
94
|
+
constructor() {
|
|
95
|
+
super();
|
|
96
|
+
this.core = createRpcNamespace("core");
|
|
97
|
+
this.design = createRpcNamespace("design");
|
|
98
|
+
this.entity = createRpcNamespace("entity");
|
|
99
|
+
this.program = createRpcNamespace("program");
|
|
100
|
+
this.presentation = createRpcNamespace("presentation");
|
|
101
|
+
}
|
|
102
|
+
static getInstance() {
|
|
103
|
+
if (!_ClientPluginApi.instance) {
|
|
104
|
+
_ClientPluginApi.instance = new _ClientPluginApi();
|
|
105
|
+
}
|
|
106
|
+
return _ClientPluginApi.instance;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// src/plugin-worker.ts
|
|
111
|
+
var Comlink2 = __toESM(require("comlink"), 1);
|
|
112
|
+
var PluginWorker = class {
|
|
113
|
+
constructor() {
|
|
114
|
+
this.hostAPI = Comlink2.wrap(
|
|
115
|
+
self
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
sendToUI(action, payload) {
|
|
119
|
+
;
|
|
120
|
+
this.hostAPI.ui.sendToUI({ action, payload });
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Signal the host that this plugin has finished its work and should be
|
|
124
|
+
* stopped. Use this in headless (UI-less) plugins that run a task and
|
|
125
|
+
* self-terminate.
|
|
126
|
+
*/
|
|
127
|
+
complete() {
|
|
128
|
+
;
|
|
129
|
+
this.hostAPI.lifecycle.complete();
|
|
130
|
+
}
|
|
131
|
+
async init() {
|
|
132
|
+
console.log(this.pluginId, "init() called");
|
|
133
|
+
console.log(this.pluginId, "Initialization complete");
|
|
134
|
+
}
|
|
135
|
+
async destroy() {
|
|
136
|
+
console.log(this.pluginId, "destroy() called \u2014 cleaning up");
|
|
137
|
+
}
|
|
138
|
+
async ping() {
|
|
139
|
+
return "pong";
|
|
140
|
+
}
|
|
141
|
+
async onUIMessage(_message) {
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Expose the worker API via Comlink and start listening.
|
|
145
|
+
* Call this once after constructing the plugin instance.
|
|
146
|
+
*
|
|
147
|
+
* The host calls `init(config)` with `{ pluginId }`,
|
|
148
|
+
* which is captured here to set `this.pluginId` before the
|
|
149
|
+
* subclass's `init()` runs.
|
|
150
|
+
*/
|
|
151
|
+
start() {
|
|
152
|
+
Comlink2.expose(
|
|
153
|
+
{
|
|
154
|
+
init: (config) => {
|
|
155
|
+
this.pluginId = config.pluginId;
|
|
156
|
+
return this.init();
|
|
157
|
+
},
|
|
158
|
+
destroy: () => this.destroy(),
|
|
159
|
+
ping: () => this.ping(),
|
|
160
|
+
onUIMessage: (message) => this.onUIMessage(message)
|
|
161
|
+
},
|
|
162
|
+
self
|
|
163
|
+
);
|
|
164
|
+
console.log("Worker loaded, API exposed via Comlink");
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// src/index.ts
|
|
169
|
+
var snaptrude = ClientPluginApi.getInstance();
|
|
170
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
171
|
+
0 && (module.exports = {
|
|
172
|
+
ClientPluginApi,
|
|
173
|
+
PluginWorker,
|
|
174
|
+
createHostApi,
|
|
175
|
+
getHostApi,
|
|
176
|
+
snaptrude
|
|
177
|
+
});
|
|
178
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/api/index.ts","../src/host-api.ts","../src/rpc-proxy.ts","../src/plugin-worker.ts"],"sourcesContent":["import { ClientPluginApi } from \"./api\"\n\nexport * from \"./api\"\nexport * from \"./host-api\"\nexport * from \"./plugin-worker\"\n\n/**\n * The Snaptrude plugin client API.\n *\n * The main entry point for plugins to interact with the Snaptrude platform.\n */\nexport const snaptrude = ClientPluginApi.getInstance()\n","import {\n PluginApi,\n PluginCoreApi,\n PluginDesignApi,\n PluginEntityApi,\n PluginProgramApi,\n PluginPresentationApi,\n} from \"@snaptrude/plugin-core\"\nimport { createRpcNamespace } from \"../rpc-proxy\"\n\nexport class ClientPluginApi extends PluginApi {\n private static instance: ClientPluginApi\n\n /**\n * Every namespace is fully remote under the all-handle model: math/geom now\n * cross to the host (values are opaque handles), so there is no in-worker\n * compute left. All dispatch through a single generic RPC Proxy. Units live\n * under `core.units`, so they ride the `core` proxy.\n */\n public core: PluginCoreApi\n public design: PluginDesignApi\n public entity: PluginEntityApi\n public program: PluginProgramApi\n public presentation: PluginPresentationApi\n\n private constructor() {\n super()\n this.core = createRpcNamespace<PluginCoreApi>(\"core\")\n this.design = createRpcNamespace<PluginDesignApi>(\"design\")\n this.entity = createRpcNamespace<PluginEntityApi>(\"entity\")\n this.program = createRpcNamespace<PluginProgramApi>(\"program\")\n this.presentation =\n createRpcNamespace<PluginPresentationApi>(\"presentation\")\n }\n\n static getInstance(): ClientPluginApi {\n if (!ClientPluginApi.instance) {\n ClientPluginApi.instance = new ClientPluginApi()\n }\n return ClientPluginApi.instance\n }\n}\n","import * as Comlink from \"comlink\"\nimport type {\n PluginApiMethod,\n PluginApiCallPayload,\n PluginApiCallWrappedResult,\n PluginApiCallResult,\n} from \"@snaptrude/plugin-core\"\n\nexport interface HostApi {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallWrappedResult<M>>\n}\n\nexport interface HostApiWrapped {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallResult<M>>\n}\n\nexport function createHostApi(endpoint?: Comlink.Endpoint): HostApi {\n return Comlink.wrap<HostApi>(\n endpoint ?? (globalThis as unknown as Comlink.Endpoint)\n ) as unknown as HostApi\n}\n\nlet _instance: HostApi | null = null\n\nexport function getHostApi(): HostApiWrapped {\n if (!_instance) {\n _instance = createHostApi()\n }\n return {\n call: async <M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>> => {\n if (!_instance) {\n throw new Error(\"Host API not initialized\")\n }\n return _instance.call(payload).then(result => {\n if (result.success) {\n return result.data\n } else {\n throw new Error(result.error)\n }\n })\n }\n }\n}\n","import type {\n PluginApiCallPayload,\n PluginApiMethod,\n} from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\n\n/**\n * Build a namespace object whose nested property access maps to a\n * dot-separated host RPC method path, and whose every call dispatches that\n * path through the host bridge.\n *\n * The host exposes the entire plugin API behind a single generic `call()`\n * (see the host `bridge.ts`), and the method string is exactly the property\n * path — so one Proxy replaces every hand-written per-method RPC wrapper for\n * every namespace (`core.*`, `design.*`, `entity.*`):\n *\n * The POSITIONAL transport forwards the whole argument tuple; the host router\n * spreads it back into the resolved method (`fn(...args)`):\n *\n * ```ts\n * snaptrude.core.math.vec3.new(1, 2, 3)\n * // → getHostApi().call({ method: \"core.math.vec3.new\", args: [1, 2, 3] })\n * ```\n *\n * Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>(\"entity\")`.\n * The Proxy is structurally cast to the abstract API type — argument and\n * return types are enforced by that type, while dispatch is dynamic.\n *\n * Every namespace uses this — including `core.math.*` and `core.geom.*`: under\n * the all-handle model there is no in-worker compute; math and geometry are\n * host calls like everything else.\n */\nexport function createRpcNamespace<T extends object>(basePath: string): T {\n const build = (path: string): unknown =>\n new Proxy(NOOP, {\n get(_target, prop) {\n // Symbols and `then` must not resolve to a callable proxy, otherwise\n // the namespace would look thenable and break Promise resolution if it\n // ever reached an `await`.\n if (typeof prop !== \"string\" || prop === \"then\") return undefined\n return build(`${path}.${prop}`)\n },\n apply(_target, _thisArg, argArray: unknown[]) {\n const payload = {\n method: path,\n args: argArray,\n } as unknown as PluginApiCallPayload<PluginApiMethod>\n return getHostApi().call(payload)\n },\n })\n\n return build(basePath) as T\n}\n\n/** Proxy target must be callable for the `apply` trap; identity is irrelevant. */\nconst NOOP = (): void => {}\n","import * as Comlink from \"comlink\"\n\nexport interface UIMessage {\n action: string\n payload: unknown\n}\n\ninterface PluginConfig {\n pluginId: string\n}\n\n/**\n * Base class for Snaptrude plugin workers.\n *\n * Handles Comlink wiring, host communication, and the standard lifecycle\n * methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and\n * override only the methods you need — then call `start()` to expose the\n * worker API.\n *\n * The plugin ID is received automatically from the host during\n * initialization — no need to pass it manually.\n *\n * @example\n * ```ts\n * import { PluginWorker } from \"@snaptrude/plugin-client\";\n *\n * class MyPlugin extends PluginWorker {\n * async onUIMessage(message: UIMessage) {\n * // handle messages from the UI panel\n * }\n * }\n *\n * new MyPlugin().start();\n * ```\n */\nexport abstract class PluginWorker {\n protected pluginId!: string\n private hostAPI: Comlink.Remote<Record<string, unknown>>\n\n constructor() {\n this.hostAPI = Comlink.wrap<Record<string, unknown>>(\n self as unknown as Comlink.Endpoint\n )\n }\n\n protected sendToUI(action: string, payload: unknown): void {\n ;(this.hostAPI as Record<string, any>).ui.sendToUI({ action, payload })\n }\n\n /**\n * Signal the host that this plugin has finished its work and should be\n * stopped. Use this in headless (UI-less) plugins that run a task and\n * self-terminate.\n */\n protected complete(): void {\n ;(this.hostAPI as Record<string, any>).lifecycle.complete()\n }\n\n async init(): Promise<void> {\n console.log(this.pluginId, \"init() called\")\n console.log(this.pluginId, \"Initialization complete\")\n }\n\n async destroy(): Promise<void> {\n console.log(this.pluginId, \"destroy() called — cleaning up\")\n }\n\n async ping(): Promise<string> {\n return \"pong\"\n }\n\n async onUIMessage(_message: UIMessage): Promise<void> {\n // Override in subclass to handle UI messages\n }\n\n /**\n * Expose the worker API via Comlink and start listening.\n * Call this once after constructing the plugin instance.\n *\n * The host calls `init(config)` with `{ pluginId }`,\n * which is captured here to set `this.pluginId` before the\n * subclass's `init()` runs.\n */\n start(): void {\n Comlink.expose(\n {\n init: (config: PluginConfig) => {\n this.pluginId = config.pluginId\n return this.init()\n },\n destroy: () => this.destroy(),\n ping: () => this.ping(),\n onUIMessage: (message: UIMessage) => this.onUIMessage(message),\n },\n self as unknown as Comlink.Endpoint\n )\n console.log(\"Worker loaded, API exposed via Comlink\")\n }\n}\n"],"mappings":"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","names":["Comlink"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ClientPluginApi } from "./api";
|
|
2
|
+
export * from "./api";
|
|
3
|
+
export * from "./host-api";
|
|
4
|
+
export * from "./plugin-worker";
|
|
5
|
+
/**
|
|
6
|
+
* The Snaptrude plugin client API.
|
|
7
|
+
*
|
|
8
|
+
* The main entry point for plugins to interact with the Snaptrude platform.
|
|
9
|
+
*/
|
|
10
|
+
export declare const snaptrude: ClientPluginApi;
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,OAAO,CAAA;AAEvC,cAAc,OAAO,CAAA;AACrB,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAE/B;;;;GAIG;AACH,eAAO,MAAM,SAAS,iBAAgC,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// src/api/index.ts
|
|
2
|
+
import {
|
|
3
|
+
PluginApi
|
|
4
|
+
} from "@snaptrude/plugin-core";
|
|
5
|
+
|
|
6
|
+
// src/host-api.ts
|
|
7
|
+
import * as Comlink from "comlink";
|
|
8
|
+
function createHostApi(endpoint) {
|
|
9
|
+
return Comlink.wrap(
|
|
10
|
+
endpoint ?? globalThis
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
var _instance = null;
|
|
14
|
+
function getHostApi() {
|
|
15
|
+
if (!_instance) {
|
|
16
|
+
_instance = createHostApi();
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
call: async (payload) => {
|
|
20
|
+
if (!_instance) {
|
|
21
|
+
throw new Error("Host API not initialized");
|
|
22
|
+
}
|
|
23
|
+
return _instance.call(payload).then((result) => {
|
|
24
|
+
if (result.success) {
|
|
25
|
+
return result.data;
|
|
26
|
+
} else {
|
|
27
|
+
throw new Error(result.error);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/rpc-proxy.ts
|
|
35
|
+
function createRpcNamespace(basePath) {
|
|
36
|
+
const build = (path) => new Proxy(NOOP, {
|
|
37
|
+
get(_target, prop) {
|
|
38
|
+
if (typeof prop !== "string" || prop === "then") return void 0;
|
|
39
|
+
return build(`${path}.${prop}`);
|
|
40
|
+
},
|
|
41
|
+
apply(_target, _thisArg, argArray) {
|
|
42
|
+
const payload = {
|
|
43
|
+
method: path,
|
|
44
|
+
args: argArray
|
|
45
|
+
};
|
|
46
|
+
return getHostApi().call(payload);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return build(basePath);
|
|
50
|
+
}
|
|
51
|
+
var NOOP = () => {
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/api/index.ts
|
|
55
|
+
var ClientPluginApi = class _ClientPluginApi extends PluginApi {
|
|
56
|
+
constructor() {
|
|
57
|
+
super();
|
|
58
|
+
this.core = createRpcNamespace("core");
|
|
59
|
+
this.design = createRpcNamespace("design");
|
|
60
|
+
this.entity = createRpcNamespace("entity");
|
|
61
|
+
this.program = createRpcNamespace("program");
|
|
62
|
+
this.presentation = createRpcNamespace("presentation");
|
|
63
|
+
}
|
|
64
|
+
static getInstance() {
|
|
65
|
+
if (!_ClientPluginApi.instance) {
|
|
66
|
+
_ClientPluginApi.instance = new _ClientPluginApi();
|
|
67
|
+
}
|
|
68
|
+
return _ClientPluginApi.instance;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// src/plugin-worker.ts
|
|
73
|
+
import * as Comlink2 from "comlink";
|
|
74
|
+
var PluginWorker = class {
|
|
75
|
+
constructor() {
|
|
76
|
+
this.hostAPI = Comlink2.wrap(
|
|
77
|
+
self
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
sendToUI(action, payload) {
|
|
81
|
+
;
|
|
82
|
+
this.hostAPI.ui.sendToUI({ action, payload });
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Signal the host that this plugin has finished its work and should be
|
|
86
|
+
* stopped. Use this in headless (UI-less) plugins that run a task and
|
|
87
|
+
* self-terminate.
|
|
88
|
+
*/
|
|
89
|
+
complete() {
|
|
90
|
+
;
|
|
91
|
+
this.hostAPI.lifecycle.complete();
|
|
92
|
+
}
|
|
93
|
+
async init() {
|
|
94
|
+
console.log(this.pluginId, "init() called");
|
|
95
|
+
console.log(this.pluginId, "Initialization complete");
|
|
96
|
+
}
|
|
97
|
+
async destroy() {
|
|
98
|
+
console.log(this.pluginId, "destroy() called \u2014 cleaning up");
|
|
99
|
+
}
|
|
100
|
+
async ping() {
|
|
101
|
+
return "pong";
|
|
102
|
+
}
|
|
103
|
+
async onUIMessage(_message) {
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Expose the worker API via Comlink and start listening.
|
|
107
|
+
* Call this once after constructing the plugin instance.
|
|
108
|
+
*
|
|
109
|
+
* The host calls `init(config)` with `{ pluginId }`,
|
|
110
|
+
* which is captured here to set `this.pluginId` before the
|
|
111
|
+
* subclass's `init()` runs.
|
|
112
|
+
*/
|
|
113
|
+
start() {
|
|
114
|
+
Comlink2.expose(
|
|
115
|
+
{
|
|
116
|
+
init: (config) => {
|
|
117
|
+
this.pluginId = config.pluginId;
|
|
118
|
+
return this.init();
|
|
119
|
+
},
|
|
120
|
+
destroy: () => this.destroy(),
|
|
121
|
+
ping: () => this.ping(),
|
|
122
|
+
onUIMessage: (message) => this.onUIMessage(message)
|
|
123
|
+
},
|
|
124
|
+
self
|
|
125
|
+
);
|
|
126
|
+
console.log("Worker loaded, API exposed via Comlink");
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/index.ts
|
|
131
|
+
var snaptrude = ClientPluginApi.getInstance();
|
|
132
|
+
export {
|
|
133
|
+
ClientPluginApi,
|
|
134
|
+
PluginWorker,
|
|
135
|
+
createHostApi,
|
|
136
|
+
getHostApi,
|
|
137
|
+
snaptrude
|
|
138
|
+
};
|
|
139
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/api/index.ts","../src/host-api.ts","../src/rpc-proxy.ts","../src/plugin-worker.ts","../src/index.ts"],"sourcesContent":["import {\n PluginApi,\n PluginCoreApi,\n PluginDesignApi,\n PluginEntityApi,\n PluginProgramApi,\n PluginPresentationApi,\n} from \"@snaptrude/plugin-core\"\nimport { createRpcNamespace } from \"../rpc-proxy\"\n\nexport class ClientPluginApi extends PluginApi {\n private static instance: ClientPluginApi\n\n /**\n * Every namespace is fully remote under the all-handle model: math/geom now\n * cross to the host (values are opaque handles), so there is no in-worker\n * compute left. All dispatch through a single generic RPC Proxy. Units live\n * under `core.units`, so they ride the `core` proxy.\n */\n public core: PluginCoreApi\n public design: PluginDesignApi\n public entity: PluginEntityApi\n public program: PluginProgramApi\n public presentation: PluginPresentationApi\n\n private constructor() {\n super()\n this.core = createRpcNamespace<PluginCoreApi>(\"core\")\n this.design = createRpcNamespace<PluginDesignApi>(\"design\")\n this.entity = createRpcNamespace<PluginEntityApi>(\"entity\")\n this.program = createRpcNamespace<PluginProgramApi>(\"program\")\n this.presentation =\n createRpcNamespace<PluginPresentationApi>(\"presentation\")\n }\n\n static getInstance(): ClientPluginApi {\n if (!ClientPluginApi.instance) {\n ClientPluginApi.instance = new ClientPluginApi()\n }\n return ClientPluginApi.instance\n }\n}\n","import * as Comlink from \"comlink\"\nimport type {\n PluginApiMethod,\n PluginApiCallPayload,\n PluginApiCallWrappedResult,\n PluginApiCallResult,\n} from \"@snaptrude/plugin-core\"\n\nexport interface HostApi {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallWrappedResult<M>>\n}\n\nexport interface HostApiWrapped {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallResult<M>>\n}\n\nexport function createHostApi(endpoint?: Comlink.Endpoint): HostApi {\n return Comlink.wrap<HostApi>(\n endpoint ?? (globalThis as unknown as Comlink.Endpoint)\n ) as unknown as HostApi\n}\n\nlet _instance: HostApi | null = null\n\nexport function getHostApi(): HostApiWrapped {\n if (!_instance) {\n _instance = createHostApi()\n }\n return {\n call: async <M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>> => {\n if (!_instance) {\n throw new Error(\"Host API not initialized\")\n }\n return _instance.call(payload).then(result => {\n if (result.success) {\n return result.data\n } else {\n throw new Error(result.error)\n }\n })\n }\n }\n}\n","import type {\n PluginApiCallPayload,\n PluginApiMethod,\n} from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\n\n/**\n * Build a namespace object whose nested property access maps to a\n * dot-separated host RPC method path, and whose every call dispatches that\n * path through the host bridge.\n *\n * The host exposes the entire plugin API behind a single generic `call()`\n * (see the host `bridge.ts`), and the method string is exactly the property\n * path — so one Proxy replaces every hand-written per-method RPC wrapper for\n * every namespace (`core.*`, `design.*`, `entity.*`):\n *\n * The POSITIONAL transport forwards the whole argument tuple; the host router\n * spreads it back into the resolved method (`fn(...args)`):\n *\n * ```ts\n * snaptrude.core.math.vec3.new(1, 2, 3)\n * // → getHostApi().call({ method: \"core.math.vec3.new\", args: [1, 2, 3] })\n * ```\n *\n * Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>(\"entity\")`.\n * The Proxy is structurally cast to the abstract API type — argument and\n * return types are enforced by that type, while dispatch is dynamic.\n *\n * Every namespace uses this — including `core.math.*` and `core.geom.*`: under\n * the all-handle model there is no in-worker compute; math and geometry are\n * host calls like everything else.\n */\nexport function createRpcNamespace<T extends object>(basePath: string): T {\n const build = (path: string): unknown =>\n new Proxy(NOOP, {\n get(_target, prop) {\n // Symbols and `then` must not resolve to a callable proxy, otherwise\n // the namespace would look thenable and break Promise resolution if it\n // ever reached an `await`.\n if (typeof prop !== \"string\" || prop === \"then\") return undefined\n return build(`${path}.${prop}`)\n },\n apply(_target, _thisArg, argArray: unknown[]) {\n const payload = {\n method: path,\n args: argArray,\n } as unknown as PluginApiCallPayload<PluginApiMethod>\n return getHostApi().call(payload)\n },\n })\n\n return build(basePath) as T\n}\n\n/** Proxy target must be callable for the `apply` trap; identity is irrelevant. */\nconst NOOP = (): void => {}\n","import * as Comlink from \"comlink\"\n\nexport interface UIMessage {\n action: string\n payload: unknown\n}\n\ninterface PluginConfig {\n pluginId: string\n}\n\n/**\n * Base class for Snaptrude plugin workers.\n *\n * Handles Comlink wiring, host communication, and the standard lifecycle\n * methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and\n * override only the methods you need — then call `start()` to expose the\n * worker API.\n *\n * The plugin ID is received automatically from the host during\n * initialization — no need to pass it manually.\n *\n * @example\n * ```ts\n * import { PluginWorker } from \"@snaptrude/plugin-client\";\n *\n * class MyPlugin extends PluginWorker {\n * async onUIMessage(message: UIMessage) {\n * // handle messages from the UI panel\n * }\n * }\n *\n * new MyPlugin().start();\n * ```\n */\nexport abstract class PluginWorker {\n protected pluginId!: string\n private hostAPI: Comlink.Remote<Record<string, unknown>>\n\n constructor() {\n this.hostAPI = Comlink.wrap<Record<string, unknown>>(\n self as unknown as Comlink.Endpoint\n )\n }\n\n protected sendToUI(action: string, payload: unknown): void {\n ;(this.hostAPI as Record<string, any>).ui.sendToUI({ action, payload })\n }\n\n /**\n * Signal the host that this plugin has finished its work and should be\n * stopped. Use this in headless (UI-less) plugins that run a task and\n * self-terminate.\n */\n protected complete(): void {\n ;(this.hostAPI as Record<string, any>).lifecycle.complete()\n }\n\n async init(): Promise<void> {\n console.log(this.pluginId, \"init() called\")\n console.log(this.pluginId, \"Initialization complete\")\n }\n\n async destroy(): Promise<void> {\n console.log(this.pluginId, \"destroy() called — cleaning up\")\n }\n\n async ping(): Promise<string> {\n return \"pong\"\n }\n\n async onUIMessage(_message: UIMessage): Promise<void> {\n // Override in subclass to handle UI messages\n }\n\n /**\n * Expose the worker API via Comlink and start listening.\n * Call this once after constructing the plugin instance.\n *\n * The host calls `init(config)` with `{ pluginId }`,\n * which is captured here to set `this.pluginId` before the\n * subclass's `init()` runs.\n */\n start(): void {\n Comlink.expose(\n {\n init: (config: PluginConfig) => {\n this.pluginId = config.pluginId\n return this.init()\n },\n destroy: () => this.destroy(),\n ping: () => this.ping(),\n onUIMessage: (message: UIMessage) => this.onUIMessage(message),\n },\n self as unknown as Comlink.Endpoint\n )\n console.log(\"Worker loaded, API exposed via Comlink\")\n }\n}\n","import { ClientPluginApi } from \"./api\"\n\nexport * from \"./api\"\nexport * from \"./host-api\"\nexport * from \"./plugin-worker\"\n\n/**\n * The Snaptrude plugin client API.\n *\n * The main entry point for plugins to interact with the Snaptrude platform.\n */\nexport const snaptrude = ClientPluginApi.getInstance()\n"],"mappings":"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","names":["Comlink"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface UIMessage {
|
|
2
|
+
action: string;
|
|
3
|
+
payload: unknown;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Base class for Snaptrude plugin workers.
|
|
7
|
+
*
|
|
8
|
+
* Handles Comlink wiring, host communication, and the standard lifecycle
|
|
9
|
+
* methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and
|
|
10
|
+
* override only the methods you need — then call `start()` to expose the
|
|
11
|
+
* worker API.
|
|
12
|
+
*
|
|
13
|
+
* The plugin ID is received automatically from the host during
|
|
14
|
+
* initialization — no need to pass it manually.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { PluginWorker } from "@snaptrude/plugin-client";
|
|
19
|
+
*
|
|
20
|
+
* class MyPlugin extends PluginWorker {
|
|
21
|
+
* async onUIMessage(message: UIMessage) {
|
|
22
|
+
* // handle messages from the UI panel
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* new MyPlugin().start();
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare abstract class PluginWorker {
|
|
30
|
+
protected pluginId: string;
|
|
31
|
+
private hostAPI;
|
|
32
|
+
constructor();
|
|
33
|
+
protected sendToUI(action: string, payload: unknown): void;
|
|
34
|
+
/**
|
|
35
|
+
* Signal the host that this plugin has finished its work and should be
|
|
36
|
+
* stopped. Use this in headless (UI-less) plugins that run a task and
|
|
37
|
+
* self-terminate.
|
|
38
|
+
*/
|
|
39
|
+
protected complete(): void;
|
|
40
|
+
init(): Promise<void>;
|
|
41
|
+
destroy(): Promise<void>;
|
|
42
|
+
ping(): Promise<string>;
|
|
43
|
+
onUIMessage(_message: UIMessage): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Expose the worker API via Comlink and start listening.
|
|
46
|
+
* Call this once after constructing the plugin instance.
|
|
47
|
+
*
|
|
48
|
+
* The host calls `init(config)` with `{ pluginId }`,
|
|
49
|
+
* which is captured here to set `this.pluginId` before the
|
|
50
|
+
* subclass's `init()` runs.
|
|
51
|
+
*/
|
|
52
|
+
start(): void;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=plugin-worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-worker.d.ts","sourceRoot":"","sources":["../src/plugin-worker.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;CACjB;AAMD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,8BAAsB,YAAY;IAChC,SAAS,CAAC,QAAQ,EAAG,MAAM,CAAA;IAC3B,OAAO,CAAC,OAAO,CAAyC;;IAQxD,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAI1D;;;;OAIG;IACH,SAAS,CAAC,QAAQ,IAAI,IAAI;IAIpB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAKrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;IAIvB,WAAW,CAAC,QAAQ,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrD;;;;;;;OAOG;IACH,KAAK,IAAI,IAAI;CAed"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a namespace object whose nested property access maps to a
|
|
3
|
+
* dot-separated host RPC method path, and whose every call dispatches that
|
|
4
|
+
* path through the host bridge.
|
|
5
|
+
*
|
|
6
|
+
* The host exposes the entire plugin API behind a single generic `call()`
|
|
7
|
+
* (see the host `bridge.ts`), and the method string is exactly the property
|
|
8
|
+
* path — so one Proxy replaces every hand-written per-method RPC wrapper for
|
|
9
|
+
* every namespace (`core.*`, `design.*`, `entity.*`):
|
|
10
|
+
*
|
|
11
|
+
* The POSITIONAL transport forwards the whole argument tuple; the host router
|
|
12
|
+
* spreads it back into the resolved method (`fn(...args)`):
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* snaptrude.core.math.vec3.new(1, 2, 3)
|
|
16
|
+
* // → getHostApi().call({ method: "core.math.vec3.new", args: [1, 2, 3] })
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>("entity")`.
|
|
20
|
+
* The Proxy is structurally cast to the abstract API type — argument and
|
|
21
|
+
* return types are enforced by that type, while dispatch is dynamic.
|
|
22
|
+
*
|
|
23
|
+
* Every namespace uses this — including `core.math.*` and `core.geom.*`: under
|
|
24
|
+
* the all-handle model there is no in-worker compute; math and geometry are
|
|
25
|
+
* host calls like everything else.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createRpcNamespace<T extends object>(basePath: string): T;
|
|
28
|
+
//# sourceMappingURL=rpc-proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc-proxy.d.ts","sourceRoot":"","sources":["../src/rpc-proxy.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,CAoBxE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@snaptrude/plugin-client",
|
|
3
|
+
"version": "0.0.0-dev-20260708130115",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"require": "./dist/index.cjs",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"comlink": "^4.4.2",
|
|
21
|
+
"@snaptrude/plugin-core": "0.0.0-dev-20260708130115"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsup": "^8.5.1",
|
|
25
|
+
"typescript": "^5.5.4"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"check-types": "tsc --noEmit",
|
|
29
|
+
"build": "tsup --clean",
|
|
30
|
+
"dev": "tsup --watch",
|
|
31
|
+
"clean-dist": "rm -rf dist"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PluginApi,
|
|
3
|
+
PluginCoreApi,
|
|
4
|
+
PluginDesignApi,
|
|
5
|
+
PluginEntityApi,
|
|
6
|
+
PluginProgramApi,
|
|
7
|
+
PluginPresentationApi,
|
|
8
|
+
} from "@snaptrude/plugin-core"
|
|
9
|
+
import { createRpcNamespace } from "../rpc-proxy"
|
|
10
|
+
|
|
11
|
+
export class ClientPluginApi extends PluginApi {
|
|
12
|
+
private static instance: ClientPluginApi
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Every namespace is fully remote under the all-handle model: math/geom now
|
|
16
|
+
* cross to the host (values are opaque handles), so there is no in-worker
|
|
17
|
+
* compute left. All dispatch through a single generic RPC Proxy. Units live
|
|
18
|
+
* under `core.units`, so they ride the `core` proxy.
|
|
19
|
+
*/
|
|
20
|
+
public core: PluginCoreApi
|
|
21
|
+
public design: PluginDesignApi
|
|
22
|
+
public entity: PluginEntityApi
|
|
23
|
+
public program: PluginProgramApi
|
|
24
|
+
public presentation: PluginPresentationApi
|
|
25
|
+
|
|
26
|
+
private constructor() {
|
|
27
|
+
super()
|
|
28
|
+
this.core = createRpcNamespace<PluginCoreApi>("core")
|
|
29
|
+
this.design = createRpcNamespace<PluginDesignApi>("design")
|
|
30
|
+
this.entity = createRpcNamespace<PluginEntityApi>("entity")
|
|
31
|
+
this.program = createRpcNamespace<PluginProgramApi>("program")
|
|
32
|
+
this.presentation =
|
|
33
|
+
createRpcNamespace<PluginPresentationApi>("presentation")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static getInstance(): ClientPluginApi {
|
|
37
|
+
if (!ClientPluginApi.instance) {
|
|
38
|
+
ClientPluginApi.instance = new ClientPluginApi()
|
|
39
|
+
}
|
|
40
|
+
return ClientPluginApi.instance
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/host-api.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as Comlink from "comlink"
|
|
2
|
+
import type {
|
|
3
|
+
PluginApiMethod,
|
|
4
|
+
PluginApiCallPayload,
|
|
5
|
+
PluginApiCallWrappedResult,
|
|
6
|
+
PluginApiCallResult,
|
|
7
|
+
} from "@snaptrude/plugin-core"
|
|
8
|
+
|
|
9
|
+
export interface HostApi {
|
|
10
|
+
call<M extends PluginApiMethod>(
|
|
11
|
+
payload: PluginApiCallPayload<M>
|
|
12
|
+
): Promise<PluginApiCallWrappedResult<M>>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface HostApiWrapped {
|
|
16
|
+
call<M extends PluginApiMethod>(
|
|
17
|
+
payload: PluginApiCallPayload<M>
|
|
18
|
+
): Promise<PluginApiCallResult<M>>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createHostApi(endpoint?: Comlink.Endpoint): HostApi {
|
|
22
|
+
return Comlink.wrap<HostApi>(
|
|
23
|
+
endpoint ?? (globalThis as unknown as Comlink.Endpoint)
|
|
24
|
+
) as unknown as HostApi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let _instance: HostApi | null = null
|
|
28
|
+
|
|
29
|
+
export function getHostApi(): HostApiWrapped {
|
|
30
|
+
if (!_instance) {
|
|
31
|
+
_instance = createHostApi()
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
call: async <M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>> => {
|
|
35
|
+
if (!_instance) {
|
|
36
|
+
throw new Error("Host API not initialized")
|
|
37
|
+
}
|
|
38
|
+
return _instance.call(payload).then(result => {
|
|
39
|
+
if (result.success) {
|
|
40
|
+
return result.data
|
|
41
|
+
} else {
|
|
42
|
+
throw new Error(result.error)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ClientPluginApi } from "./api"
|
|
2
|
+
|
|
3
|
+
export * from "./api"
|
|
4
|
+
export * from "./host-api"
|
|
5
|
+
export * from "./plugin-worker"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The Snaptrude plugin client API.
|
|
9
|
+
*
|
|
10
|
+
* The main entry point for plugins to interact with the Snaptrude platform.
|
|
11
|
+
*/
|
|
12
|
+
export const snaptrude = ClientPluginApi.getInstance()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as Comlink from "comlink"
|
|
2
|
+
|
|
3
|
+
export interface UIMessage {
|
|
4
|
+
action: string
|
|
5
|
+
payload: unknown
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface PluginConfig {
|
|
9
|
+
pluginId: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base class for Snaptrude plugin workers.
|
|
14
|
+
*
|
|
15
|
+
* Handles Comlink wiring, host communication, and the standard lifecycle
|
|
16
|
+
* methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and
|
|
17
|
+
* override only the methods you need — then call `start()` to expose the
|
|
18
|
+
* worker API.
|
|
19
|
+
*
|
|
20
|
+
* The plugin ID is received automatically from the host during
|
|
21
|
+
* initialization — no need to pass it manually.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { PluginWorker } from "@snaptrude/plugin-client";
|
|
26
|
+
*
|
|
27
|
+
* class MyPlugin extends PluginWorker {
|
|
28
|
+
* async onUIMessage(message: UIMessage) {
|
|
29
|
+
* // handle messages from the UI panel
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* new MyPlugin().start();
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export abstract class PluginWorker {
|
|
37
|
+
protected pluginId!: string
|
|
38
|
+
private hostAPI: Comlink.Remote<Record<string, unknown>>
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
this.hostAPI = Comlink.wrap<Record<string, unknown>>(
|
|
42
|
+
self as unknown as Comlink.Endpoint
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected sendToUI(action: string, payload: unknown): void {
|
|
47
|
+
;(this.hostAPI as Record<string, any>).ui.sendToUI({ action, payload })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Signal the host that this plugin has finished its work and should be
|
|
52
|
+
* stopped. Use this in headless (UI-less) plugins that run a task and
|
|
53
|
+
* self-terminate.
|
|
54
|
+
*/
|
|
55
|
+
protected complete(): void {
|
|
56
|
+
;(this.hostAPI as Record<string, any>).lifecycle.complete()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async init(): Promise<void> {
|
|
60
|
+
console.log(this.pluginId, "init() called")
|
|
61
|
+
console.log(this.pluginId, "Initialization complete")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async destroy(): Promise<void> {
|
|
65
|
+
console.log(this.pluginId, "destroy() called — cleaning up")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async ping(): Promise<string> {
|
|
69
|
+
return "pong"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async onUIMessage(_message: UIMessage): Promise<void> {
|
|
73
|
+
// Override in subclass to handle UI messages
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Expose the worker API via Comlink and start listening.
|
|
78
|
+
* Call this once after constructing the plugin instance.
|
|
79
|
+
*
|
|
80
|
+
* The host calls `init(config)` with `{ pluginId }`,
|
|
81
|
+
* which is captured here to set `this.pluginId` before the
|
|
82
|
+
* subclass's `init()` runs.
|
|
83
|
+
*/
|
|
84
|
+
start(): void {
|
|
85
|
+
Comlink.expose(
|
|
86
|
+
{
|
|
87
|
+
init: (config: PluginConfig) => {
|
|
88
|
+
this.pluginId = config.pluginId
|
|
89
|
+
return this.init()
|
|
90
|
+
},
|
|
91
|
+
destroy: () => this.destroy(),
|
|
92
|
+
ping: () => this.ping(),
|
|
93
|
+
onUIMessage: (message: UIMessage) => this.onUIMessage(message),
|
|
94
|
+
},
|
|
95
|
+
self as unknown as Comlink.Endpoint
|
|
96
|
+
)
|
|
97
|
+
console.log("Worker loaded, API exposed via Comlink")
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/rpc-proxy.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PluginApiCallPayload,
|
|
3
|
+
PluginApiMethod,
|
|
4
|
+
} from "@snaptrude/plugin-core"
|
|
5
|
+
import { getHostApi } from "./host-api"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a namespace object whose nested property access maps to a
|
|
9
|
+
* dot-separated host RPC method path, and whose every call dispatches that
|
|
10
|
+
* path through the host bridge.
|
|
11
|
+
*
|
|
12
|
+
* The host exposes the entire plugin API behind a single generic `call()`
|
|
13
|
+
* (see the host `bridge.ts`), and the method string is exactly the property
|
|
14
|
+
* path — so one Proxy replaces every hand-written per-method RPC wrapper for
|
|
15
|
+
* every namespace (`core.*`, `design.*`, `entity.*`):
|
|
16
|
+
*
|
|
17
|
+
* The POSITIONAL transport forwards the whole argument tuple; the host router
|
|
18
|
+
* spreads it back into the resolved method (`fn(...args)`):
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* snaptrude.core.math.vec3.new(1, 2, 3)
|
|
22
|
+
* // → getHostApi().call({ method: "core.math.vec3.new", args: [1, 2, 3] })
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>("entity")`.
|
|
26
|
+
* The Proxy is structurally cast to the abstract API type — argument and
|
|
27
|
+
* return types are enforced by that type, while dispatch is dynamic.
|
|
28
|
+
*
|
|
29
|
+
* Every namespace uses this — including `core.math.*` and `core.geom.*`: under
|
|
30
|
+
* the all-handle model there is no in-worker compute; math and geometry are
|
|
31
|
+
* host calls like everything else.
|
|
32
|
+
*/
|
|
33
|
+
export function createRpcNamespace<T extends object>(basePath: string): T {
|
|
34
|
+
const build = (path: string): unknown =>
|
|
35
|
+
new Proxy(NOOP, {
|
|
36
|
+
get(_target, prop) {
|
|
37
|
+
// Symbols and `then` must not resolve to a callable proxy, otherwise
|
|
38
|
+
// the namespace would look thenable and break Promise resolution if it
|
|
39
|
+
// ever reached an `await`.
|
|
40
|
+
if (typeof prop !== "string" || prop === "then") return undefined
|
|
41
|
+
return build(`${path}.${prop}`)
|
|
42
|
+
},
|
|
43
|
+
apply(_target, _thisArg, argArray: unknown[]) {
|
|
44
|
+
const payload = {
|
|
45
|
+
method: path,
|
|
46
|
+
args: argArray,
|
|
47
|
+
} as unknown as PluginApiCallPayload<PluginApiMethod>
|
|
48
|
+
return getHostApi().call(payload)
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return build(basePath) as T
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Proxy target must be callable for the `apply` trap; identity is irrelevant. */
|
|
56
|
+
const NOOP = (): void => {}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ES2020",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2020", "WebWorker"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"emitDeclarationOnly": true,
|
|
11
|
+
"typeRoots": [],
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"outDir": "dist"
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*.ts"]
|
|
17
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from "tsup"
|
|
2
|
+
|
|
3
|
+
export default defineConfig( (options) => {
|
|
4
|
+
return {
|
|
5
|
+
entry: ["src/index.ts"],
|
|
6
|
+
format: ["cjs", "esm"],
|
|
7
|
+
dts: false,
|
|
8
|
+
sourcemap: true,
|
|
9
|
+
onSuccess: "tsc", // Run tsc to generate d.ts and d.ts.map
|
|
10
|
+
watch: options.watch,
|
|
11
|
+
}
|
|
12
|
+
})
|