@rainbow-robotics/plugin-ui 0.1.2-dev.44 → 0.1.2-dev.46
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/README.md +222 -75
- package/build-dist.mjs +55 -17
- package/dist/host-ui.d.ts +49 -0
- package/dist/host-ui.d.ts.map +1 -0
- package/dist/host-ui.js +33 -0
- package/dist/host-ui.js.map +7 -0
- package/dist/manifest.d.ts +20 -4
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +1 -1
- package/dist/manifest.js.map +2 -2
- package/dist/pluginData.d.ts +68 -0
- package/dist/pluginData.d.ts.map +1 -0
- package/dist/react.d.ts +3430 -36
- package/dist/react.js +5 -54
- package/dist/react.js.map +2 -2
- package/dist/sdk.d.ts +38 -2
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +29 -4
- package/dist/sdk.js.map +2 -2
- package/dist/viteHost.d.ts +48 -0
- package/dist/viteHost.d.ts.map +1 -0
- package/dist/viteHost.js +262 -0
- package/dist/viteHost.js.map +7 -0
- package/dist/ws.d.ts +53 -0
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +128 -0
- package/dist/ws.js.map +7 -0
- package/package.json +43 -20
- package/src/host-ui.ts +80 -0
- package/src/host.tsx +236 -308
- package/src/index.d.ts +1 -0
- package/src/manifest.ts +25 -4
- package/src/pluginData.tsx +241 -0
- package/src/react.tsx +16 -127
- package/src/sdk.ts +74 -7
- package/src/viteHost.ts +316 -0
- package/src/ws.ts +47 -0
- package/build-browser.mjs +0 -84
- package/dist/connect.d.ts +0 -11
- package/dist/connect.d.ts.map +0 -1
- package/dist/connect.js +0 -34
- package/dist/connect.js.map +0 -7
- package/dist/elements.d.ts +0 -195
- package/dist/elements.d.ts.map +0 -1
- package/dist/elements.js +0 -115
- package/dist/elements.js.map +0 -7
- package/dist/react.d.ts.map +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-37HVFFJB.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-4MGSRUAS.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-Q6XGCIPE.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/connect.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/elements.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/react.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/sdk.js +0 -1
- package/dist-browser/vendor/react-dom-client.js +0 -1
- package/dist-browser/vendor/react-jsx-dev-runtime.js +0 -1
- package/dist-browser/vendor/react-jsx-runtime.js +0 -1
- package/dist-browser/vendor/react-runtime.js +0 -65
- package/dist-browser/vendor/react.js +0 -1
- package/src/connect.ts +0 -64
- package/src/elements.ts +0 -257
package/src/host.tsx
CHANGED
|
@@ -1,356 +1,284 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Host
|
|
2
|
+
* Host mounting — used by nexus (Muscat) only.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* host resolves here. This keeps plugin bundles small and lets rb-components
|
|
8
|
-
* evolve freely behind the stable contract.
|
|
4
|
+
* No iframe, no remote-dom, no Module Federation: a plugin renders directly in
|
|
5
|
+
* the host document. Two mount strategies, chosen per `PluginModuleSource.kind`
|
|
6
|
+
* (see ./pluginData):
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* - "esm" (built plugin, `muscat-pk build` / robot runtime): the host does a
|
|
9
|
+
* plain dynamic `import()` of the plugin's built entry. The plugin is built
|
|
10
|
+
* with react / react-dom / rb-components / plugin-ui context modules left
|
|
11
|
+
* EXTERNAL as bare specifiers; the host page carries an import map (see
|
|
12
|
+
* @rainbow-robotics/plugin-ui/host-runtime + the shim modules nexus serves)
|
|
13
|
+
* that resolves those specifiers to the host's OWN already-loaded instances.
|
|
14
|
+
* So the loaded component shares the host's single React + rb-components +
|
|
15
|
+
* RbContext + host-ui — rendered as a genuinely nested React tree, with no
|
|
16
|
+
* duplicate React instance and minimal plugin bundle size.
|
|
17
|
+
*
|
|
18
|
+
* - "dev" (`muscat-pk dev`): the plugin's own Vite dev server serves a
|
|
19
|
+
* bootstrap module exposing `mount(container, opts)`. That module uses ITS
|
|
20
|
+
* OWN react/react-dom/plugin-ui (resolved from the plugin's own node_modules),
|
|
21
|
+
* so it self-mounts into the container as a fully independent React root.
|
|
22
|
+
* HostServices passed in are plain functions (see ./host-ui) — safe to
|
|
23
|
+
* call across the React-instance boundary, since they don't touch React
|
|
24
|
+
* internals themselves.
|
|
15
25
|
*/
|
|
16
26
|
import {
|
|
17
|
-
|
|
27
|
+
useCallback,
|
|
18
28
|
useEffect,
|
|
19
29
|
useMemo,
|
|
20
30
|
useRef,
|
|
31
|
+
useState,
|
|
21
32
|
type ComponentType,
|
|
22
|
-
type ReactNode,
|
|
23
33
|
} from "react";
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
RBLabelButton,
|
|
34
|
-
RBIconButton,
|
|
35
|
-
RBSwitch,
|
|
36
|
-
RBSelectBox,
|
|
37
|
-
RBSelectOption,
|
|
38
|
-
RBTextInput,
|
|
39
|
-
RBIcons,
|
|
40
|
-
} from "@repo/rb-components/new_components";
|
|
41
|
-
import type { RbSelectOption } from "./elements";
|
|
42
|
-
import type { RbNotifyType } from "./sdk";
|
|
34
|
+
import { RbContext, type RbSdk } from "./sdk";
|
|
35
|
+
import { RbWs, createLazyRbSdk } from "./ws";
|
|
36
|
+
// IMPORTANT: this file is shipped as SOURCE (see package.json — "./host" maps
|
|
37
|
+
// to src/host.tsx, not dist), so a relative "./host-ui" import here would
|
|
38
|
+
// resolve to src/host-ui.ts, a DIFFERENT file than the dist/host-ui.js every
|
|
39
|
+
// other consumer (e.g. nexus's App.tsx) resolves to — two files, two separate
|
|
40
|
+
// module-scope `services` singletons. Must import through the package specifier.
|
|
41
|
+
import { getHostServices } from "@rainbow-robotics/plugin-ui/host-ui";
|
|
42
|
+
import { usePluginData, type PluginModuleSource } from "./pluginData";
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return Icon ? <Icon /> : null;
|
|
44
|
+
function pluginRootAttrs(pluginId: string) {
|
|
45
|
+
return {
|
|
46
|
+
"data-muscat-plugin-root": "",
|
|
47
|
+
"data-muscat-plugin-id": pluginId,
|
|
48
|
+
};
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
/* -------------------------------------------------------------------------- */
|
|
53
|
-
/*
|
|
52
|
+
/* rb sdk — pub/sub/action/serve over the plugin's Zenoh WebSocket */
|
|
54
53
|
/* -------------------------------------------------------------------------- */
|
|
55
54
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*/
|
|
62
|
-
function adapt<P extends object>(
|
|
63
|
-
Component: (props: P) => ReactNode,
|
|
64
|
-
): ComponentType<P> {
|
|
65
|
-
const Wrapped = forwardRef<unknown, P>(function Adapted(props, _ref) {
|
|
66
|
-
return Component(props as P);
|
|
67
|
-
});
|
|
68
|
-
return Wrapped as unknown as ComponentType<P>;
|
|
69
|
-
}
|
|
55
|
+
/** Per-mount host-provided bits (initial data + save registration). */
|
|
56
|
+
type PluginMount = {
|
|
57
|
+
data?: unknown;
|
|
58
|
+
onSave: (getPayload: () => unknown) => () => void;
|
|
59
|
+
};
|
|
70
60
|
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
disabled?: boolean;
|
|
84
|
-
onChange?: (value: number) => void;
|
|
85
|
-
}) {
|
|
86
|
-
return (
|
|
87
|
-
<RBSlider
|
|
88
|
-
value={[value ?? min]}
|
|
89
|
-
min={min}
|
|
90
|
-
max={max}
|
|
91
|
-
step={step}
|
|
92
|
-
disabled={disabled}
|
|
93
|
-
onValueChange={(values) => onChange?.(values[0] ?? min)}
|
|
94
|
-
/>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
61
|
+
function useRbSdk(
|
|
62
|
+
socketUrl: string,
|
|
63
|
+
pluginId: string,
|
|
64
|
+
mount: PluginMount,
|
|
65
|
+
): RbSdk {
|
|
66
|
+
const wsRef = useRef<Promise<RbWs> | null>(null);
|
|
67
|
+
const keyRef = useRef("");
|
|
68
|
+
const key = `${socketUrl}::${pluginId}`;
|
|
69
|
+
if (keyRef.current !== key) {
|
|
70
|
+
keyRef.current = key;
|
|
71
|
+
wsRef.current = RbWs.connect(socketUrl, pluginId);
|
|
72
|
+
}
|
|
97
73
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
children,
|
|
105
|
-
}: {
|
|
106
|
-
label?: string;
|
|
107
|
-
variant?: string;
|
|
108
|
-
size?: string;
|
|
109
|
-
disabled?: boolean;
|
|
110
|
-
onPress?: () => void;
|
|
111
|
-
children?: ReactNode;
|
|
112
|
-
}) {
|
|
113
|
-
return (
|
|
114
|
-
<RBLabelButton
|
|
115
|
-
variant={variant as never}
|
|
116
|
-
size={size as never}
|
|
117
|
-
disabled={disabled}
|
|
118
|
-
onClick={() => onPress?.()}
|
|
119
|
-
>
|
|
120
|
-
{label ?? children}
|
|
121
|
-
</RBLabelButton>
|
|
74
|
+
return useMemo<RbSdk>(
|
|
75
|
+
// `mount.data` is captured at first render (stable for a form's lifetime);
|
|
76
|
+
// `mount.onSave` is a stable callback. Both intentionally out of deps.
|
|
77
|
+
() => createLazyRbSdk(wsRef.current!, getHostServices(), mount),
|
|
78
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
79
|
+
[key],
|
|
122
80
|
);
|
|
123
81
|
}
|
|
124
82
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
83
|
+
/* -------------------------------------------------------------------------- */
|
|
84
|
+
/* ESM mount (prod / robot runtime — import map resolves shared externals) */
|
|
85
|
+
/* -------------------------------------------------------------------------- */
|
|
86
|
+
|
|
87
|
+
function EsmPluginHost({
|
|
88
|
+
source,
|
|
89
|
+
socketUrl,
|
|
90
|
+
pluginId,
|
|
91
|
+
mount,
|
|
131
92
|
}: {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
onPress?: () => void;
|
|
93
|
+
source: Extract<PluginModuleSource, { kind: "esm" }>;
|
|
94
|
+
socketUrl: string;
|
|
95
|
+
pluginId: string;
|
|
96
|
+
mount: PluginMount;
|
|
137
97
|
}) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
variant={variant as never}
|
|
142
|
-
size={size as never}
|
|
143
|
-
disabled={disabled}
|
|
144
|
-
onClick={() => onPress?.()}
|
|
145
|
-
/>
|
|
146
|
-
);
|
|
147
|
-
}
|
|
98
|
+
const rb = useRbSdk(socketUrl, pluginId, mount);
|
|
99
|
+
const [App, setApp] = useState<ComponentType | null>(null);
|
|
100
|
+
const [error, setError] = useState<Error | null>(null);
|
|
148
101
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
let cancelled = false;
|
|
104
|
+
setApp(null);
|
|
105
|
+
setError(null);
|
|
106
|
+
import(/* @vite-ignore */ source.entryUrl)
|
|
107
|
+
.then((mod: { default?: ComponentType }) => {
|
|
108
|
+
if (cancelled) return;
|
|
109
|
+
if (!mod.default) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`plugin entry ${source.entryUrl} has no default export`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
setApp(() => mod.default!);
|
|
115
|
+
})
|
|
116
|
+
.catch((e: unknown) => {
|
|
117
|
+
if (!cancelled) setError(e instanceof Error ? e : new Error(String(e)));
|
|
118
|
+
});
|
|
119
|
+
return () => {
|
|
120
|
+
cancelled = true;
|
|
121
|
+
};
|
|
122
|
+
}, [source.entryUrl]);
|
|
152
123
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
124
|
+
if (error) {
|
|
125
|
+
return (
|
|
126
|
+
<div className="text-fg-danger p-4 text-sm">
|
|
127
|
+
Failed to load plugin: {error.message}
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (!App) {
|
|
132
|
+
return (
|
|
133
|
+
<div className="flex items-center justify-center p-4">
|
|
134
|
+
<div className="border-default size-5 animate-spin rounded-full border-2 border-t-transparent" />
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
164
138
|
return (
|
|
165
|
-
<
|
|
166
|
-
value={
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
/>
|
|
139
|
+
<div {...pluginRootAttrs(pluginId)}>
|
|
140
|
+
<RbContext.Provider value={rb}>
|
|
141
|
+
<App />
|
|
142
|
+
</RbContext.Provider>
|
|
143
|
+
</div>
|
|
171
144
|
);
|
|
172
145
|
}
|
|
173
146
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
);
|
|
147
|
+
/* -------------------------------------------------------------------------- */
|
|
148
|
+
/* Dev mount (muscat-pk dev) */
|
|
149
|
+
/* -------------------------------------------------------------------------- */
|
|
150
|
+
|
|
151
|
+
interface DevBootstrapModule {
|
|
152
|
+
mount: (
|
|
153
|
+
container: HTMLElement,
|
|
154
|
+
opts: {
|
|
155
|
+
socketUrl: string;
|
|
156
|
+
pluginId: string;
|
|
157
|
+
hostServices: ReturnType<typeof getHostServices>;
|
|
158
|
+
data?: unknown;
|
|
159
|
+
onSave: PluginMount["onSave"];
|
|
160
|
+
},
|
|
161
|
+
) => () => void;
|
|
190
162
|
}
|
|
191
163
|
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
onChange,
|
|
164
|
+
function DevPluginHost({
|
|
165
|
+
source,
|
|
166
|
+
socketUrl,
|
|
167
|
+
pluginId,
|
|
168
|
+
mount,
|
|
198
169
|
}: {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
onChange?: (value: string) => void;
|
|
170
|
+
source: Extract<PluginModuleSource, { kind: "dev" }>;
|
|
171
|
+
socketUrl: string;
|
|
172
|
+
pluginId: string;
|
|
173
|
+
mount: PluginMount;
|
|
204
174
|
}) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
value={value ?? ""}
|
|
208
|
-
placeholder={placeholder}
|
|
209
|
-
disabled={disabled}
|
|
210
|
-
onChange={(e) => onChange?.((e.target as HTMLInputElement).value)}
|
|
211
|
-
>
|
|
212
|
-
{options.map((option) => (
|
|
213
|
-
<RBSelectOption key={option.value} value={option.value}>
|
|
214
|
-
{option.label}
|
|
215
|
-
</RBSelectOption>
|
|
216
|
-
))}
|
|
217
|
-
</RBSelectBox>
|
|
218
|
-
);
|
|
219
|
-
}
|
|
175
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
176
|
+
const [error, setError] = useState<Error | null>(null);
|
|
220
177
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
"rb-slider",
|
|
228
|
-
createRemoteComponentRenderer(adapt(SliderAdapter), {
|
|
229
|
-
eventProps: { onChange: { event: "change" } },
|
|
230
|
-
}),
|
|
231
|
-
],
|
|
232
|
-
[
|
|
233
|
-
"rb-button",
|
|
234
|
-
createRemoteComponentRenderer(adapt(ButtonAdapter), {
|
|
235
|
-
eventProps: { onPress: { event: "press" } },
|
|
236
|
-
}),
|
|
237
|
-
],
|
|
238
|
-
[
|
|
239
|
-
"rb-icon-button",
|
|
240
|
-
createRemoteComponentRenderer(adapt(IconButtonAdapter), {
|
|
241
|
-
eventProps: { onPress: { event: "press" } },
|
|
242
|
-
}),
|
|
243
|
-
],
|
|
244
|
-
["rb-text", createRemoteComponentRenderer(adapt(TextAdapter))],
|
|
245
|
-
[
|
|
246
|
-
"rb-text-input",
|
|
247
|
-
createRemoteComponentRenderer(adapt(TextInputAdapter), {
|
|
248
|
-
eventProps: { onInput: { event: "input" } },
|
|
249
|
-
}),
|
|
250
|
-
],
|
|
251
|
-
[
|
|
252
|
-
"rb-switch",
|
|
253
|
-
createRemoteComponentRenderer(adapt(SwitchAdapter), {
|
|
254
|
-
eventProps: { onChange: { event: "change" } },
|
|
255
|
-
}),
|
|
256
|
-
],
|
|
257
|
-
[
|
|
258
|
-
"rb-select",
|
|
259
|
-
createRemoteComponentRenderer(adapt(SelectAdapter), {
|
|
260
|
-
eventProps: { onChange: { event: "change" } },
|
|
261
|
-
}),
|
|
262
|
-
],
|
|
263
|
-
]);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const container = containerRef.current;
|
|
180
|
+
if (!container) return;
|
|
181
|
+
let unmount: (() => void) | undefined;
|
|
182
|
+
let cancelled = false;
|
|
183
|
+
const deferUnmount = () => queueMicrotask(() => unmount?.());
|
|
264
184
|
|
|
265
|
-
/*
|
|
266
|
-
|
|
267
|
-
|
|
185
|
+
import(/* @vite-ignore */ source.bootstrapUrl)
|
|
186
|
+
.then((mod: DevBootstrapModule) => {
|
|
187
|
+
if (cancelled) return;
|
|
188
|
+
unmount = mod.mount(container, {
|
|
189
|
+
socketUrl,
|
|
190
|
+
pluginId,
|
|
191
|
+
hostServices: getHostServices(),
|
|
192
|
+
data: mount.data,
|
|
193
|
+
onSave: mount.onSave,
|
|
194
|
+
});
|
|
195
|
+
})
|
|
196
|
+
.catch((e: unknown) => {
|
|
197
|
+
if (!cancelled) setError(e instanceof Error ? e : new Error(String(e)));
|
|
198
|
+
});
|
|
268
199
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
* and implements pub/sub/action/serve directly; the host only forwards the
|
|
273
|
-
* remote-dom connection, the Socket.IO config, and toast().
|
|
274
|
-
*/
|
|
275
|
-
interface HostThreadExports {
|
|
276
|
-
connect: () => {
|
|
277
|
-
connection: {
|
|
278
|
-
mutate: (records: unknown) => void;
|
|
279
|
-
call: (id: string, method: string, ...args: unknown[]) => unknown;
|
|
200
|
+
return () => {
|
|
201
|
+
cancelled = true;
|
|
202
|
+
deferUnmount();
|
|
280
203
|
};
|
|
281
|
-
|
|
282
|
-
socketUrl: string;
|
|
283
|
-
pluginId: string;
|
|
284
|
-
};
|
|
285
|
-
}
|
|
204
|
+
}, [source.bootstrapUrl, socketUrl, pluginId]);
|
|
286
205
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
toast: (message: string, type?: RbNotifyType) => void;
|
|
206
|
+
if (error) {
|
|
207
|
+
return (
|
|
208
|
+
<div className="text-fg-danger p-4 text-sm">
|
|
209
|
+
Failed to load plugin (dev): {error.message}
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return <div ref={containerRef} {...pluginRootAttrs(pluginId)} />;
|
|
296
214
|
}
|
|
297
215
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
socketUrl,
|
|
216
|
+
/* -------------------------------------------------------------------------- */
|
|
217
|
+
/* Entry point */
|
|
218
|
+
/* -------------------------------------------------------------------------- */
|
|
219
|
+
|
|
220
|
+
/** Renders a plugin's UI. Requires a `PluginDataProvider` above. */
|
|
221
|
+
export function PluginHost({
|
|
305
222
|
pluginId,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
223
|
+
ui,
|
|
224
|
+
data,
|
|
225
|
+
onRegisterSave,
|
|
226
|
+
}: {
|
|
227
|
+
pluginId: string;
|
|
228
|
+
ui: string;
|
|
229
|
+
/** Initial data handed to the plugin (e.g. previously-saved form values). */
|
|
230
|
+
data?: unknown;
|
|
231
|
+
/**
|
|
232
|
+
* Called once with a getter the host can invoke (e.g. from a Save button) to
|
|
233
|
+
* collect the plugin's current payload — `undefined` until the plugin
|
|
234
|
+
* registers via `useRbSave`, or if it never does.
|
|
235
|
+
*/
|
|
236
|
+
onRegisterSave?: (getPayload: () => unknown) => void;
|
|
237
|
+
}) {
|
|
238
|
+
const { getModuleSource, getSocketUrl } = usePluginData();
|
|
239
|
+
const source = useMemo(
|
|
240
|
+
() => getModuleSource(pluginId, ui),
|
|
241
|
+
[getModuleSource, pluginId, ui],
|
|
242
|
+
);
|
|
243
|
+
const socketUrl = getSocketUrl(pluginId);
|
|
312
244
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
245
|
+
// The plugin registers its payload getter here; the host reads it back
|
|
246
|
+
// through the stable getter handed to `onRegisterSave`.
|
|
247
|
+
const saveRef = useRef<(() => unknown) | null>(null);
|
|
248
|
+
const registerSave = useCallback((getPayload: () => unknown) => {
|
|
249
|
+
saveRef.current = getPayload;
|
|
250
|
+
return () => {
|
|
251
|
+
if (saveRef.current === getPayload) saveRef.current = null;
|
|
252
|
+
};
|
|
253
|
+
}, []);
|
|
316
254
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
targetOrigin: "*",
|
|
321
|
-
signal: controller.signal,
|
|
322
|
-
exports: {
|
|
323
|
-
connect() {
|
|
324
|
-
return {
|
|
325
|
-
// forward the receiver's low-level connection across the boundary
|
|
326
|
-
connection: {
|
|
327
|
-
mutate: (records: unknown) =>
|
|
328
|
-
receiver.connection.mutate(records as never),
|
|
329
|
-
call: (id: string, method: string, ...args: unknown[]) =>
|
|
330
|
-
receiver.connection.call(id, method, ...args),
|
|
331
|
-
},
|
|
332
|
-
toast: (message: string, type?: RbNotifyType) =>
|
|
333
|
-
toastRef.current(message, type),
|
|
334
|
-
socketUrl,
|
|
335
|
-
pluginId,
|
|
336
|
-
};
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
});
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
onRegisterSave?.(() => saveRef.current?.());
|
|
257
|
+
}, [onRegisterSave]);
|
|
340
258
|
|
|
341
|
-
|
|
342
|
-
|
|
259
|
+
const mount = useMemo<PluginMount>(
|
|
260
|
+
// data captured once (stable per form lifetime); registerSave is stable.
|
|
261
|
+
() => ({ data, onSave: registerSave }),
|
|
262
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
263
|
+
[registerSave],
|
|
264
|
+
);
|
|
343
265
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
title="rb-plugin-sandbox"
|
|
266
|
+
if (source.kind === "dev") {
|
|
267
|
+
return (
|
|
268
|
+
<DevPluginHost
|
|
269
|
+
source={source}
|
|
270
|
+
socketUrl={socketUrl}
|
|
271
|
+
pluginId={pluginId}
|
|
272
|
+
mount={mount}
|
|
352
273
|
/>
|
|
353
|
-
|
|
354
|
-
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return (
|
|
277
|
+
<EsmPluginHost
|
|
278
|
+
source={source}
|
|
279
|
+
socketUrl={socketUrl}
|
|
280
|
+
pluginId={pluginId}
|
|
281
|
+
mount={mount}
|
|
282
|
+
/>
|
|
355
283
|
);
|
|
356
284
|
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="@repo/typescript-config" />
|
package/src/manifest.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
2
|
/**
|
|
2
3
|
* Plugin manifest schema — the single source of truth shared by the front end
|
|
3
4
|
* (nexus reads it) and the back end (rby2 mirrors it for validation).
|
|
@@ -7,7 +8,7 @@
|
|
|
7
8
|
|
|
8
9
|
/** Known contribution points the host understands. */
|
|
9
10
|
export const CONTRIBUTION_POINTS = [
|
|
10
|
-
"
|
|
11
|
+
"toolbar",
|
|
11
12
|
"lnb",
|
|
12
13
|
"move.panel",
|
|
13
14
|
"program.panel",
|
|
@@ -21,12 +22,26 @@ export type ContributionOpens = "popover" | "panel" | "page";
|
|
|
21
22
|
export interface PluginContribution {
|
|
22
23
|
/** Where this contribution mounts. */
|
|
23
24
|
point: ContributionPoint | (string & {});
|
|
25
|
+
/** UI entry name relative to ui/ (without extension). Defaults to "content". */
|
|
26
|
+
ui?: string;
|
|
24
27
|
/** Icon path relative to the plugin root (e.g. "assets/icon.svg"). */
|
|
25
|
-
icon?:
|
|
28
|
+
icon?:
|
|
29
|
+
| string
|
|
30
|
+
| {
|
|
31
|
+
"16"?: string;
|
|
32
|
+
"48"?: string;
|
|
33
|
+
"126"?: string;
|
|
34
|
+
};
|
|
26
35
|
/** Display label. */
|
|
27
|
-
label?: string
|
|
36
|
+
label?: string | Record<string, string>;
|
|
28
37
|
/** For header.toolbar: how the contribution surfaces. */
|
|
29
38
|
opens?: ContributionOpens;
|
|
39
|
+
commandInfo?: {
|
|
40
|
+
name: string;
|
|
41
|
+
icon: string;
|
|
42
|
+
funcName: string;
|
|
43
|
+
args?: Record<string, any>;
|
|
44
|
+
};
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
export interface PluginAction {
|
|
@@ -58,7 +73,13 @@ export interface PluginManifest {
|
|
|
58
73
|
/** Minimum host (Muscat) version this plugin supports. */
|
|
59
74
|
minAppVersion?: string;
|
|
60
75
|
/** Icon path relative to plugin root. */
|
|
61
|
-
|
|
76
|
+
icons:
|
|
77
|
+
| string
|
|
78
|
+
| {
|
|
79
|
+
"16"?: string;
|
|
80
|
+
"48"?: string;
|
|
81
|
+
"128"?: string;
|
|
82
|
+
};
|
|
62
83
|
/** UI / chrome contributions. */
|
|
63
84
|
contributes?: PluginContribution[];
|
|
64
85
|
/** Host actions the plugin can invoke. */
|