@rainbow-robotics/plugin-ui 1.3.8-dev.4 → 1.10.1-dev.0
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 +246 -246
- package/build-dist.mjs +80 -80
- package/dist/host-ui.js.map +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/react.d.ts +13 -13
- package/dist/react.js.map +1 -1
- package/dist/sdk.js.map +1 -1
- package/dist/viteHost.js.map +1 -1
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/src/host-ui.ts +95 -95
- package/src/host.tsx +277 -277
- package/src/index.d.ts +1 -1
- package/src/manifest.ts +135 -135
- package/src/pluginData.tsx +240 -240
- package/src/react.tsx +13 -13
- package/src/sdk.ts +175 -175
- package/src/viteHost.ts +309 -309
- package/src/ws.ts +256 -256
package/dist/host-ui.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/host-ui.ts"],
|
|
4
|
-
"sourcesContent": ["import { ReactElement } from \"react\";\n\n/** One step of a program tree a plugin hands to the host Program page. */\nexport interface PluginProgramStep {\n /** Muscat command name, e.g. \"MoveJ\", \"Point\", \"Wait\", \"Repeat\", \"Condition\". */\n command: string;\n args?: Record<string, unknown>;\n summary?: string;\n children?: PluginProgramStep[];\n}\n\nexport interface PluginUIServices {\n /** Replaces the loaded program's main step tree (unsaved edit; the user saves). */\n program: {\n setSteps: (steps: PluginProgramStep[]) => Promise<void>;\n /** Command catalog of the loaded robot (names, default args, nesting rules). */\n getCatalog: () => Promise<unknown[]>;\n };\n alert: (params: {\n title?: string;\n message: string;\n buttonText?: string;\n }) => Promise<void>;\n confirm: (params: {\n title?: string;\n message: string;\n buttonText?: {\n yes: string;\n no: string;\n };\n }) => Promise<boolean>;\n prompt: (params: {\n title?: string;\n message: string;\n value?: string;\n placeholder?: string;\n buttonText?: {\n yes: string;\n no: string;\n };\n }) => Promise<string | undefined>;\n toast: (params: {\n title?: string;\n message: string | ReactElement;\n type?: \"info\" | \"danger\" | \"warning\" | \"caution\" | \"success\";\n actionLabel?: string;\n handleActionClick?: () => void;\n closeOnActionClick?: boolean;\n duration?: number;\n onClose?: () => void;\n }) => void;\n}\n\nlet services: PluginUIServices | null = null;\n\n/** Called by the host when it mounts a plugin. */\nexport function setHostServices(next: PluginUIServices): void {\n services = next;\n}\n\n/** Read the injected services, throwing a clear error if the host forgot. */\nexport function getHostServices(): PluginUIServices {\n if (!services) {\n throw new Error(\n \"[plugin-ui] services not set \u2014 the plugin must be mounted by a Muscat host.\",\n );\n }\n return services;\n}\n\n/** Open a host alert dialog from a plugin. */\nexport function alert(\n params: Parameters<PluginUIServices[\"alert\"]>[0],\n): Promise<void> {\n return getHostServices().alert(params);\n}\n\n/** Open a host confirm dialog from a plugin. Resolves true/false. */\nexport function confirm(\n params: Parameters<PluginUIServices[\"confirm\"]>[0],\n): Promise<boolean> {\n return getHostServices().confirm(params);\n}\n\n/** Open a host prompt dialog from a plugin. Resolves the entered value, or undefined if cancelled. */\nexport function prompt(\n params: Parameters<PluginUIServices[\"prompt\"]>[0],\n): Promise<string | undefined> {\n return getHostServices().prompt(params);\n}\n\n/** Show a host toast from a plugin. */\nexport function toast(params: Parameters<PluginUIServices[\"toast\"]>[0]): void {\n getHostServices().toast(params);\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { ReactElement } from \"react\";\r\n\r\n/** One step of a program tree a plugin hands to the host Program page. */\r\nexport interface PluginProgramStep {\r\n /** Muscat command name, e.g. \"MoveJ\", \"Point\", \"Wait\", \"Repeat\", \"Condition\". */\r\n command: string;\r\n args?: Record<string, unknown>;\r\n summary?: string;\r\n children?: PluginProgramStep[];\r\n}\r\n\r\nexport interface PluginUIServices {\r\n /** Replaces the loaded program's main step tree (unsaved edit; the user saves). */\r\n program: {\r\n setSteps: (steps: PluginProgramStep[]) => Promise<void>;\r\n /** Command catalog of the loaded robot (names, default args, nesting rules). */\r\n getCatalog: () => Promise<unknown[]>;\r\n };\r\n alert: (params: {\r\n title?: string;\r\n message: string;\r\n buttonText?: string;\r\n }) => Promise<void>;\r\n confirm: (params: {\r\n title?: string;\r\n message: string;\r\n buttonText?: {\r\n yes: string;\r\n no: string;\r\n };\r\n }) => Promise<boolean>;\r\n prompt: (params: {\r\n title?: string;\r\n message: string;\r\n value?: string;\r\n placeholder?: string;\r\n buttonText?: {\r\n yes: string;\r\n no: string;\r\n };\r\n }) => Promise<string | undefined>;\r\n toast: (params: {\r\n title?: string;\r\n message: string | ReactElement;\r\n type?: \"info\" | \"danger\" | \"warning\" | \"caution\" | \"success\";\r\n actionLabel?: string;\r\n handleActionClick?: () => void;\r\n closeOnActionClick?: boolean;\r\n duration?: number;\r\n onClose?: () => void;\r\n }) => void;\r\n}\r\n\r\nlet services: PluginUIServices | null = null;\r\n\r\n/** Called by the host when it mounts a plugin. */\r\nexport function setHostServices(next: PluginUIServices): void {\r\n services = next;\r\n}\r\n\r\n/** Read the injected services, throwing a clear error if the host forgot. */\r\nexport function getHostServices(): PluginUIServices {\r\n if (!services) {\r\n throw new Error(\r\n \"[plugin-ui] services not set \u2014 the plugin must be mounted by a Muscat host.\",\r\n );\r\n }\r\n return services;\r\n}\r\n\r\n/** Open a host alert dialog from a plugin. */\r\nexport function alert(\r\n params: Parameters<PluginUIServices[\"alert\"]>[0],\r\n): Promise<void> {\r\n return getHostServices().alert(params);\r\n}\r\n\r\n/** Open a host confirm dialog from a plugin. Resolves true/false. */\r\nexport function confirm(\r\n params: Parameters<PluginUIServices[\"confirm\"]>[0],\r\n): Promise<boolean> {\r\n return getHostServices().confirm(params);\r\n}\r\n\r\n/** Open a host prompt dialog from a plugin. Resolves the entered value, or undefined if cancelled. */\r\nexport function prompt(\r\n params: Parameters<PluginUIServices[\"prompt\"]>[0],\r\n): Promise<string | undefined> {\r\n return getHostServices().prompt(params);\r\n}\r\n\r\n/** Show a host toast from a plugin. */\r\nexport function toast(params: Parameters<PluginUIServices[\"toast\"]>[0]): void {\r\n getHostServices().toast(params);\r\n}\r\n"],
|
|
5
5
|
"mappings": "AAqDA,IAAI,WAAoC;AAGjC,SAAS,gBAAgB,MAA8B;AAC5D,aAAW;AACb;AAGO,SAAS,kBAAoC;AAClD,MAAI,CAAC,UAAU;AACb,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,MACd,QACe;AACf,SAAO,gBAAgB,EAAE,MAAM,MAAM;AACvC;AAGO,SAAS,QACd,QACkB;AAClB,SAAO,gBAAgB,EAAE,QAAQ,MAAM;AACzC;AAGO,SAAS,OACd,QAC6B;AAC7B,SAAO,gBAAgB,EAAE,OAAO,MAAM;AACxC;AAGO,SAAS,MAAM,QAAwD;AAC5E,kBAAgB,EAAE,MAAM,MAAM;AAChC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/manifest.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/manifest.ts"],
|
|
4
|
-
"sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Plugin manifest schema \u2014 the single source of truth shared by the front end\n * (nexus reads it) and the back end (rby2 mirrors it for validation).\n *\n * Evolve additively only.\n */\n\n/** Known contribution points the host understands. */\nexport const CONTRIBUTION_POINTS = [\n \"toolbar\",\n \"lnb\",\n \"move.panel\",\n \"program.panel\",\n] as const;\n\nexport type ContributionPoint = (typeof CONTRIBUTION_POINTS)[number];\n\n/** How a header.toolbar contribution opens when clicked. */\nexport type ContributionOpens = \"popover\" | \"panel\" | \"page\";\n\nexport interface PluginContribution {\n /** Where this contribution mounts. */\n point: ContributionPoint | (string & {});\n /** UI entry name relative to ui/ (without extension). Defaults to \"content\". */\n ui?: string;\n /** Icon path relative to the plugin root (e.g. \"assets/icon.svg\"). */\n icon?:\n | string\n | {\n \"16\"?: string;\n \"48\"?: string;\n \"126\"?: string;\n };\n /** Display label. */\n label?: string | Record<string, string>;\n /** For header.toolbar: how the contribution surfaces. */\n opens?: ContributionOpens;\n commandInfo?: {\n name: string;\n icon: string;\n funcName: string;\n args?: Record<string, any>;\n };\n}\n\nexport interface PluginAction {\n /** Action id referenced by `rb.action(id, payload)`. */\n id: string;\n /** Transport topic (Zenoh / ROS2) the back end routes to. */\n topic: string;\n /** Optional payload schema hint (field \u2192 type name). */\n schema?: Record<string, string>;\n}\n\n/** Allowed variable value types exposed to pyfm. */\nexport const VARIABLE_TYPES = [\"number\", \"string\", \"boolean\", \"array\"] as const;\n\nexport type PluginVariableType = (typeof VARIABLE_TYPES)[number];\n\n/** A variable the plugin pre-declares for use in pyfm programs. */\nexport interface PluginVariable {\n /** Identifier, must start with \"RBP_\" (then letters, digits, underscore). Unique per plugin. */\n name: `RBP_${string}`;\n type: PluginVariableType;\n /** Initial value \u2014 must match `type`. */\n init: number | string | boolean | any[];\n /** Optional. Without it the UI cannot explain what the variable is for. */\n description?: string | Record<string, string>;\n}\n\nexport type PluginRuntime =\n | { type: \"none\" }\n | { type: \"python\"; entry: string }\n | {\n type: \"ros2\";\n package: string;\n launch: string;\n rosClient: \"rclpy\" | \"rclcpp\";\n };\n\nexport interface PluginManifest {\n /** Reverse-DNS unique id, e.g. \"com.rainbow.gripper-tool\". */\n id: string;\n /** Human-readable name. */\n name: string;\n /** Plugin version (semver). */\n version: string;\n /** Minimum host (Muscat) version this plugin supports. */\n minAppVersion?: string;\n /** Icon path relative to plugin root. */\n icons:\n | string\n | {\n \"16\"?: string;\n \"48\"?: string;\n \"128\"?: string;\n };\n /** UI / chrome contributions. */\n contributes?: PluginContribution[];\n /** Host actions the plugin can invoke. */\n actions?: PluginAction[];\n /** Variables pre-declared for pyfm; the variable picker UI lists these. */\n variables?: PluginVariable[];\n /** Back-end runtime declaration. */\n runtime?: PluginRuntime;\n}\n\n/**\n * Lightweight structural check. Returns the list of problems (empty = valid).\n * Kept dependency-free so both front and back ends can reuse it; richer\n * validation (zod) can layer on top later without breaking this contract.\n */\nexport function validateManifest(value: unknown): string[] {\n const errors: string[] = [];\n if (typeof value !== \"object\" || value === null) {\n return [\"manifest must be an object\"];\n }\n const m = value as Record<string, unknown>;\n if (typeof m.id !== \"string\" || m.id.length === 0)\n errors.push(\"manifest.id is required (string)\");\n else if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*(\\.[a-z][a-z0-9]*(-[a-z0-9]+)*)*$/.test(m.id))\n errors.push('manifest.id must be \".\"-separated parts of lowercase letters/digits and single \"-\", each starting with a letter (e.g. com.example.my-plugin)');\n if (typeof m.name !== \"string\" || m.name.length === 0)\n errors.push(\"manifest.name is required (string)\");\n if (typeof m.version !== \"string\" || m.version.length === 0)\n errors.push(\"manifest.version is required (string)\");\n if (m.contributes !== undefined && !Array.isArray(m.contributes))\n errors.push(\"manifest.contributes must be an array\");\n if (m.actions !== undefined && !Array.isArray(m.actions))\n errors.push(\"manifest.actions must be an array\");\n if (m.variables !== undefined && !Array.isArray(m.variables))\n errors.push(\"manifest.variables must be an array\");\n return errors;\n}\n"],
|
|
4
|
+
"sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\r\n/**\r\n * Plugin manifest schema \u2014 the single source of truth shared by the front end\r\n * (nexus reads it) and the back end (rby2 mirrors it for validation).\r\n *\r\n * Evolve additively only.\r\n */\r\n\r\n/** Known contribution points the host understands. */\r\nexport const CONTRIBUTION_POINTS = [\r\n \"toolbar\",\r\n \"lnb\",\r\n \"move.panel\",\r\n \"program.panel\",\r\n] as const;\r\n\r\nexport type ContributionPoint = (typeof CONTRIBUTION_POINTS)[number];\r\n\r\n/** How a header.toolbar contribution opens when clicked. */\r\nexport type ContributionOpens = \"popover\" | \"panel\" | \"page\";\r\n\r\nexport interface PluginContribution {\r\n /** Where this contribution mounts. */\r\n point: ContributionPoint | (string & {});\r\n /** UI entry name relative to ui/ (without extension). Defaults to \"content\". */\r\n ui?: string;\r\n /** Icon path relative to the plugin root (e.g. \"assets/icon.svg\"). */\r\n icon?:\r\n | string\r\n | {\r\n \"16\"?: string;\r\n \"48\"?: string;\r\n \"126\"?: string;\r\n };\r\n /** Display label. */\r\n label?: string | Record<string, string>;\r\n /** For header.toolbar: how the contribution surfaces. */\r\n opens?: ContributionOpens;\r\n commandInfo?: {\r\n name: string;\r\n icon: string;\r\n funcName: string;\r\n args?: Record<string, any>;\r\n };\r\n}\r\n\r\nexport interface PluginAction {\r\n /** Action id referenced by `rb.action(id, payload)`. */\r\n id: string;\r\n /** Transport topic (Zenoh / ROS2) the back end routes to. */\r\n topic: string;\r\n /** Optional payload schema hint (field \u2192 type name). */\r\n schema?: Record<string, string>;\r\n}\r\n\r\n/** Allowed variable value types exposed to pyfm. */\r\nexport const VARIABLE_TYPES = [\"number\", \"string\", \"boolean\", \"array\"] as const;\r\n\r\nexport type PluginVariableType = (typeof VARIABLE_TYPES)[number];\r\n\r\n/** A variable the plugin pre-declares for use in pyfm programs. */\r\nexport interface PluginVariable {\r\n /** Identifier, must start with \"RBP_\" (then letters, digits, underscore). Unique per plugin. */\r\n name: `RBP_${string}`;\r\n type: PluginVariableType;\r\n /** Initial value \u2014 must match `type`. */\r\n init: number | string | boolean | any[];\r\n /** Optional. Without it the UI cannot explain what the variable is for. */\r\n description?: string | Record<string, string>;\r\n}\r\n\r\nexport type PluginRuntime =\r\n | { type: \"none\" }\r\n | { type: \"python\"; entry: string }\r\n | {\r\n type: \"ros2\";\r\n package: string;\r\n launch: string;\r\n rosClient: \"rclpy\" | \"rclcpp\";\r\n };\r\n\r\nexport interface PluginManifest {\r\n /** Reverse-DNS unique id, e.g. \"com.rainbow.gripper-tool\". */\r\n id: string;\r\n /** Human-readable name. */\r\n name: string;\r\n /** Plugin version (semver). */\r\n version: string;\r\n /** Minimum host (Muscat) version this plugin supports. */\r\n minAppVersion?: string;\r\n /** Icon path relative to plugin root. */\r\n icons:\r\n | string\r\n | {\r\n \"16\"?: string;\r\n \"48\"?: string;\r\n \"128\"?: string;\r\n };\r\n /** UI / chrome contributions. */\r\n contributes?: PluginContribution[];\r\n /** Host actions the plugin can invoke. */\r\n actions?: PluginAction[];\r\n /** Variables pre-declared for pyfm; the variable picker UI lists these. */\r\n variables?: PluginVariable[];\r\n /** Back-end runtime declaration. */\r\n runtime?: PluginRuntime;\r\n}\r\n\r\n/**\r\n * Lightweight structural check. Returns the list of problems (empty = valid).\r\n * Kept dependency-free so both front and back ends can reuse it; richer\r\n * validation (zod) can layer on top later without breaking this contract.\r\n */\r\nexport function validateManifest(value: unknown): string[] {\r\n const errors: string[] = [];\r\n if (typeof value !== \"object\" || value === null) {\r\n return [\"manifest must be an object\"];\r\n }\r\n const m = value as Record<string, unknown>;\r\n if (typeof m.id !== \"string\" || m.id.length === 0)\r\n errors.push(\"manifest.id is required (string)\");\r\n else if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*(\\.[a-z][a-z0-9]*(-[a-z0-9]+)*)*$/.test(m.id))\r\n errors.push('manifest.id must be \".\"-separated parts of lowercase letters/digits and single \"-\", each starting with a letter (e.g. com.example.my-plugin)');\r\n if (typeof m.name !== \"string\" || m.name.length === 0)\r\n errors.push(\"manifest.name is required (string)\");\r\n if (typeof m.version !== \"string\" || m.version.length === 0)\r\n errors.push(\"manifest.version is required (string)\");\r\n if (m.contributes !== undefined && !Array.isArray(m.contributes))\r\n errors.push(\"manifest.contributes must be an array\");\r\n if (m.actions !== undefined && !Array.isArray(m.actions))\r\n errors.push(\"manifest.actions must be an array\");\r\n if (m.variables !== undefined && !Array.isArray(m.variables))\r\n errors.push(\"manifest.variables must be an array\");\r\n return errors;\r\n}\r\n"],
|
|
5
5
|
"mappings": "AASO,MAAM,sBAAsB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AA0CO,MAAM,iBAAiB,CAAC,UAAU,UAAU,WAAW,OAAO;AAyD9D,SAAS,iBAAiB,OAA0B;AACzD,QAAM,SAAmB,CAAC;AAC1B,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,WAAO,CAAC,4BAA4B;AAAA,EACtC;AACA,QAAM,IAAI;AACV,MAAI,OAAO,EAAE,OAAO,YAAY,EAAE,GAAG,WAAW;AAC9C,WAAO,KAAK,kCAAkC;AAAA,WACvC,CAAC,gEAAgE,KAAK,EAAE,EAAE;AACjF,WAAO,KAAK,8IAA8I;AAC5J,MAAI,OAAO,EAAE,SAAS,YAAY,EAAE,KAAK,WAAW;AAClD,WAAO,KAAK,oCAAoC;AAClD,MAAI,OAAO,EAAE,YAAY,YAAY,EAAE,QAAQ,WAAW;AACxD,WAAO,KAAK,uCAAuC;AACrD,MAAI,EAAE,gBAAgB,UAAa,CAAC,MAAM,QAAQ,EAAE,WAAW;AAC7D,WAAO,KAAK,uCAAuC;AACrD,MAAI,EAAE,YAAY,UAAa,CAAC,MAAM,QAAQ,EAAE,OAAO;AACrD,WAAO,KAAK,mCAAmC;AACjD,MAAI,EAAE,cAAc,UAAa,CAAC,MAAM,QAAQ,EAAE,SAAS;AACzD,WAAO,KAAK,qCAAqC;AACnD,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/react.d.ts
CHANGED
|
@@ -527,18 +527,18 @@ declare const textAreaVariants: (props?: {
|
|
|
527
527
|
} & class_variance_authority_types.ClassProp) => string;
|
|
528
528
|
type TextAreaVariantProps = VariantProps<typeof textAreaVariants>;
|
|
529
529
|
|
|
530
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
interface ModalPropsType<T = AllowAny> {
|
|
534
|
-
children?: ReactNode;
|
|
535
|
-
className?: string;
|
|
536
|
-
open: boolean;
|
|
537
|
-
close: () => void | Promise<unknown>;
|
|
538
|
-
onOpenChange: (open: boolean) => void;
|
|
539
|
-
resolve: (value: T, useClose?: boolean) => void;
|
|
540
|
-
enableDimClose?: boolean;
|
|
541
|
-
hideCloseButton?: boolean;
|
|
530
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
interface ModalPropsType<T = AllowAny> {
|
|
534
|
+
children?: ReactNode;
|
|
535
|
+
className?: string;
|
|
536
|
+
open: boolean;
|
|
537
|
+
close: () => void | Promise<unknown>;
|
|
538
|
+
onOpenChange: (open: boolean) => void;
|
|
539
|
+
resolve: (value: T, useClose?: boolean) => void;
|
|
540
|
+
enableDimClose?: boolean;
|
|
541
|
+
hideCloseButton?: boolean;
|
|
542
542
|
}
|
|
543
543
|
|
|
544
544
|
type KeypadOption = {
|
|
@@ -2326,7 +2326,7 @@ interface PropsType$5 {
|
|
|
2326
2326
|
declare function RBSwitchField({ children, className, controlPlacement, description, descriptionClassName, invalid, label, labelClassName, size, type, }: PropsType$5): react_jsx_runtime.JSX.Element;
|
|
2327
2327
|
|
|
2328
2328
|
declare const toggleRootVariants: (props?: {
|
|
2329
|
-
size?: "sm" | "
|
|
2329
|
+
size?: "sm" | "default" | "lg";
|
|
2330
2330
|
variant?: "default" | "outline";
|
|
2331
2331
|
} & class_variance_authority_types.ClassProp) => string;
|
|
2332
2332
|
type ToggleVariantProps = VariantProps<typeof toggleRootVariants>;
|
package/dist/react.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/react.tsx"],
|
|
4
|
-
"sourcesContent": ["/**\n * Author component surface \u2014 re-exports the @repo/rb-components new_components\n * barrel, plus the sdk hooks.\n *\n * import { RBSlider, RBLabelButton, useRb } from \"@rainbow-robotics/plugin-ui/react\";\n *\n * The runtime import-map shim (./viteHost.ts) reads this module's exports\n * rather than a hardcoded list, so the two stay in sync.\n */\nexport * from \"@repo/rb-components/new_components\";\n\nexport { useRb, useRbFormData, useRbSave } from \"./sdk.js\";\nexport type { RbSdk } from \"./sdk\";\n"],
|
|
4
|
+
"sourcesContent": ["/**\r\n * Author component surface \u2014 re-exports the @repo/rb-components new_components\r\n * barrel, plus the sdk hooks.\r\n *\r\n * import { RBSlider, RBLabelButton, useRb } from \"@rainbow-robotics/plugin-ui/react\";\r\n *\r\n * The runtime import-map shim (./viteHost.ts) reads this module's exports\r\n * rather than a hardcoded list, so the two stay in sync.\r\n */\r\nexport * from \"@repo/rb-components/new_components\";\r\n\r\nexport { useRb, useRbFormData, useRbSave } from \"./sdk.js\";\r\nexport type { RbSdk } from \"./sdk\";\r\n"],
|
|
5
5
|
"mappings": "AASA,cAAc;AAEd,SAAS,OAAO,eAAe,iBAAiB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/sdk.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/sdk.ts"],
|
|
4
|
-
"sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Plugin runtime SDK (`useRb()`).\n *\n * pub/sub/action/serve talk to the Zenoh router over WebSocket. Topics are\n * namespaced to {pluginId}/... ; a leading \"/\" makes the path absolute.\n * alert/confirm/prompt/toast delegate to the host.\n */\nimport { createContext, useContext, useEffect, useRef } from \"react\";\nimport type {\n WsSubscriber as Subscriber,\n WsQueryable as Queryable,\n} from \"./ws\";\n// Type-only import \u2014 no runtime coupling to host-ui.ts (which itself has no\n// dependency back on sdk.ts, so this stays one-directional).\nimport type { PluginUIServices } from \"./host-ui\";\n\nexport type RbNotifyType = \"info\" | \"success\" | \"warning\" | \"error\";\n\nexport interface RbSdk {\n /**\n * Publish JSON payload to a topic.\n * \"sensor/data\" \u2192 {pluginId}/sensor/data\n * \"/broadcast/x\" \u2192 broadcast/x (absolute)\n */\n pub: <P = any>(topic: string, payload: P) => Promise<void>;\n\n /** Subscribe to a topic. Returns a handle with undeclare(). */\n sub: <P = any>(\n topic: string,\n handler: (payload: P) => void,\n ) => Promise<Subscriber>;\n\n /**\n * Send a request and await the first reply (Zenoh query).\n * Use this to call a backend handler registered with rb.serve() / ZenohRouter.\n */\n action: <R = any, P = any>(topic: string, payload?: P) => Promise<R>;\n\n /**\n * Register a queryable \u2014 the frontend responds to backend queries.\n * Mirror of rb.action() on the other side.\n */\n serve: <P = any, R = any>(\n topic: string,\n handler: (payload?: P) => R | Promise<R>,\n ) => Promise<Queryable>;\n\n /** Show a transient notification through the host's toast system. */\n toast: PluginUIServices[\"toast\"];\n\n /** Show a host alert dialog. */\n alert: PluginUIServices[\"alert\"];\n /** Show a host confirm dialog. Resolves true/false. */\n confirm: PluginUIServices[\"confirm\"];\n /** Show a host prompt dialog. Resolves the entered value, or undefined if cancelled. */\n prompt: PluginUIServices[\"prompt\"];\n\n /** Push a step tree into the host Program page as an unsaved edit. */\n program: PluginUIServices[\"program\"];\n\n /**\n * Host-provided initial data for this mount \u2014 e.g. the previously-saved\n * values when the host re-opens a form. `undefined` for a fresh mount.\n * Prefer the `useRbFormData()` hook over reading this directly.\n */\n data?: unknown;\n\n /**\n * Register a getter the host calls (synchronously) to collect this plugin's\n * save payload \u2014 e.g. when the host's Save button is clicked. The returned\n * value must be JSON-serialisable (the host persists it). Returns an\n * unregister fn. Prefer the `useRbSave()` hook.\n */\n onSave: (getPayload: () => unknown) => () => void;\n\n /**\n * PCs allowed to talk to this plugin's ROS 2 nodes. ROS discovery is robot-local\n * (ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST); Muscat keeps this list per plugin, starts\n * the plugin with ROS_STATIC_PEERS set to it, and restarts the plugin when it changes.\n */\n rosPeers: {\n get: () => Promise<string[]>;\n /** Saves the list and restarts the plugin backend. Rejects on an invalid IP. */\n set: (peers: string[]) => Promise<string[]>;\n };\n}\n\nconst noopSdk: RbSdk = {\n pub: async <P = any>(_topic?: string, _payload?: P) => {\n console.warn(\"[plugin-ui] rb.pub called outside of a host context\");\n },\n sub: async <P = any>(_topic?: string, _handler?: (payload: P) => void) => {\n console.warn(\"[plugin-ui] rb.sub called outside of a host context\");\n return { undeclare: async () => {} } as unknown as Subscriber;\n },\n action: async <R = any, P = any>(_topic?: string, _payload?: P) => {\n console.warn(\"[plugin-ui] rb.action called outside of a host context\");\n return undefined as R;\n },\n serve: async <P = any, R = any>(\n _topic?: string,\n _handler?: (payload?: P) => R,\n ) => {\n console.warn(\"[plugin-ui] rb.serve called outside of a host context\");\n return { undeclare: async () => {} } as unknown as Queryable;\n },\n toast: (params) => {\n console.warn(\"[plugin-ui] rb.toast (no host):\", params);\n },\n alert: async (_) => {\n console.warn(\"[plugin-ui] rb.alert called outside of a host context\");\n },\n confirm: async (_) => {\n console.warn(\"[plugin-ui] rb.confirm called outside of a host context\");\n return false;\n },\n prompt: async (_) => {\n console.warn(\"[plugin-ui] rb.prompt called outside of a host context\");\n return undefined;\n },\n program: {\n setSteps: async () => {\n console.warn(\"[plugin-ui] rb.program called outside of a host context\");\n },\n getCatalog: async () => {\n console.warn(\"[plugin-ui] rb.program called outside of a host context\");\n return [];\n },\n },\n data: undefined,\n onSave: () => () => {},\n rosPeers: {\n get: async () => {\n console.warn(\"[plugin-ui] rb.rosPeers called outside of a host context\");\n return [];\n },\n set: async () => {\n console.warn(\"[plugin-ui] rb.rosPeers called outside of a host context\");\n return [];\n },\n },\n};\n\nexport const RbContext = createContext<RbSdk>(noopSdk);\n\n/** Access the host SDK from within a plugin component. */\nexport function useRb(): RbSdk {\n return useContext(RbContext);\n}\n\n/**\n * Read the host-provided initial data for this mount (e.g. previously-saved\n * form values). `undefined` for a fresh mount.\n *\n * const saved = useRbFormData<MyArgs>();\n * const methods = useForm({ defaultValues: saved ?? DEFAULTS });\n */\nexport function useRbFormData<T = unknown>(): T | undefined {\n return useRb().data as T | undefined;\n}\n\n/**\n * Register a getter the host calls to collect this plugin's save payload when\n * the host's Save is clicked. The latest `getPayload` is always used, so it can\n * close over current state. The returned value must be JSON-serialisable.\n *\n * useRbSave(() => ({ args: getValues(), summary: buildSummary() }));\n */\nexport function useRbSave<T = unknown>(getPayload: () => T): void {\n const rb = useRb();\n const ref = useRef(getPayload);\n ref.current = getPayload;\n useEffect(() => rb.onSave(() => ref.current()), [rb]);\n}\n"],
|
|
4
|
+
"sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\r\n/**\r\n * Plugin runtime SDK (`useRb()`).\r\n *\r\n * pub/sub/action/serve talk to the Zenoh router over WebSocket. Topics are\r\n * namespaced to {pluginId}/... ; a leading \"/\" makes the path absolute.\r\n * alert/confirm/prompt/toast delegate to the host.\r\n */\r\nimport { createContext, useContext, useEffect, useRef } from \"react\";\r\nimport type {\r\n WsSubscriber as Subscriber,\r\n WsQueryable as Queryable,\r\n} from \"./ws\";\r\n// Type-only import \u2014 no runtime coupling to host-ui.ts (which itself has no\r\n// dependency back on sdk.ts, so this stays one-directional).\r\nimport type { PluginUIServices } from \"./host-ui\";\r\n\r\nexport type RbNotifyType = \"info\" | \"success\" | \"warning\" | \"error\";\r\n\r\nexport interface RbSdk {\r\n /**\r\n * Publish JSON payload to a topic.\r\n * \"sensor/data\" \u2192 {pluginId}/sensor/data\r\n * \"/broadcast/x\" \u2192 broadcast/x (absolute)\r\n */\r\n pub: <P = any>(topic: string, payload: P) => Promise<void>;\r\n\r\n /** Subscribe to a topic. Returns a handle with undeclare(). */\r\n sub: <P = any>(\r\n topic: string,\r\n handler: (payload: P) => void,\r\n ) => Promise<Subscriber>;\r\n\r\n /**\r\n * Send a request and await the first reply (Zenoh query).\r\n * Use this to call a backend handler registered with rb.serve() / ZenohRouter.\r\n */\r\n action: <R = any, P = any>(topic: string, payload?: P) => Promise<R>;\r\n\r\n /**\r\n * Register a queryable \u2014 the frontend responds to backend queries.\r\n * Mirror of rb.action() on the other side.\r\n */\r\n serve: <P = any, R = any>(\r\n topic: string,\r\n handler: (payload?: P) => R | Promise<R>,\r\n ) => Promise<Queryable>;\r\n\r\n /** Show a transient notification through the host's toast system. */\r\n toast: PluginUIServices[\"toast\"];\r\n\r\n /** Show a host alert dialog. */\r\n alert: PluginUIServices[\"alert\"];\r\n /** Show a host confirm dialog. Resolves true/false. */\r\n confirm: PluginUIServices[\"confirm\"];\r\n /** Show a host prompt dialog. Resolves the entered value, or undefined if cancelled. */\r\n prompt: PluginUIServices[\"prompt\"];\r\n\r\n /** Push a step tree into the host Program page as an unsaved edit. */\r\n program: PluginUIServices[\"program\"];\r\n\r\n /**\r\n * Host-provided initial data for this mount \u2014 e.g. the previously-saved\r\n * values when the host re-opens a form. `undefined` for a fresh mount.\r\n * Prefer the `useRbFormData()` hook over reading this directly.\r\n */\r\n data?: unknown;\r\n\r\n /**\r\n * Register a getter the host calls (synchronously) to collect this plugin's\r\n * save payload \u2014 e.g. when the host's Save button is clicked. The returned\r\n * value must be JSON-serialisable (the host persists it). Returns an\r\n * unregister fn. Prefer the `useRbSave()` hook.\r\n */\r\n onSave: (getPayload: () => unknown) => () => void;\r\n\r\n /**\r\n * PCs allowed to talk to this plugin's ROS 2 nodes. ROS discovery is robot-local\r\n * (ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST); Muscat keeps this list per plugin, starts\r\n * the plugin with ROS_STATIC_PEERS set to it, and restarts the plugin when it changes.\r\n */\r\n rosPeers: {\r\n get: () => Promise<string[]>;\r\n /** Saves the list and restarts the plugin backend. Rejects on an invalid IP. */\r\n set: (peers: string[]) => Promise<string[]>;\r\n };\r\n}\r\n\r\nconst noopSdk: RbSdk = {\r\n pub: async <P = any>(_topic?: string, _payload?: P) => {\r\n console.warn(\"[plugin-ui] rb.pub called outside of a host context\");\r\n },\r\n sub: async <P = any>(_topic?: string, _handler?: (payload: P) => void) => {\r\n console.warn(\"[plugin-ui] rb.sub called outside of a host context\");\r\n return { undeclare: async () => {} } as unknown as Subscriber;\r\n },\r\n action: async <R = any, P = any>(_topic?: string, _payload?: P) => {\r\n console.warn(\"[plugin-ui] rb.action called outside of a host context\");\r\n return undefined as R;\r\n },\r\n serve: async <P = any, R = any>(\r\n _topic?: string,\r\n _handler?: (payload?: P) => R,\r\n ) => {\r\n console.warn(\"[plugin-ui] rb.serve called outside of a host context\");\r\n return { undeclare: async () => {} } as unknown as Queryable;\r\n },\r\n toast: (params) => {\r\n console.warn(\"[plugin-ui] rb.toast (no host):\", params);\r\n },\r\n alert: async (_) => {\r\n console.warn(\"[plugin-ui] rb.alert called outside of a host context\");\r\n },\r\n confirm: async (_) => {\r\n console.warn(\"[plugin-ui] rb.confirm called outside of a host context\");\r\n return false;\r\n },\r\n prompt: async (_) => {\r\n console.warn(\"[plugin-ui] rb.prompt called outside of a host context\");\r\n return undefined;\r\n },\r\n program: {\r\n setSteps: async () => {\r\n console.warn(\"[plugin-ui] rb.program called outside of a host context\");\r\n },\r\n getCatalog: async () => {\r\n console.warn(\"[plugin-ui] rb.program called outside of a host context\");\r\n return [];\r\n },\r\n },\r\n data: undefined,\r\n onSave: () => () => {},\r\n rosPeers: {\r\n get: async () => {\r\n console.warn(\"[plugin-ui] rb.rosPeers called outside of a host context\");\r\n return [];\r\n },\r\n set: async () => {\r\n console.warn(\"[plugin-ui] rb.rosPeers called outside of a host context\");\r\n return [];\r\n },\r\n },\r\n};\r\n\r\nexport const RbContext = createContext<RbSdk>(noopSdk);\r\n\r\n/** Access the host SDK from within a plugin component. */\r\nexport function useRb(): RbSdk {\r\n return useContext(RbContext);\r\n}\r\n\r\n/**\r\n * Read the host-provided initial data for this mount (e.g. previously-saved\r\n * form values). `undefined` for a fresh mount.\r\n *\r\n * const saved = useRbFormData<MyArgs>();\r\n * const methods = useForm({ defaultValues: saved ?? DEFAULTS });\r\n */\r\nexport function useRbFormData<T = unknown>(): T | undefined {\r\n return useRb().data as T | undefined;\r\n}\r\n\r\n/**\r\n * Register a getter the host calls to collect this plugin's save payload when\r\n * the host's Save is clicked. The latest `getPayload` is always used, so it can\r\n * close over current state. The returned value must be JSON-serialisable.\r\n *\r\n * useRbSave(() => ({ args: getValues(), summary: buildSummary() }));\r\n */\r\nexport function useRbSave<T = unknown>(getPayload: () => T): void {\r\n const rb = useRb();\r\n const ref = useRef(getPayload);\r\n ref.current = getPayload;\r\n useEffect(() => rb.onSave(() => ref.current()), [rb]);\r\n}\r\n"],
|
|
5
5
|
"mappings": "AAQA,SAAS,eAAe,YAAY,WAAW,cAAc;AAgF7D,MAAM,UAAiB;AAAA,EACrB,KAAK,OAAgB,QAAiB,aAAiB;AACrD,YAAQ,KAAK,qDAAqD;AAAA,EACpE;AAAA,EACA,KAAK,OAAgB,QAAiB,aAAoC;AACxE,YAAQ,KAAK,qDAAqD;AAClE,WAAO,EAAE,WAAW,YAAY;AAAA,IAAC,EAAE;AAAA,EACrC;AAAA,EACA,QAAQ,OAAyB,QAAiB,aAAiB;AACjE,YAAQ,KAAK,wDAAwD;AACrE,WAAO;AAAA,EACT;AAAA,EACA,OAAO,OACL,QACA,aACG;AACH,YAAQ,KAAK,uDAAuD;AACpE,WAAO,EAAE,WAAW,YAAY;AAAA,IAAC,EAAE;AAAA,EACrC;AAAA,EACA,OAAO,CAAC,WAAW;AACjB,YAAQ,KAAK,mCAAmC,MAAM;AAAA,EACxD;AAAA,EACA,OAAO,OAAO,MAAM;AAClB,YAAQ,KAAK,uDAAuD;AAAA,EACtE;AAAA,EACA,SAAS,OAAO,MAAM;AACpB,YAAQ,KAAK,yDAAyD;AACtE,WAAO;AAAA,EACT;AAAA,EACA,QAAQ,OAAO,MAAM;AACnB,YAAQ,KAAK,wDAAwD;AACrE,WAAO;AAAA,EACT;AAAA,EACA,SAAS;AAAA,IACP,UAAU,YAAY;AACpB,cAAQ,KAAK,yDAAyD;AAAA,IACxE;AAAA,IACA,YAAY,YAAY;AACtB,cAAQ,KAAK,yDAAyD;AACtE,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA,EACA,MAAM;AAAA,EACN,QAAQ,MAAM,MAAM;AAAA,EAAC;AAAA,EACrB,UAAU;AAAA,IACR,KAAK,YAAY;AACf,cAAQ,KAAK,0DAA0D;AACvE,aAAO,CAAC;AAAA,IACV;AAAA,IACA,KAAK,YAAY;AACf,cAAQ,KAAK,0DAA0D;AACvE,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACF;AAEO,MAAM,YAAY,cAAqB,OAAO;AAG9C,SAAS,QAAe;AAC7B,SAAO,WAAW,SAAS;AAC7B;AASO,SAAS,gBAA4C;AAC1D,SAAO,MAAM,EAAE;AACjB;AASO,SAAS,UAAuB,YAA2B;AAChE,QAAM,KAAK,MAAM;AACjB,QAAM,MAAM,OAAO,UAAU;AAC7B,MAAI,UAAU;AACd,YAAU,MAAM,GAAG,OAAO,MAAM,IAAI,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;AACtD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/viteHost.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/viteHost.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Vite plugins for a HOST app previewing locally-developed plugins.\n * Host-agnostic \u2014 paths are parameters.\n *\n * import {\n * pluginPreviewRuntimePlugin,\n * pluginPreviewAssetsPlugin,\n * isPluginPreviewMode,\n * } from \"@rainbow-robotics/plugin-ui/vite-host\";\n *\n * A built plugin's bare imports (react, rb-components, plugin-ui/*) resolve via\n * the import map this file injects, to the host's already-loaded instances.\n * See ./host.tsx.\n */\nimport type { Plugin } from \"vite\";\nimport { createReadStream, existsSync, readFileSync, statSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { extname, resolve } from \"node:path\";\n\nconst require = createRequire(import.meta.url);\n\nexport const defaultPreviewAssetPrefixes = [\n \"/imgs/\",\n \"/fonts/\",\n \"/models/\",\n \"/sounds/\",\n \"/data/\",\n];\n\nexport const contentTypeByExt: Record<string, string> = {\n \".avif\": \"image/avif\",\n \".css\": \"text/css; charset=utf-8\",\n \".gif\": \"image/gif\",\n \".glb\": \"model/gltf-binary\",\n \".jpeg\": \"image/jpeg\",\n \".jpg\": \"image/jpeg\",\n \".js\": \"text/javascript; charset=utf-8\",\n \".json\": \"application/json; charset=utf-8\",\n \".mp3\": \"audio/mpeg\",\n \".png\": \"image/png\",\n \".svg\": \"image/svg+xml\",\n \".wasm\": \"application/wasm\",\n \".webp\": \"image/webp\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n};\n\nexport const isPluginPreviewMode = (mode: string) => mode === \"plugin-preview\";\n\n/* -------------------------------------------------------------------------- */\n/* Shared-runtime import map (plugins reuse the host's loaded instances) */\n/* -------------------------------------------------------------------------- */\n\nconst SHARED_BASE = \"/plugin-shared\";\n\n// Each shared specifier \u2192 the named exports its shim re-exports from\n// window.__RB_SHARED__[specifier] (set by the host, e.g. nexus/src/pluginShared).\n//\n// `names`: hardcoded list (small, stable modules).\n// `parseNamesFrom`: parse the export names out of a source file's text \u2014 used\n// for react.tsx, which re-exports the whole rb-components barrel (100+ names).\n// We text-parse rather than import it: the barrel is .ts source, which plain\n// Node can't execute.\nconst SHARED_MODULES: Record<\n string,\n {\n file: string;\n names?: string[];\n parseNamesFrom?: { specifier: string; extraNames?: string[] };\n hasDefault?: boolean;\n }\n> = {\n react: {\n file: \"react\",\n hasDefault: true,\n names: [\n \"Children\", \"Component\", \"Fragment\", \"Profiler\", \"PureComponent\",\n \"StrictMode\", \"Suspense\", \"cloneElement\", \"createContext\",\n \"createElement\", \"createRef\", \"forwardRef\", \"isValidElement\", \"lazy\",\n \"memo\", \"startTransition\", \"useCallback\", \"useContext\", \"useDebugValue\",\n \"useDeferredValue\", \"useEffect\", \"useId\", \"useImperativeHandle\",\n \"useInsertionEffect\", \"useLayoutEffect\", \"useMemo\", \"useReducer\",\n \"useRef\", \"useState\", \"useSyncExternalStore\", \"useTransition\", \"version\",\n ],\n },\n \"react/jsx-runtime\": {\n file: \"react_jsx-runtime\",\n names: [\"Fragment\", \"jsx\", \"jsxs\"],\n },\n \"react/jsx-dev-runtime\": {\n file: \"react_jsx-dev-runtime\",\n names: [\"Fragment\", \"jsxDEV\"],\n },\n \"react-dom\": {\n file: \"react-dom\",\n hasDefault: true,\n names: [\n \"__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED\",\n \"createPortal\",\n \"createRoot\",\n \"findDOMNode\",\n \"flushSync\",\n \"hydrate\",\n \"hydrateRoot\",\n \"render\",\n \"unmountComponentAtNode\",\n \"unstable_batchedUpdates\",\n \"unstable_renderSubtreeIntoContainer\",\n \"version\",\n ],\n },\n \"react-dom/client\": {\n file: \"react-dom_client\",\n names: [\"createRoot\", \"hydrateRoot\"],\n },\n \"@rainbow-robotics/plugin-ui/react\": {\n file: \"plugin-ui_react\",\n // react.tsx re-exports the full rb-components barrel (export *) plus the sdk\n // hooks.\n parseNamesFrom: {\n specifier: \"@repo/rb-components/new_components\",\n extraNames: [\"useRb\", \"useRbFormData\", \"useRbSave\"],\n },\n },\n \"@rainbow-robotics/plugin-ui/sdk\": {\n file: \"plugin-ui_sdk\",\n names: [\"RbContext\", \"useRb\", \"useRbFormData\", \"useRbSave\"],\n },\n \"@rainbow-robotics/plugin-ui/host-ui\": {\n file: \"plugin-ui_host-ui\",\n names: [\n \"alert\",\n \"confirm\",\n \"getHostServices\",\n \"prompt\",\n \"setHostServices\",\n \"toast\",\n ],\n },\n \"@rainbow-robotics/plugin-ui/ws\": {\n file: \"plugin-ui_ws\",\n names: [\"RbWs\", \"createLazyRbSdk\"],\n },\n};\n\n// Parses `export { a, b as c, ... }` value names out of a source file's text\n// (skips `export type { ... }` blocks and individual `type X` entries within\n// a mixed block \u2014 types don't exist as runtime module properties). Static\n// text parsing, not module execution, so it works for .ts source files with\n// no loader and has no ESM-resolution footguns.\nfunction parseExportedValueNames(source: string): string[] {\n const names = new Set<string>();\n const blockRe = /export\\s+(type\\s+)?\\{([^}]*)\\}/g;\n let match: RegExpExecArray | null;\n while ((match = blockRe.exec(source))) {\n if (match[1]) continue; // whole `export type { ... }` block\n for (const rawEntry of match[2].split(\",\")) {\n const entry = rawEntry.trim();\n if (!entry || entry.startsWith(\"type \")) continue;\n const asMatch = entry.match(/^\\S+\\s+as\\s+(\\S+)$/);\n names.add(asMatch ? asMatch[1] : entry);\n }\n }\n return [...names];\n}\n\nconst nameCache = new Map<string, string[]>();\n\nfunction resolveNames(spec: string): string[] {\n const mod = SHARED_MODULES[spec];\n if (mod.names) return mod.names;\n if (!mod.parseNamesFrom) {\n throw new Error(`[plugin-ui] ${spec} has neither \"names\" nor \"parseNamesFrom\"`);\n }\n if (!nameCache.has(spec)) {\n const { specifier, extraNames = [] } = mod.parseNamesFrom;\n const sourcePath = require.resolve(specifier);\n const parsed = parseExportedValueNames(readFileSync(sourcePath, \"utf8\"));\n nameCache.set(spec, [...new Set([...parsed, ...extraNames])]);\n }\n return nameCache.get(spec)!;\n}\n\nfunction shimSource(spec: string): string {\n const mod = SHARED_MODULES[spec];\n const names = resolveNames(spec);\n const g = `(globalThis.__RB_SHARED__||{})[${JSON.stringify(spec)}]`;\n return (\n `const m = ${g};\\n` +\n `if (!m) throw new Error(\"[plugin-ui] host shared module not registered: ${spec}\");\\n` +\n `export const { ${names.join(\", \")} } = m;\\n` +\n (mod.hasDefault ? `export default (m.default ?? m);\\n` : \"\")\n );\n}\n\nconst fileToSpec = Object.fromEntries(\n Object.entries(SHARED_MODULES).map(([spec, { file }]) => [file, spec]),\n);\n\nfunction importMapScript(): string {\n const imports: Record<string, string> = {};\n for (const [spec, { file }] of Object.entries(SHARED_MODULES)) {\n imports[spec] = `${SHARED_BASE}/${file}.js`;\n }\n return `<script type=\"importmap\">${JSON.stringify({ imports })}</script>`;\n}\n\n/**\n * Serves the shared-runtime shim modules at /plugin-shared/*.js and injects the\n * import map so a dynamically-imported plugin's bare `import \"react\"` /\n * `import \"@rainbow-robotics/plugin-ui/react\"` etc. resolve to the host's OWN\n * instances (via window.__RB_SHARED__). Applied in all modes \u2014 any nexus build\n * that can host plugins needs it.\n */\nexport const pluginSharedRuntimePlugin = (): Plugin => ({\n name: \"plugin-ui-shared-runtime\",\n transformIndexHtml(html) {\n return html.replace(\"</head>\", ` ${importMapScript()}\\n </head>`);\n },\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = req.url?.split(\"?\")[0];\n if (!url?.startsWith(`${SHARED_BASE}/`)) return next();\n const file = url.slice(`${SHARED_BASE}/`.length).replace(/\\.js$/, \"\");\n const spec = fileToSpec[file];\n if (!spec) return next();\n res.setHeader(\"Content-Type\", \"text/javascript; charset=utf-8\");\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.end(shimSource(spec));\n });\n },\n generateBundle() {\n for (const [spec, { file }] of Object.entries(SHARED_MODULES)) {\n this.emitFile({\n type: \"asset\",\n fileName: `plugin-shared/${file}.js`,\n source: shimSource(spec),\n });\n }\n },\n});\n\n/** Inject the preview-host runtime flags into index.html (plugin-preview only). */\nexport const pluginPreviewRuntimePlugin = (): Plugin => ({\n name: \"plugin-ui-preview-runtime\",\n apply: (_config, { mode }) => isPluginPreviewMode(mode),\n transformIndexHtml(html) {\n return html.replace(\n \"</head>\",\n [\n \" <script>\",\n \" window.__RB_PLUGIN_PREVIEW_HOST__ = true;\",\n \" window.__RB_ASSETS_BASE_URL__ = window.location.origin;\",\n \" </script>\",\n \" </head>\",\n ].join(\"\\n\"),\n );\n },\n});\n\n/** Serve the host's static assets (imgs/fonts/...) in plugin-preview dev mode. */\nexport const pluginPreviewAssetsPlugin = (opts: {\n assetsDir: string;\n prefixes?: string[];\n}): Plugin => {\n const { assetsDir, prefixes = defaultPreviewAssetPrefixes } = opts;\n return {\n name: \"plugin-ui-preview-assets\",\n apply: (_config, { command, mode }) =>\n command === \"serve\" && isPluginPreviewMode(mode),\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = req.url?.split(\"?\")[0];\n if (!url || !prefixes.some((prefix) => url.startsWith(prefix)))\n return next();\n\n const decodedUrl = decodeURIComponent(url);\n const filePath = resolve(assetsDir, `.${decodedUrl}`);\n if (!filePath.startsWith(assetsDir)) return next();\n if (!existsSync(filePath) || !statSync(filePath).isFile()) return next();\n\n const contentType = contentTypeByExt[extname(filePath)];\n if (contentType) res.setHeader(\"Content-Type\", contentType);\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n createReadStream(filePath).pipe(res);\n });\n },\n };\n};\n\n/**\n * All host-side plugin-ui Vite plugins in one call. Each self-gates by mode, so\n * a host just spreads this into its `plugins` array.\n *\n * plugins: [muscatVitePlugin({ previewAssetsDir: resolve(here, \"../assets/public\") })]\n */\nexport const muscatVitePlugin = (opts: {\n /** Host static assets dir served in plugin-preview dev mode. */\n previewAssetsDir: string;\n /** Override preview asset URL prefixes. */\n previewAssetPrefixes?: string[];\n}): Plugin[] => [\n pluginSharedRuntimePlugin(),\n pluginPreviewRuntimePlugin(),\n pluginPreviewAssetsPlugin({\n assetsDir: opts.previewAssetsDir,\n prefixes: opts.previewAssetPrefixes,\n }),\n];\n"],
|
|
4
|
+
"sourcesContent": ["/**\r\n * Vite plugins for a HOST app previewing locally-developed plugins.\r\n * Host-agnostic \u2014 paths are parameters.\r\n *\r\n * import {\r\n * pluginPreviewRuntimePlugin,\r\n * pluginPreviewAssetsPlugin,\r\n * isPluginPreviewMode,\r\n * } from \"@rainbow-robotics/plugin-ui/vite-host\";\r\n *\r\n * A built plugin's bare imports (react, rb-components, plugin-ui/*) resolve via\r\n * the import map this file injects, to the host's already-loaded instances.\r\n * See ./host.tsx.\r\n */\r\nimport type { Plugin } from \"vite\";\r\nimport { createReadStream, existsSync, readFileSync, statSync } from \"node:fs\";\r\nimport { createRequire } from \"node:module\";\r\nimport { extname, resolve } from \"node:path\";\r\n\r\nconst require = createRequire(import.meta.url);\r\n\r\nexport const defaultPreviewAssetPrefixes = [\r\n \"/imgs/\",\r\n \"/fonts/\",\r\n \"/models/\",\r\n \"/sounds/\",\r\n \"/data/\",\r\n];\r\n\r\nexport const contentTypeByExt: Record<string, string> = {\r\n \".avif\": \"image/avif\",\r\n \".css\": \"text/css; charset=utf-8\",\r\n \".gif\": \"image/gif\",\r\n \".glb\": \"model/gltf-binary\",\r\n \".jpeg\": \"image/jpeg\",\r\n \".jpg\": \"image/jpeg\",\r\n \".js\": \"text/javascript; charset=utf-8\",\r\n \".json\": \"application/json; charset=utf-8\",\r\n \".mp3\": \"audio/mpeg\",\r\n \".png\": \"image/png\",\r\n \".svg\": \"image/svg+xml\",\r\n \".wasm\": \"application/wasm\",\r\n \".webp\": \"image/webp\",\r\n \".woff\": \"font/woff\",\r\n \".woff2\": \"font/woff2\",\r\n};\r\n\r\nexport const isPluginPreviewMode = (mode: string) => mode === \"plugin-preview\";\r\n\r\n/* -------------------------------------------------------------------------- */\r\n/* Shared-runtime import map (plugins reuse the host's loaded instances) */\r\n/* -------------------------------------------------------------------------- */\r\n\r\nconst SHARED_BASE = \"/plugin-shared\";\r\n\r\n// Each shared specifier \u2192 the named exports its shim re-exports from\r\n// window.__RB_SHARED__[specifier] (set by the host, e.g. nexus/src/pluginShared).\r\n//\r\n// `names`: hardcoded list (small, stable modules).\r\n// `parseNamesFrom`: parse the export names out of a source file's text \u2014 used\r\n// for react.tsx, which re-exports the whole rb-components barrel (100+ names).\r\n// We text-parse rather than import it: the barrel is .ts source, which plain\r\n// Node can't execute.\r\nconst SHARED_MODULES: Record<\r\n string,\r\n {\r\n file: string;\r\n names?: string[];\r\n parseNamesFrom?: { specifier: string; extraNames?: string[] };\r\n hasDefault?: boolean;\r\n }\r\n> = {\r\n react: {\r\n file: \"react\",\r\n hasDefault: true,\r\n names: [\r\n \"Children\", \"Component\", \"Fragment\", \"Profiler\", \"PureComponent\",\r\n \"StrictMode\", \"Suspense\", \"cloneElement\", \"createContext\",\r\n \"createElement\", \"createRef\", \"forwardRef\", \"isValidElement\", \"lazy\",\r\n \"memo\", \"startTransition\", \"useCallback\", \"useContext\", \"useDebugValue\",\r\n \"useDeferredValue\", \"useEffect\", \"useId\", \"useImperativeHandle\",\r\n \"useInsertionEffect\", \"useLayoutEffect\", \"useMemo\", \"useReducer\",\r\n \"useRef\", \"useState\", \"useSyncExternalStore\", \"useTransition\", \"version\",\r\n ],\r\n },\r\n \"react/jsx-runtime\": {\r\n file: \"react_jsx-runtime\",\r\n names: [\"Fragment\", \"jsx\", \"jsxs\"],\r\n },\r\n \"react/jsx-dev-runtime\": {\r\n file: \"react_jsx-dev-runtime\",\r\n names: [\"Fragment\", \"jsxDEV\"],\r\n },\r\n \"react-dom\": {\r\n file: \"react-dom\",\r\n hasDefault: true,\r\n names: [\r\n \"__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED\",\r\n \"createPortal\",\r\n \"createRoot\",\r\n \"findDOMNode\",\r\n \"flushSync\",\r\n \"hydrate\",\r\n \"hydrateRoot\",\r\n \"render\",\r\n \"unmountComponentAtNode\",\r\n \"unstable_batchedUpdates\",\r\n \"unstable_renderSubtreeIntoContainer\",\r\n \"version\",\r\n ],\r\n },\r\n \"react-dom/client\": {\r\n file: \"react-dom_client\",\r\n names: [\"createRoot\", \"hydrateRoot\"],\r\n },\r\n \"@rainbow-robotics/plugin-ui/react\": {\r\n file: \"plugin-ui_react\",\r\n // react.tsx re-exports the full rb-components barrel (export *) plus the sdk\r\n // hooks.\r\n parseNamesFrom: {\r\n specifier: \"@repo/rb-components/new_components\",\r\n extraNames: [\"useRb\", \"useRbFormData\", \"useRbSave\"],\r\n },\r\n },\r\n \"@rainbow-robotics/plugin-ui/sdk\": {\r\n file: \"plugin-ui_sdk\",\r\n names: [\"RbContext\", \"useRb\", \"useRbFormData\", \"useRbSave\"],\r\n },\r\n \"@rainbow-robotics/plugin-ui/host-ui\": {\r\n file: \"plugin-ui_host-ui\",\r\n names: [\r\n \"alert\",\r\n \"confirm\",\r\n \"getHostServices\",\r\n \"prompt\",\r\n \"setHostServices\",\r\n \"toast\",\r\n ],\r\n },\r\n \"@rainbow-robotics/plugin-ui/ws\": {\r\n file: \"plugin-ui_ws\",\r\n names: [\"RbWs\", \"createLazyRbSdk\"],\r\n },\r\n};\r\n\r\n// Parses `export { a, b as c, ... }` value names out of a source file's text\r\n// (skips `export type { ... }` blocks and individual `type X` entries within\r\n// a mixed block \u2014 types don't exist as runtime module properties). Static\r\n// text parsing, not module execution, so it works for .ts source files with\r\n// no loader and has no ESM-resolution footguns.\r\nfunction parseExportedValueNames(source: string): string[] {\r\n const names = new Set<string>();\r\n const blockRe = /export\\s+(type\\s+)?\\{([^}]*)\\}/g;\r\n let match: RegExpExecArray | null;\r\n while ((match = blockRe.exec(source))) {\r\n if (match[1]) continue; // whole `export type { ... }` block\r\n for (const rawEntry of match[2].split(\",\")) {\r\n const entry = rawEntry.trim();\r\n if (!entry || entry.startsWith(\"type \")) continue;\r\n const asMatch = entry.match(/^\\S+\\s+as\\s+(\\S+)$/);\r\n names.add(asMatch ? asMatch[1] : entry);\r\n }\r\n }\r\n return [...names];\r\n}\r\n\r\nconst nameCache = new Map<string, string[]>();\r\n\r\nfunction resolveNames(spec: string): string[] {\r\n const mod = SHARED_MODULES[spec];\r\n if (mod.names) return mod.names;\r\n if (!mod.parseNamesFrom) {\r\n throw new Error(`[plugin-ui] ${spec} has neither \"names\" nor \"parseNamesFrom\"`);\r\n }\r\n if (!nameCache.has(spec)) {\r\n const { specifier, extraNames = [] } = mod.parseNamesFrom;\r\n const sourcePath = require.resolve(specifier);\r\n const parsed = parseExportedValueNames(readFileSync(sourcePath, \"utf8\"));\r\n nameCache.set(spec, [...new Set([...parsed, ...extraNames])]);\r\n }\r\n return nameCache.get(spec)!;\r\n}\r\n\r\nfunction shimSource(spec: string): string {\r\n const mod = SHARED_MODULES[spec];\r\n const names = resolveNames(spec);\r\n const g = `(globalThis.__RB_SHARED__||{})[${JSON.stringify(spec)}]`;\r\n return (\r\n `const m = ${g};\\n` +\r\n `if (!m) throw new Error(\"[plugin-ui] host shared module not registered: ${spec}\");\\n` +\r\n `export const { ${names.join(\", \")} } = m;\\n` +\r\n (mod.hasDefault ? `export default (m.default ?? m);\\n` : \"\")\r\n );\r\n}\r\n\r\nconst fileToSpec = Object.fromEntries(\r\n Object.entries(SHARED_MODULES).map(([spec, { file }]) => [file, spec]),\r\n);\r\n\r\nfunction importMapScript(): string {\r\n const imports: Record<string, string> = {};\r\n for (const [spec, { file }] of Object.entries(SHARED_MODULES)) {\r\n imports[spec] = `${SHARED_BASE}/${file}.js`;\r\n }\r\n return `<script type=\"importmap\">${JSON.stringify({ imports })}</script>`;\r\n}\r\n\r\n/**\r\n * Serves the shared-runtime shim modules at /plugin-shared/*.js and injects the\r\n * import map so a dynamically-imported plugin's bare `import \"react\"` /\r\n * `import \"@rainbow-robotics/plugin-ui/react\"` etc. resolve to the host's OWN\r\n * instances (via window.__RB_SHARED__). Applied in all modes \u2014 any nexus build\r\n * that can host plugins needs it.\r\n */\r\nexport const pluginSharedRuntimePlugin = (): Plugin => ({\r\n name: \"plugin-ui-shared-runtime\",\r\n transformIndexHtml(html) {\r\n return html.replace(\"</head>\", ` ${importMapScript()}\\n </head>`);\r\n },\r\n configureServer(server) {\r\n server.middlewares.use((req, res, next) => {\r\n const url = req.url?.split(\"?\")[0];\r\n if (!url?.startsWith(`${SHARED_BASE}/`)) return next();\r\n const file = url.slice(`${SHARED_BASE}/`.length).replace(/\\.js$/, \"\");\r\n const spec = fileToSpec[file];\r\n if (!spec) return next();\r\n res.setHeader(\"Content-Type\", \"text/javascript; charset=utf-8\");\r\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\r\n res.end(shimSource(spec));\r\n });\r\n },\r\n generateBundle() {\r\n for (const [spec, { file }] of Object.entries(SHARED_MODULES)) {\r\n this.emitFile({\r\n type: \"asset\",\r\n fileName: `plugin-shared/${file}.js`,\r\n source: shimSource(spec),\r\n });\r\n }\r\n },\r\n});\r\n\r\n/** Inject the preview-host runtime flags into index.html (plugin-preview only). */\r\nexport const pluginPreviewRuntimePlugin = (): Plugin => ({\r\n name: \"plugin-ui-preview-runtime\",\r\n apply: (_config, { mode }) => isPluginPreviewMode(mode),\r\n transformIndexHtml(html) {\r\n return html.replace(\r\n \"</head>\",\r\n [\r\n \" <script>\",\r\n \" window.__RB_PLUGIN_PREVIEW_HOST__ = true;\",\r\n \" window.__RB_ASSETS_BASE_URL__ = window.location.origin;\",\r\n \" </script>\",\r\n \" </head>\",\r\n ].join(\"\\n\"),\r\n );\r\n },\r\n});\r\n\r\n/** Serve the host's static assets (imgs/fonts/...) in plugin-preview dev mode. */\r\nexport const pluginPreviewAssetsPlugin = (opts: {\r\n assetsDir: string;\r\n prefixes?: string[];\r\n}): Plugin => {\r\n const { assetsDir, prefixes = defaultPreviewAssetPrefixes } = opts;\r\n return {\r\n name: \"plugin-ui-preview-assets\",\r\n apply: (_config, { command, mode }) =>\r\n command === \"serve\" && isPluginPreviewMode(mode),\r\n configureServer(server) {\r\n server.middlewares.use((req, res, next) => {\r\n const url = req.url?.split(\"?\")[0];\r\n if (!url || !prefixes.some((prefix) => url.startsWith(prefix)))\r\n return next();\r\n\r\n const decodedUrl = decodeURIComponent(url);\r\n const filePath = resolve(assetsDir, `.${decodedUrl}`);\r\n if (!filePath.startsWith(assetsDir)) return next();\r\n if (!existsSync(filePath) || !statSync(filePath).isFile()) return next();\r\n\r\n const contentType = contentTypeByExt[extname(filePath)];\r\n if (contentType) res.setHeader(\"Content-Type\", contentType);\r\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\r\n createReadStream(filePath).pipe(res);\r\n });\r\n },\r\n };\r\n};\r\n\r\n/**\r\n * All host-side plugin-ui Vite plugins in one call. Each self-gates by mode, so\r\n * a host just spreads this into its `plugins` array.\r\n *\r\n * plugins: [muscatVitePlugin({ previewAssetsDir: resolve(here, \"../assets/public\") })]\r\n */\r\nexport const muscatVitePlugin = (opts: {\r\n /** Host static assets dir served in plugin-preview dev mode. */\r\n previewAssetsDir: string;\r\n /** Override preview asset URL prefixes. */\r\n previewAssetPrefixes?: string[];\r\n}): Plugin[] => [\r\n pluginSharedRuntimePlugin(),\r\n pluginPreviewRuntimePlugin(),\r\n pluginPreviewAssetsPlugin({\r\n assetsDir: opts.previewAssetsDir,\r\n prefixes: opts.previewAssetPrefixes,\r\n }),\r\n];\r\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["require"]
|
|
7
7
|
}
|
package/dist/ws.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws.d.ts","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD,MAAM,WAAW,QAAQ;IACvB,SAAS,IAAI,IAAI,CAAC;CACnB;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;AAMnC,qBAAa,IAAI;IACf,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAGzB;IACJ,OAAO,CAAC,QAAQ,CAAC,cAAc,CAG3B;IAEJ,OAAO;IAsCP,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB5D,OAAO,CAAC,IAAI;IAKN,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtD,GAAG,CAAC,CAAC,GAAG,GAAG,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,GAC5B,OAAO,CAAC,YAAY,CAAC;IAsBlB,MAAM,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAuChE,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EAC1B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACvC,OAAO,CAAC,WAAW,CAAC;IAYvB,KAAK,IAAI,IAAI;CAGd;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,EACxB,QAAQ,EAAE,gBAAgB,EAI1B,KAAK,CAAC,EAAE;IACN,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,OAAO,KAAK,MAAM,IAAI,CAAC;CACpD;iBAIoB,MAAM,WAAW,GAAG;iBAG9B,MAAM,WAEJ,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI;oBAGX,MAAM,YAAY,GAAG;mBAGlC,MAAM,WAEJ,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,GAAG;;
|
|
1
|
+
{"version":3,"file":"ws.d.ts","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD,MAAM,WAAW,QAAQ;IACvB,SAAS,IAAI,IAAI,CAAC;CACnB;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;AAMnC,qBAAa,IAAI;IACf,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAGzB;IACJ,OAAO,CAAC,QAAQ,CAAC,cAAc,CAG3B;IAEJ,OAAO;IAsCP,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB5D,OAAO,CAAC,IAAI;IAKN,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtD,GAAG,CAAC,CAAC,GAAG,GAAG,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,GAC5B,OAAO,CAAC,YAAY,CAAC;IAsBlB,MAAM,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAuChE,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EAC1B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACvC,OAAO,CAAC,WAAW,CAAC;IAYvB,KAAK,IAAI,IAAI;CAGd;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,EACxB,QAAQ,EAAE,gBAAgB,EAI1B,KAAK,CAAC,EAAE;IACN,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,OAAO,KAAK,MAAM,IAAI,CAAC;CACpD;iBAIoB,MAAM,WAAW,GAAG;iBAG9B,MAAM,WAEJ,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI;oBAGX,MAAM,YAAY,GAAG;mBAGlC,MAAM,WAEJ,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,GAAG;;aA9LC,CAAC;;YAE/B,CAAC;mBAEuC,CAAC;yBAAgC,CAAC;0BACrD,CAAA;gBAAwB,CAAC;eAAsB,CAAC;;;aArB9D,CAAC;;kBAA+C,CAAC;;;aACC,CAAC;;kBAItD,CAAC;;;;;;aAGT,CAAA;;aAC6B,CAAC;mBAEnB,CAAC;kBAAyB,CAAC;;;;;;;;;;yBAiLhB,MAAM,OAAO,KAAK,MAAM,IAAI;;mBAsCnC,OAAO,CAAC,MAAM,EAAE,CAAC;qBACb,MAAM,EAAE,KAAG,OAAO,CAAC,MAAM,EAAE,CAAC;;EAPlD"}
|
package/dist/ws.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/ws.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Socket.IO communication client for plugin frontends.\n *\n * Topics are automatically namespaced to prevent cross-plugin collisions:\n * \"sensor/data\" \u2192 \"{pluginId}/sensor/data\"\n * \"/broadcast/x\" \u2192 \"broadcast/x\" (leading slash = absolute)\n *\n * Browser \u2190\u2192 Python backend protocol:\n * browser emits : sub, unsub, pub, action (with ack), serve_reply\n * server emits : msg, serve_call\n */\nimport { io, type Socket } from \"socket.io-client\";\n// Type-only \u2014 no runtime coupling to host-ui.ts.\nimport type { PluginUIServices } from \"./host-ui\";\n\nexport interface WsHandle {\n undeclare(): void;\n}\n\nexport type WsSubscriber = WsHandle;\nexport type WsQueryable = WsHandle;\n\nfunction resolveTopic(pluginId: string, topic: string): string {\n return topic.startsWith(\"/\") ? topic.slice(1) : `${pluginId}/${topic}`;\n}\n\nexport class RbWs {\n private readonly _socket: Socket;\n private readonly _pluginId: string;\n private readonly _subHandlers = new Map<\n string,\n Set<(payload: unknown) => void>\n >();\n private readonly _serveHandlers = new Map<\n string,\n (payload?: unknown) => unknown | Promise<unknown>\n >();\n\n private constructor(socket: Socket, pluginId: string) {\n this._socket = socket;\n this._pluginId = pluginId;\n\n // The gateway keeps subscriptions/serve registrations in memory \u2014 re-send them after\n // a reconnect (e.g. the gateway restarted), or they silently stop working.\n socket.io.on(\"reconnect\", () => {\n for (const topic of this._subHandlers.keys()) socket.emit(\"sub\", { topic });\n for (const topic of this._serveHandlers.keys()) socket.emit(\"serve_register\", { topic });\n });\n\n // Deliver incoming pub/sub messages to registered handlers\n socket.on(\n \"msg\",\n ({ topic, payload }: { topic: string; payload: unknown }) => {\n this._subHandlers.get(topic)?.forEach((h) => h(payload));\n },\n );\n\n // Python calls a browser-side serve handler\n socket.on(\n \"serve_call\",\n async ({\n id,\n topic,\n payload,\n }: {\n id: string;\n topic: string;\n payload: unknown;\n }) => {\n const handler = this._serveHandlers.get(topic);\n const result = handler ? await handler(payload) : undefined;\n socket.emit(\"serve_reply\", { id, payload: result });\n },\n );\n }\n\n get pluginId(): string {\n return this._pluginId;\n }\n\n static connect(url: string, pluginId: string): Promise<RbWs> {\n return new Promise((resolve, reject) => {\n const socket = io(url, { transports: [\"websocket\"] });\n const onConnect = () => {\n cleanup();\n resolve(new RbWs(socket, pluginId));\n };\n const onError = (err: Error) => {\n cleanup();\n reject(err);\n };\n const cleanup = () => {\n socket.off(\"connect\", onConnect);\n socket.off(\"connect_error\", onError);\n };\n socket.once(\"connect\", onConnect);\n socket.once(\"connect_error\", onError);\n });\n }\n\n private _key(topic: string): string {\n return resolveTopic(this._pluginId, topic);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async pub<P = any>(topic: string, payload: P): Promise<void> {\n this._socket.emit(\"pub\", { topic: this._key(topic), payload });\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async sub<P = any>(\n topic: string,\n handler: (payload: P) => void,\n ): Promise<WsSubscriber> {\n const key = this._key(topic);\n if (!this._subHandlers.has(key)) {\n this._subHandlers.set(key, new Set());\n this._socket.emit(\"sub\", { topic: key });\n }\n const h = handler as (payload: unknown) => void;\n this._subHandlers.get(key)!.add(h);\n return {\n undeclare: () => {\n const set = this._subHandlers.get(key);\n if (!set) return;\n set.delete(h);\n if (set.size === 0) {\n this._subHandlers.delete(key);\n this._socket.emit(\"unsub\", { topic: key });\n }\n },\n };\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async action<R = any, P = any>(topic: string, payload?: P): Promise<R> {\n const key = this._key(topic);\n return new Promise((resolve, reject) => {\n this._socket\n .timeout(10_000)\n .emit(\n \"action\",\n { topic: key, payload },\n (err: Error | null, result: R) => {\n if (err) {\n // socket.io ack timeout \u2014 the bridge itself never answered.\n reject(new Error(`action \"${key}\": no ack from bridge (${err.message})`));\n } else if (\n result &&\n typeof result === \"object\" &&\n \"error\" in result &&\n typeof result.error === \"string\"\n ) {\n // Backend handler returned an error payload \u2014 surface it verbatim.\n reject(new Error(result.error));\n } else if (result === undefined || result === null) {\n // The bridge acked but with no payload: the backend handler is\n // registered yet didn't reply in time (hung inside the handler,\n // e.g. a blocked SDK call) or returned nothing. Distinct from a\n // \"No handler registered\" error, which comes back as result.error.\n reject(\n new Error(\n `action \"${key}\": no reply from plugin backend \u2014 the handler was reached but didn't return in time (check the backend's stdout on the robot).`,\n ),\n );\n } else {\n resolve(result);\n }\n },\n );\n });\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async serve<P = any, R = any>(\n topic: string,\n handler: (payload?: P) => R | Promise<R>,\n ): Promise<WsQueryable> {\n const key = this._key(topic);\n this._serveHandlers.set(key, handler as (payload?: unknown) => unknown);\n this._socket.emit(\"serve_register\", { topic: key });\n return {\n undeclare: () => {\n this._serveHandlers.delete(key);\n this._socket.emit(\"serve_unregister\", { topic: key });\n },\n };\n }\n\n close(): void {\n this._socket.disconnect();\n }\n}\n\n/**\n * Builds an rb-sdk-shaped object (pub/sub/action/serve + the passed-through\n * toast/alert/confirm/prompt) that lazily awaits a not-yet-connected `RbWs` \u2014\n * so callers don't block their first render on the WebSocket handshake (there\n * may be no backend reachable yet). Shared by both host mount paths (see\n * ./host.tsx and muscat-plugin-kit's dev bootstrap), which otherwise\n * duplicate this wiring.\n */\nexport function createLazyRbSdk(\n wsPromise: Promise<RbWs>,\n services: PluginUIServices,\n // Per-mount, host-provided (unlike `services`, which is a global singleton):\n // initial `data` and the `onSave` registration the host reads back. Plain\n // values/closures, so they cross the dev React-instance boundary safely.\n mount?: {\n data?: unknown;\n onSave?: (getPayload: () => unknown) => () => void;\n },\n) {\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n pub: async (topic: string, payload: any) =>\n (await wsPromise).pub(topic, payload),\n sub: async (\n topic: string,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n handler: (payload: any) => void,\n ) => (await wsPromise).sub(topic, handler),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n action: async (topic: string, payload?: any) =>\n (await wsPromise).action(topic, payload),\n serve: async (\n topic: string,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n handler: (payload?: any) => any,\n ) => (await wsPromise).serve(topic, handler),\n toast: services.toast,\n alert: services.alert,\n confirm: services.confirm,\n prompt: services.prompt,\n program: services.program,\n data: mount?.data,\n onSave: mount?.onSave ?? (() => () => {}),\n rosPeers: rosPeersApi(async (op, payload) => {\n const ws = await wsPromise;\n return ws.action(`/muscat/plugin/${ws.pluginId}/ros_peers/${op}`, payload);\n }),\n };\n}\n\n// Answered by Muscat (common) on the robot and by `muscat-pk dev` locally.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction rosPeersApi(call: (op: \"get\" | \"set\", payload?: any) => Promise<any>) {\n return {\n get: async (): Promise<string[]> => (await call(\"get\"))?.peers ?? [],\n set: async (peers: string[]): Promise<string[]> => {\n const r = await call(\"set\", { peers });\n if (!r?.ok) throw new Error(r?.error ?? \"rb.rosPeers.set failed\");\n return r.peers;\n },\n };\n}\n"],
|
|
4
|
+
"sourcesContent": ["/**\r\n * Socket.IO communication client for plugin frontends.\r\n *\r\n * Topics are automatically namespaced to prevent cross-plugin collisions:\r\n * \"sensor/data\" \u2192 \"{pluginId}/sensor/data\"\r\n * \"/broadcast/x\" \u2192 \"broadcast/x\" (leading slash = absolute)\r\n *\r\n * Browser \u2190\u2192 Python backend protocol:\r\n * browser emits : sub, unsub, pub, action (with ack), serve_reply\r\n * server emits : msg, serve_call\r\n */\r\nimport { io, type Socket } from \"socket.io-client\";\r\n// Type-only \u2014 no runtime coupling to host-ui.ts.\r\nimport type { PluginUIServices } from \"./host-ui\";\r\n\r\nexport interface WsHandle {\r\n undeclare(): void;\r\n}\r\n\r\nexport type WsSubscriber = WsHandle;\r\nexport type WsQueryable = WsHandle;\r\n\r\nfunction resolveTopic(pluginId: string, topic: string): string {\r\n return topic.startsWith(\"/\") ? topic.slice(1) : `${pluginId}/${topic}`;\r\n}\r\n\r\nexport class RbWs {\r\n private readonly _socket: Socket;\r\n private readonly _pluginId: string;\r\n private readonly _subHandlers = new Map<\r\n string,\r\n Set<(payload: unknown) => void>\r\n >();\r\n private readonly _serveHandlers = new Map<\r\n string,\r\n (payload?: unknown) => unknown | Promise<unknown>\r\n >();\r\n\r\n private constructor(socket: Socket, pluginId: string) {\r\n this._socket = socket;\r\n this._pluginId = pluginId;\r\n\r\n // The gateway keeps subscriptions/serve registrations in memory \u2014 re-send them after\r\n // a reconnect (e.g. the gateway restarted), or they silently stop working.\r\n socket.io.on(\"reconnect\", () => {\r\n for (const topic of this._subHandlers.keys()) socket.emit(\"sub\", { topic });\r\n for (const topic of this._serveHandlers.keys()) socket.emit(\"serve_register\", { topic });\r\n });\r\n\r\n // Deliver incoming pub/sub messages to registered handlers\r\n socket.on(\r\n \"msg\",\r\n ({ topic, payload }: { topic: string; payload: unknown }) => {\r\n this._subHandlers.get(topic)?.forEach((h) => h(payload));\r\n },\r\n );\r\n\r\n // Python calls a browser-side serve handler\r\n socket.on(\r\n \"serve_call\",\r\n async ({\r\n id,\r\n topic,\r\n payload,\r\n }: {\r\n id: string;\r\n topic: string;\r\n payload: unknown;\r\n }) => {\r\n const handler = this._serveHandlers.get(topic);\r\n const result = handler ? await handler(payload) : undefined;\r\n socket.emit(\"serve_reply\", { id, payload: result });\r\n },\r\n );\r\n }\r\n\r\n get pluginId(): string {\r\n return this._pluginId;\r\n }\r\n\r\n static connect(url: string, pluginId: string): Promise<RbWs> {\r\n return new Promise((resolve, reject) => {\r\n const socket = io(url, { transports: [\"websocket\"] });\r\n const onConnect = () => {\r\n cleanup();\r\n resolve(new RbWs(socket, pluginId));\r\n };\r\n const onError = (err: Error) => {\r\n cleanup();\r\n reject(err);\r\n };\r\n const cleanup = () => {\r\n socket.off(\"connect\", onConnect);\r\n socket.off(\"connect_error\", onError);\r\n };\r\n socket.once(\"connect\", onConnect);\r\n socket.once(\"connect_error\", onError);\r\n });\r\n }\r\n\r\n private _key(topic: string): string {\r\n return resolveTopic(this._pluginId, topic);\r\n }\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n async pub<P = any>(topic: string, payload: P): Promise<void> {\r\n this._socket.emit(\"pub\", { topic: this._key(topic), payload });\r\n }\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n async sub<P = any>(\r\n topic: string,\r\n handler: (payload: P) => void,\r\n ): Promise<WsSubscriber> {\r\n const key = this._key(topic);\r\n if (!this._subHandlers.has(key)) {\r\n this._subHandlers.set(key, new Set());\r\n this._socket.emit(\"sub\", { topic: key });\r\n }\r\n const h = handler as (payload: unknown) => void;\r\n this._subHandlers.get(key)!.add(h);\r\n return {\r\n undeclare: () => {\r\n const set = this._subHandlers.get(key);\r\n if (!set) return;\r\n set.delete(h);\r\n if (set.size === 0) {\r\n this._subHandlers.delete(key);\r\n this._socket.emit(\"unsub\", { topic: key });\r\n }\r\n },\r\n };\r\n }\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n async action<R = any, P = any>(topic: string, payload?: P): Promise<R> {\r\n const key = this._key(topic);\r\n return new Promise((resolve, reject) => {\r\n this._socket\r\n .timeout(10_000)\r\n .emit(\r\n \"action\",\r\n { topic: key, payload },\r\n (err: Error | null, result: R) => {\r\n if (err) {\r\n // socket.io ack timeout \u2014 the bridge itself never answered.\r\n reject(new Error(`action \"${key}\": no ack from bridge (${err.message})`));\r\n } else if (\r\n result &&\r\n typeof result === \"object\" &&\r\n \"error\" in result &&\r\n typeof result.error === \"string\"\r\n ) {\r\n // Backend handler returned an error payload \u2014 surface it verbatim.\r\n reject(new Error(result.error));\r\n } else if (result === undefined || result === null) {\r\n // The bridge acked but with no payload: the backend handler is\r\n // registered yet didn't reply in time (hung inside the handler,\r\n // e.g. a blocked SDK call) or returned nothing. Distinct from a\r\n // \"No handler registered\" error, which comes back as result.error.\r\n reject(\r\n new Error(\r\n `action \"${key}\": no reply from plugin backend \u2014 the handler was reached but didn't return in time (check the backend's stdout on the robot).`,\r\n ),\r\n );\r\n } else {\r\n resolve(result);\r\n }\r\n },\r\n );\r\n });\r\n }\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n async serve<P = any, R = any>(\r\n topic: string,\r\n handler: (payload?: P) => R | Promise<R>,\r\n ): Promise<WsQueryable> {\r\n const key = this._key(topic);\r\n this._serveHandlers.set(key, handler as (payload?: unknown) => unknown);\r\n this._socket.emit(\"serve_register\", { topic: key });\r\n return {\r\n undeclare: () => {\r\n this._serveHandlers.delete(key);\r\n this._socket.emit(\"serve_unregister\", { topic: key });\r\n },\r\n };\r\n }\r\n\r\n close(): void {\r\n this._socket.disconnect();\r\n }\r\n}\r\n\r\n/**\r\n * Builds an rb-sdk-shaped object (pub/sub/action/serve + the passed-through\r\n * toast/alert/confirm/prompt) that lazily awaits a not-yet-connected `RbWs` \u2014\r\n * so callers don't block their first render on the WebSocket handshake (there\r\n * may be no backend reachable yet). Shared by both host mount paths (see\r\n * ./host.tsx and muscat-plugin-kit's dev bootstrap), which otherwise\r\n * duplicate this wiring.\r\n */\r\nexport function createLazyRbSdk(\r\n wsPromise: Promise<RbWs>,\r\n services: PluginUIServices,\r\n // Per-mount, host-provided (unlike `services`, which is a global singleton):\r\n // initial `data` and the `onSave` registration the host reads back. Plain\r\n // values/closures, so they cross the dev React-instance boundary safely.\r\n mount?: {\r\n data?: unknown;\r\n onSave?: (getPayload: () => unknown) => () => void;\r\n },\r\n) {\r\n return {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n pub: async (topic: string, payload: any) =>\r\n (await wsPromise).pub(topic, payload),\r\n sub: async (\r\n topic: string,\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n handler: (payload: any) => void,\r\n ) => (await wsPromise).sub(topic, handler),\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n action: async (topic: string, payload?: any) =>\r\n (await wsPromise).action(topic, payload),\r\n serve: async (\r\n topic: string,\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n handler: (payload?: any) => any,\r\n ) => (await wsPromise).serve(topic, handler),\r\n toast: services.toast,\r\n alert: services.alert,\r\n confirm: services.confirm,\r\n prompt: services.prompt,\r\n program: services.program,\r\n data: mount?.data,\r\n onSave: mount?.onSave ?? (() => () => {}),\r\n rosPeers: rosPeersApi(async (op, payload) => {\r\n const ws = await wsPromise;\r\n return ws.action(`/muscat/plugin/${ws.pluginId}/ros_peers/${op}`, payload);\r\n }),\r\n };\r\n}\r\n\r\n// Answered by Muscat (common) on the robot and by `muscat-pk dev` locally.\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\nfunction rosPeersApi(call: (op: \"get\" | \"set\", payload?: any) => Promise<any>) {\r\n return {\r\n get: async (): Promise<string[]> => (await call(\"get\"))?.peers ?? [],\r\n set: async (peers: string[]): Promise<string[]> => {\r\n const r = await call(\"set\", { peers });\r\n if (!r?.ok) throw new Error(r?.error ?? \"rb.rosPeers.set failed\");\r\n return r.peers;\r\n },\r\n };\r\n}\r\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
package/src/host-ui.ts
CHANGED
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
import { ReactElement } from "react";
|
|
2
|
-
|
|
3
|
-
/** One step of a program tree a plugin hands to the host Program page. */
|
|
4
|
-
export interface PluginProgramStep {
|
|
5
|
-
/** Muscat command name, e.g. "MoveJ", "Point", "Wait", "Repeat", "Condition". */
|
|
6
|
-
command: string;
|
|
7
|
-
args?: Record<string, unknown>;
|
|
8
|
-
summary?: string;
|
|
9
|
-
children?: PluginProgramStep[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface PluginUIServices {
|
|
13
|
-
/** Replaces the loaded program's main step tree (unsaved edit; the user saves). */
|
|
14
|
-
program: {
|
|
15
|
-
setSteps: (steps: PluginProgramStep[]) => Promise<void>;
|
|
16
|
-
/** Command catalog of the loaded robot (names, default args, nesting rules). */
|
|
17
|
-
getCatalog: () => Promise<unknown[]>;
|
|
18
|
-
};
|
|
19
|
-
alert: (params: {
|
|
20
|
-
title?: string;
|
|
21
|
-
message: string;
|
|
22
|
-
buttonText?: string;
|
|
23
|
-
}) => Promise<void>;
|
|
24
|
-
confirm: (params: {
|
|
25
|
-
title?: string;
|
|
26
|
-
message: string;
|
|
27
|
-
buttonText?: {
|
|
28
|
-
yes: string;
|
|
29
|
-
no: string;
|
|
30
|
-
};
|
|
31
|
-
}) => Promise<boolean>;
|
|
32
|
-
prompt: (params: {
|
|
33
|
-
title?: string;
|
|
34
|
-
message: string;
|
|
35
|
-
value?: string;
|
|
36
|
-
placeholder?: string;
|
|
37
|
-
buttonText?: {
|
|
38
|
-
yes: string;
|
|
39
|
-
no: string;
|
|
40
|
-
};
|
|
41
|
-
}) => Promise<string | undefined>;
|
|
42
|
-
toast: (params: {
|
|
43
|
-
title?: string;
|
|
44
|
-
message: string | ReactElement;
|
|
45
|
-
type?: "info" | "danger" | "warning" | "caution" | "success";
|
|
46
|
-
actionLabel?: string;
|
|
47
|
-
handleActionClick?: () => void;
|
|
48
|
-
closeOnActionClick?: boolean;
|
|
49
|
-
duration?: number;
|
|
50
|
-
onClose?: () => void;
|
|
51
|
-
}) => void;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
let services: PluginUIServices | null = null;
|
|
55
|
-
|
|
56
|
-
/** Called by the host when it mounts a plugin. */
|
|
57
|
-
export function setHostServices(next: PluginUIServices): void {
|
|
58
|
-
services = next;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Read the injected services, throwing a clear error if the host forgot. */
|
|
62
|
-
export function getHostServices(): PluginUIServices {
|
|
63
|
-
if (!services) {
|
|
64
|
-
throw new Error(
|
|
65
|
-
"[plugin-ui] services not set — the plugin must be mounted by a Muscat host.",
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
return services;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Open a host alert dialog from a plugin. */
|
|
72
|
-
export function alert(
|
|
73
|
-
params: Parameters<PluginUIServices["alert"]>[0],
|
|
74
|
-
): Promise<void> {
|
|
75
|
-
return getHostServices().alert(params);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Open a host confirm dialog from a plugin. Resolves true/false. */
|
|
79
|
-
export function confirm(
|
|
80
|
-
params: Parameters<PluginUIServices["confirm"]>[0],
|
|
81
|
-
): Promise<boolean> {
|
|
82
|
-
return getHostServices().confirm(params);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Open a host prompt dialog from a plugin. Resolves the entered value, or undefined if cancelled. */
|
|
86
|
-
export function prompt(
|
|
87
|
-
params: Parameters<PluginUIServices["prompt"]>[0],
|
|
88
|
-
): Promise<string | undefined> {
|
|
89
|
-
return getHostServices().prompt(params);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Show a host toast from a plugin. */
|
|
93
|
-
export function toast(params: Parameters<PluginUIServices["toast"]>[0]): void {
|
|
94
|
-
getHostServices().toast(params);
|
|
95
|
-
}
|
|
1
|
+
import { ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
/** One step of a program tree a plugin hands to the host Program page. */
|
|
4
|
+
export interface PluginProgramStep {
|
|
5
|
+
/** Muscat command name, e.g. "MoveJ", "Point", "Wait", "Repeat", "Condition". */
|
|
6
|
+
command: string;
|
|
7
|
+
args?: Record<string, unknown>;
|
|
8
|
+
summary?: string;
|
|
9
|
+
children?: PluginProgramStep[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PluginUIServices {
|
|
13
|
+
/** Replaces the loaded program's main step tree (unsaved edit; the user saves). */
|
|
14
|
+
program: {
|
|
15
|
+
setSteps: (steps: PluginProgramStep[]) => Promise<void>;
|
|
16
|
+
/** Command catalog of the loaded robot (names, default args, nesting rules). */
|
|
17
|
+
getCatalog: () => Promise<unknown[]>;
|
|
18
|
+
};
|
|
19
|
+
alert: (params: {
|
|
20
|
+
title?: string;
|
|
21
|
+
message: string;
|
|
22
|
+
buttonText?: string;
|
|
23
|
+
}) => Promise<void>;
|
|
24
|
+
confirm: (params: {
|
|
25
|
+
title?: string;
|
|
26
|
+
message: string;
|
|
27
|
+
buttonText?: {
|
|
28
|
+
yes: string;
|
|
29
|
+
no: string;
|
|
30
|
+
};
|
|
31
|
+
}) => Promise<boolean>;
|
|
32
|
+
prompt: (params: {
|
|
33
|
+
title?: string;
|
|
34
|
+
message: string;
|
|
35
|
+
value?: string;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
buttonText?: {
|
|
38
|
+
yes: string;
|
|
39
|
+
no: string;
|
|
40
|
+
};
|
|
41
|
+
}) => Promise<string | undefined>;
|
|
42
|
+
toast: (params: {
|
|
43
|
+
title?: string;
|
|
44
|
+
message: string | ReactElement;
|
|
45
|
+
type?: "info" | "danger" | "warning" | "caution" | "success";
|
|
46
|
+
actionLabel?: string;
|
|
47
|
+
handleActionClick?: () => void;
|
|
48
|
+
closeOnActionClick?: boolean;
|
|
49
|
+
duration?: number;
|
|
50
|
+
onClose?: () => void;
|
|
51
|
+
}) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let services: PluginUIServices | null = null;
|
|
55
|
+
|
|
56
|
+
/** Called by the host when it mounts a plugin. */
|
|
57
|
+
export function setHostServices(next: PluginUIServices): void {
|
|
58
|
+
services = next;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Read the injected services, throwing a clear error if the host forgot. */
|
|
62
|
+
export function getHostServices(): PluginUIServices {
|
|
63
|
+
if (!services) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"[plugin-ui] services not set — the plugin must be mounted by a Muscat host.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return services;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Open a host alert dialog from a plugin. */
|
|
72
|
+
export function alert(
|
|
73
|
+
params: Parameters<PluginUIServices["alert"]>[0],
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
return getHostServices().alert(params);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Open a host confirm dialog from a plugin. Resolves true/false. */
|
|
79
|
+
export function confirm(
|
|
80
|
+
params: Parameters<PluginUIServices["confirm"]>[0],
|
|
81
|
+
): Promise<boolean> {
|
|
82
|
+
return getHostServices().confirm(params);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Open a host prompt dialog from a plugin. Resolves the entered value, or undefined if cancelled. */
|
|
86
|
+
export function prompt(
|
|
87
|
+
params: Parameters<PluginUIServices["prompt"]>[0],
|
|
88
|
+
): Promise<string | undefined> {
|
|
89
|
+
return getHostServices().prompt(params);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Show a host toast from a plugin. */
|
|
93
|
+
export function toast(params: Parameters<PluginUIServices["toast"]>[0]): void {
|
|
94
|
+
getHostServices().toast(params);
|
|
95
|
+
}
|