@jenil94/plugin-sdk 0.1.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 +115 -0
- package/dist/index.cjs +227 -0
- package/dist/index.d.cts +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +195 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @jenil94/plugin-sdk
|
|
2
|
+
|
|
3
|
+
SDK for building Harness plugins with React. Provides communication utilities, context management, and routing synchronization for plugins running in iframes.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @jenil94/plugin-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Peer Dependencies
|
|
12
|
+
|
|
13
|
+
This package requires the following peer dependencies:
|
|
14
|
+
|
|
15
|
+
- `react` (^18.0.0 || ^19.0.0)
|
|
16
|
+
- `react-dom` (^18.0.0 || ^19.0.0)
|
|
17
|
+
- `react-router-dom` (^6.0.0 || ^7.0.0)
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Basic Setup
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { PluginAPI, PluginContextProvider, PluginRouter } from '@jenil94/plugin-sdk'
|
|
25
|
+
|
|
26
|
+
// Initialize the plugin as early as possible
|
|
27
|
+
PluginAPI.init()
|
|
28
|
+
|
|
29
|
+
function App() {
|
|
30
|
+
return (
|
|
31
|
+
<PluginContextProvider>
|
|
32
|
+
<PluginRouter>
|
|
33
|
+
{/* Your app routes here */}
|
|
34
|
+
</PluginRouter>
|
|
35
|
+
</PluginContextProvider>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Using Plugin Context
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { usePluginContext } from '@jenil94/plugin-sdk'
|
|
44
|
+
|
|
45
|
+
function MyComponent() {
|
|
46
|
+
const context = usePluginContext()
|
|
47
|
+
|
|
48
|
+
if (!context) {
|
|
49
|
+
return <div>Loading...</div>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div>
|
|
54
|
+
<p>Theme: {context.theme}</p>
|
|
55
|
+
<p>Location: {context.location}</p>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Proxy Fetch (for authenticated API calls)
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { PluginAPI } from '@jenil94/plugin-sdk'
|
|
65
|
+
|
|
66
|
+
async function fetchData() {
|
|
67
|
+
const response = await PluginAPI.proxyFetch('/api/some-endpoint')
|
|
68
|
+
const data = await response.json()
|
|
69
|
+
return data
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API Reference
|
|
74
|
+
|
|
75
|
+
### PluginAPI
|
|
76
|
+
|
|
77
|
+
Static class for communication with the host application.
|
|
78
|
+
|
|
79
|
+
- `PluginAPI.init()` - Signals to the host that the plugin is ready
|
|
80
|
+
- `PluginAPI.getContext()` - Retrieves the current plugin context
|
|
81
|
+
- `PluginAPI.proxyFetch(url, init?)` - Proxies fetch requests through the host
|
|
82
|
+
|
|
83
|
+
### PluginContextProvider
|
|
84
|
+
|
|
85
|
+
React context provider that manages plugin context state.
|
|
86
|
+
|
|
87
|
+
### usePluginContext()
|
|
88
|
+
|
|
89
|
+
Hook to access the current plugin context.
|
|
90
|
+
|
|
91
|
+
### PluginRouter
|
|
92
|
+
|
|
93
|
+
Router component that synchronizes with the parent page's URL.
|
|
94
|
+
|
|
95
|
+
### useParentRoutingSync()
|
|
96
|
+
|
|
97
|
+
Hook for manual route synchronization with the parent page.
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Install dependencies
|
|
103
|
+
npm install
|
|
104
|
+
|
|
105
|
+
# Build the package
|
|
106
|
+
npm run build
|
|
107
|
+
|
|
108
|
+
# Watch mode
|
|
109
|
+
npm run dev
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
115
|
+
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
PluginAPI: () => PluginAPI,
|
|
24
|
+
PluginContextProvider: () => PluginContextProvider,
|
|
25
|
+
PluginMessageType: () => PluginMessageType,
|
|
26
|
+
PluginRouter: () => PluginRouter,
|
|
27
|
+
useParentRoutingSync: () => useParentRoutingSync,
|
|
28
|
+
usePluginContext: () => usePluginContext
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/plugin-api.ts
|
|
33
|
+
var PluginMessageType = {
|
|
34
|
+
Init: "init",
|
|
35
|
+
Context: "context",
|
|
36
|
+
GetContext: "getContext",
|
|
37
|
+
Fetch: "fetch",
|
|
38
|
+
FetchResponse: "fetchResponse"
|
|
39
|
+
};
|
|
40
|
+
var PluginAPI = class {
|
|
41
|
+
/**
|
|
42
|
+
* Signals to the host that the plugin is ready
|
|
43
|
+
* This should be called as early as possible in the plugin lifecycle
|
|
44
|
+
*/
|
|
45
|
+
static async init() {
|
|
46
|
+
window.parent.postMessage({ type: PluginMessageType.Init }, "*");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Retrieves the current plugin context from the host
|
|
50
|
+
* Includes entity data, user info, theme settings, and API base URL
|
|
51
|
+
*/
|
|
52
|
+
static async getContext() {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const handler = (event) => {
|
|
55
|
+
if (event.data.type === PluginMessageType.Context) {
|
|
56
|
+
window.removeEventListener("message", handler);
|
|
57
|
+
resolve(event.data.data);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
window.addEventListener("message", handler);
|
|
61
|
+
window.parent.postMessage({ type: PluginMessageType.GetContext }, "*");
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Proxies a fetch request through the host application
|
|
66
|
+
* This allows the host to handle authentication and CORS
|
|
67
|
+
*/
|
|
68
|
+
static async proxyFetch(url, init) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
const handler = (event) => {
|
|
71
|
+
const response = event.data?.response;
|
|
72
|
+
if (response) {
|
|
73
|
+
window.removeEventListener("message", handler);
|
|
74
|
+
console.log("proxyFetch response from host:", event.data);
|
|
75
|
+
let body = response.body;
|
|
76
|
+
if (body !== null && typeof body === "object") {
|
|
77
|
+
body = JSON.stringify(body);
|
|
78
|
+
}
|
|
79
|
+
resolve(
|
|
80
|
+
new Response(body, {
|
|
81
|
+
status: response.status ?? 200,
|
|
82
|
+
statusText: response.statusText ?? "",
|
|
83
|
+
headers: response.headers
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
window.addEventListener("message", handler);
|
|
89
|
+
window.parent.postMessage(
|
|
90
|
+
{
|
|
91
|
+
type: PluginMessageType.Fetch,
|
|
92
|
+
url,
|
|
93
|
+
init
|
|
94
|
+
},
|
|
95
|
+
"*"
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// src/plugin-context.tsx
|
|
102
|
+
var import_react = require("react");
|
|
103
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
104
|
+
var PluginDataContext = (0, import_react.createContext)(null);
|
|
105
|
+
function PluginContextProvider({ children }) {
|
|
106
|
+
const [context, setContext] = (0, import_react.useState)(null);
|
|
107
|
+
(0, import_react.useEffect)(() => {
|
|
108
|
+
const handler = (event) => {
|
|
109
|
+
if (event.data?.type === "context") {
|
|
110
|
+
console.log("Received context from host:", event.data.data);
|
|
111
|
+
setContext(event.data.data);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
window.addEventListener("message", handler);
|
|
115
|
+
return () => window.removeEventListener("message", handler);
|
|
116
|
+
}, []);
|
|
117
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PluginDataContext.Provider, { value: context, children });
|
|
118
|
+
}
|
|
119
|
+
function usePluginContext() {
|
|
120
|
+
const context = (0, import_react.useContext)(PluginDataContext);
|
|
121
|
+
return context;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/plugin-router.tsx
|
|
125
|
+
var import_react2 = require("react");
|
|
126
|
+
var import_react_router_dom = require("react-router-dom");
|
|
127
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
128
|
+
var toCamelCase = (str) => {
|
|
129
|
+
return str.split("-").filter((segment) => segment.length > 0).map(
|
|
130
|
+
(word, index) => index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
|
131
|
+
).join("");
|
|
132
|
+
};
|
|
133
|
+
var useParentRoutingSync = () => {
|
|
134
|
+
const context = usePluginContext();
|
|
135
|
+
const tag = context?.tag;
|
|
136
|
+
const queryParamKey = (0, import_react2.useMemo)(() => {
|
|
137
|
+
return tag ? toCamelCase(tag) + "PluginRoute" : "pluginRoute";
|
|
138
|
+
}, [tag]);
|
|
139
|
+
const location = (0, import_react_router_dom.useLocation)();
|
|
140
|
+
const navigate = (0, import_react_router_dom.useNavigate)();
|
|
141
|
+
(0, import_react2.useEffect)(() => {
|
|
142
|
+
const newRoute = `${location.pathname}${location.search}${location.hash}`;
|
|
143
|
+
console.log("Syncing route to parent:", newRoute);
|
|
144
|
+
window.parent.postMessage(
|
|
145
|
+
{
|
|
146
|
+
type: "routeChange",
|
|
147
|
+
route: newRoute,
|
|
148
|
+
queryParamKey
|
|
149
|
+
},
|
|
150
|
+
"*"
|
|
151
|
+
);
|
|
152
|
+
}, [location, queryParamKey]);
|
|
153
|
+
(0, import_react2.useEffect)(() => {
|
|
154
|
+
const handleMessage = (event) => {
|
|
155
|
+
if (event.data?.type === "navigateTo" && event.data.route) {
|
|
156
|
+
console.log("Received navigation command from parent:", event.data.route);
|
|
157
|
+
navigate(event.data.route);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
window.addEventListener("message", handleMessage);
|
|
161
|
+
return () => window.removeEventListener("message", handleMessage);
|
|
162
|
+
}, [navigate]);
|
|
163
|
+
};
|
|
164
|
+
var ParentRouterSync = () => {
|
|
165
|
+
useParentRoutingSync();
|
|
166
|
+
return null;
|
|
167
|
+
};
|
|
168
|
+
var PluginRouter = ({ children, initialEntries }) => {
|
|
169
|
+
const context = usePluginContext();
|
|
170
|
+
const tag = context?.tag;
|
|
171
|
+
const [initialRoute, setInitialRoute] = (0, import_react2.useState)(null);
|
|
172
|
+
const [isReady, setIsReady] = (0, import_react2.useState)(false);
|
|
173
|
+
(0, import_react2.useEffect)(() => {
|
|
174
|
+
const queryParamKey = tag ? toCamelCase(tag) + "PluginRoute" : "pluginRoute";
|
|
175
|
+
const handleMessage = (event) => {
|
|
176
|
+
if (event.data?.type === "initialRoute") {
|
|
177
|
+
console.log("Received initial route from parent:", event.data.route);
|
|
178
|
+
setInitialRoute(event.data.route || "/");
|
|
179
|
+
setIsReady(true);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
window.addEventListener("message", handleMessage);
|
|
183
|
+
window.parent.postMessage(
|
|
184
|
+
{
|
|
185
|
+
type: "getInitialRoute",
|
|
186
|
+
queryParamKey
|
|
187
|
+
},
|
|
188
|
+
"*"
|
|
189
|
+
);
|
|
190
|
+
const timeout = setTimeout(() => {
|
|
191
|
+
if (!isReady) {
|
|
192
|
+
console.log("No initial route from parent, using default");
|
|
193
|
+
setInitialRoute("/");
|
|
194
|
+
setIsReady(true);
|
|
195
|
+
}
|
|
196
|
+
}, 300);
|
|
197
|
+
return () => {
|
|
198
|
+
window.removeEventListener("message", handleMessage);
|
|
199
|
+
clearTimeout(timeout);
|
|
200
|
+
};
|
|
201
|
+
}, [tag, isReady]);
|
|
202
|
+
const defaultInitialEntries = (0, import_react2.useMemo)(() => {
|
|
203
|
+
if (initialRoute) {
|
|
204
|
+
return [initialRoute.startsWith("/") ? initialRoute : "/" + initialRoute];
|
|
205
|
+
}
|
|
206
|
+
if (initialEntries && initialEntries.length > 0) {
|
|
207
|
+
return initialEntries;
|
|
208
|
+
}
|
|
209
|
+
return ["/"];
|
|
210
|
+
}, [initialEntries, initialRoute]);
|
|
211
|
+
if (!isReady) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_router_dom.MemoryRouter, { initialEntries: defaultInitialEntries, children: [
|
|
215
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ParentRouterSync, {}),
|
|
216
|
+
children
|
|
217
|
+
] });
|
|
218
|
+
};
|
|
219
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
220
|
+
0 && (module.exports = {
|
|
221
|
+
PluginAPI,
|
|
222
|
+
PluginContextProvider,
|
|
223
|
+
PluginMessageType,
|
|
224
|
+
PluginRouter,
|
|
225
|
+
useParentRoutingSync,
|
|
226
|
+
usePluginContext
|
|
227
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React$1, { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Plugin API for communication with the host application
|
|
6
|
+
* Based on the Cortex plugin architecture pattern
|
|
7
|
+
*/
|
|
8
|
+
interface PluginContext {
|
|
9
|
+
apiBaseUrl: string;
|
|
10
|
+
entity: unknown | null;
|
|
11
|
+
location: 'ENTITY' | 'GLOBAL';
|
|
12
|
+
tag: string | null;
|
|
13
|
+
user: unknown;
|
|
14
|
+
style: Record<string, string>;
|
|
15
|
+
theme: 'light' | 'dark';
|
|
16
|
+
}
|
|
17
|
+
declare const PluginMessageType: {
|
|
18
|
+
readonly Init: "init";
|
|
19
|
+
readonly Context: "context";
|
|
20
|
+
readonly GetContext: "getContext";
|
|
21
|
+
readonly Fetch: "fetch";
|
|
22
|
+
readonly FetchResponse: "fetchResponse";
|
|
23
|
+
};
|
|
24
|
+
declare class PluginAPI {
|
|
25
|
+
/**
|
|
26
|
+
* Signals to the host that the plugin is ready
|
|
27
|
+
* This should be called as early as possible in the plugin lifecycle
|
|
28
|
+
*/
|
|
29
|
+
static init(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Retrieves the current plugin context from the host
|
|
32
|
+
* Includes entity data, user info, theme settings, and API base URL
|
|
33
|
+
*/
|
|
34
|
+
static getContext(): Promise<PluginContext>;
|
|
35
|
+
/**
|
|
36
|
+
* Proxies a fetch request through the host application
|
|
37
|
+
* This allows the host to handle authentication and CORS
|
|
38
|
+
*/
|
|
39
|
+
static proxyFetch(url: string, init?: RequestInit): Promise<Response>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare function PluginContextProvider({ children }: {
|
|
43
|
+
children: React.ReactNode;
|
|
44
|
+
}): react_jsx_runtime.JSX.Element;
|
|
45
|
+
declare function usePluginContext(): PluginContext | null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Custom hook that synchronizes the in-app route with the parent page's URL via postMessage.
|
|
49
|
+
*/
|
|
50
|
+
declare const useParentRoutingSync: () => void;
|
|
51
|
+
interface PluginRouterProps {
|
|
52
|
+
children: ReactNode;
|
|
53
|
+
initialEntries?: string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* PluginRouter wraps your app's routing logic and sets the initial route
|
|
57
|
+
* based on the parent page's URL if no initial route is provided.
|
|
58
|
+
*/
|
|
59
|
+
declare const PluginRouter: React$1.FC<PluginRouterProps>;
|
|
60
|
+
|
|
61
|
+
export { PluginAPI, type PluginContext, PluginContextProvider, PluginMessageType, PluginRouter, useParentRoutingSync, usePluginContext };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React$1, { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Plugin API for communication with the host application
|
|
6
|
+
* Based on the Cortex plugin architecture pattern
|
|
7
|
+
*/
|
|
8
|
+
interface PluginContext {
|
|
9
|
+
apiBaseUrl: string;
|
|
10
|
+
entity: unknown | null;
|
|
11
|
+
location: 'ENTITY' | 'GLOBAL';
|
|
12
|
+
tag: string | null;
|
|
13
|
+
user: unknown;
|
|
14
|
+
style: Record<string, string>;
|
|
15
|
+
theme: 'light' | 'dark';
|
|
16
|
+
}
|
|
17
|
+
declare const PluginMessageType: {
|
|
18
|
+
readonly Init: "init";
|
|
19
|
+
readonly Context: "context";
|
|
20
|
+
readonly GetContext: "getContext";
|
|
21
|
+
readonly Fetch: "fetch";
|
|
22
|
+
readonly FetchResponse: "fetchResponse";
|
|
23
|
+
};
|
|
24
|
+
declare class PluginAPI {
|
|
25
|
+
/**
|
|
26
|
+
* Signals to the host that the plugin is ready
|
|
27
|
+
* This should be called as early as possible in the plugin lifecycle
|
|
28
|
+
*/
|
|
29
|
+
static init(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Retrieves the current plugin context from the host
|
|
32
|
+
* Includes entity data, user info, theme settings, and API base URL
|
|
33
|
+
*/
|
|
34
|
+
static getContext(): Promise<PluginContext>;
|
|
35
|
+
/**
|
|
36
|
+
* Proxies a fetch request through the host application
|
|
37
|
+
* This allows the host to handle authentication and CORS
|
|
38
|
+
*/
|
|
39
|
+
static proxyFetch(url: string, init?: RequestInit): Promise<Response>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare function PluginContextProvider({ children }: {
|
|
43
|
+
children: React.ReactNode;
|
|
44
|
+
}): react_jsx_runtime.JSX.Element;
|
|
45
|
+
declare function usePluginContext(): PluginContext | null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Custom hook that synchronizes the in-app route with the parent page's URL via postMessage.
|
|
49
|
+
*/
|
|
50
|
+
declare const useParentRoutingSync: () => void;
|
|
51
|
+
interface PluginRouterProps {
|
|
52
|
+
children: ReactNode;
|
|
53
|
+
initialEntries?: string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* PluginRouter wraps your app's routing logic and sets the initial route
|
|
57
|
+
* based on the parent page's URL if no initial route is provided.
|
|
58
|
+
*/
|
|
59
|
+
declare const PluginRouter: React$1.FC<PluginRouterProps>;
|
|
60
|
+
|
|
61
|
+
export { PluginAPI, type PluginContext, PluginContextProvider, PluginMessageType, PluginRouter, useParentRoutingSync, usePluginContext };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// src/plugin-api.ts
|
|
2
|
+
var PluginMessageType = {
|
|
3
|
+
Init: "init",
|
|
4
|
+
Context: "context",
|
|
5
|
+
GetContext: "getContext",
|
|
6
|
+
Fetch: "fetch",
|
|
7
|
+
FetchResponse: "fetchResponse"
|
|
8
|
+
};
|
|
9
|
+
var PluginAPI = class {
|
|
10
|
+
/**
|
|
11
|
+
* Signals to the host that the plugin is ready
|
|
12
|
+
* This should be called as early as possible in the plugin lifecycle
|
|
13
|
+
*/
|
|
14
|
+
static async init() {
|
|
15
|
+
window.parent.postMessage({ type: PluginMessageType.Init }, "*");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Retrieves the current plugin context from the host
|
|
19
|
+
* Includes entity data, user info, theme settings, and API base URL
|
|
20
|
+
*/
|
|
21
|
+
static async getContext() {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const handler = (event) => {
|
|
24
|
+
if (event.data.type === PluginMessageType.Context) {
|
|
25
|
+
window.removeEventListener("message", handler);
|
|
26
|
+
resolve(event.data.data);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
window.addEventListener("message", handler);
|
|
30
|
+
window.parent.postMessage({ type: PluginMessageType.GetContext }, "*");
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Proxies a fetch request through the host application
|
|
35
|
+
* This allows the host to handle authentication and CORS
|
|
36
|
+
*/
|
|
37
|
+
static async proxyFetch(url, init) {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const handler = (event) => {
|
|
40
|
+
const response = event.data?.response;
|
|
41
|
+
if (response) {
|
|
42
|
+
window.removeEventListener("message", handler);
|
|
43
|
+
console.log("proxyFetch response from host:", event.data);
|
|
44
|
+
let body = response.body;
|
|
45
|
+
if (body !== null && typeof body === "object") {
|
|
46
|
+
body = JSON.stringify(body);
|
|
47
|
+
}
|
|
48
|
+
resolve(
|
|
49
|
+
new Response(body, {
|
|
50
|
+
status: response.status ?? 200,
|
|
51
|
+
statusText: response.statusText ?? "",
|
|
52
|
+
headers: response.headers
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
window.addEventListener("message", handler);
|
|
58
|
+
window.parent.postMessage(
|
|
59
|
+
{
|
|
60
|
+
type: PluginMessageType.Fetch,
|
|
61
|
+
url,
|
|
62
|
+
init
|
|
63
|
+
},
|
|
64
|
+
"*"
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/plugin-context.tsx
|
|
71
|
+
import { createContext, useContext, useState, useEffect } from "react";
|
|
72
|
+
import { jsx } from "react/jsx-runtime";
|
|
73
|
+
var PluginDataContext = createContext(null);
|
|
74
|
+
function PluginContextProvider({ children }) {
|
|
75
|
+
const [context, setContext] = useState(null);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const handler = (event) => {
|
|
78
|
+
if (event.data?.type === "context") {
|
|
79
|
+
console.log("Received context from host:", event.data.data);
|
|
80
|
+
setContext(event.data.data);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
window.addEventListener("message", handler);
|
|
84
|
+
return () => window.removeEventListener("message", handler);
|
|
85
|
+
}, []);
|
|
86
|
+
return /* @__PURE__ */ jsx(PluginDataContext.Provider, { value: context, children });
|
|
87
|
+
}
|
|
88
|
+
function usePluginContext() {
|
|
89
|
+
const context = useContext(PluginDataContext);
|
|
90
|
+
return context;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/plugin-router.tsx
|
|
94
|
+
import { useEffect as useEffect2, useMemo, useState as useState2 } from "react";
|
|
95
|
+
import { MemoryRouter, useLocation, useNavigate } from "react-router-dom";
|
|
96
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
97
|
+
var toCamelCase = (str) => {
|
|
98
|
+
return str.split("-").filter((segment) => segment.length > 0).map(
|
|
99
|
+
(word, index) => index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
|
100
|
+
).join("");
|
|
101
|
+
};
|
|
102
|
+
var useParentRoutingSync = () => {
|
|
103
|
+
const context = usePluginContext();
|
|
104
|
+
const tag = context?.tag;
|
|
105
|
+
const queryParamKey = useMemo(() => {
|
|
106
|
+
return tag ? toCamelCase(tag) + "PluginRoute" : "pluginRoute";
|
|
107
|
+
}, [tag]);
|
|
108
|
+
const location = useLocation();
|
|
109
|
+
const navigate = useNavigate();
|
|
110
|
+
useEffect2(() => {
|
|
111
|
+
const newRoute = `${location.pathname}${location.search}${location.hash}`;
|
|
112
|
+
console.log("Syncing route to parent:", newRoute);
|
|
113
|
+
window.parent.postMessage(
|
|
114
|
+
{
|
|
115
|
+
type: "routeChange",
|
|
116
|
+
route: newRoute,
|
|
117
|
+
queryParamKey
|
|
118
|
+
},
|
|
119
|
+
"*"
|
|
120
|
+
);
|
|
121
|
+
}, [location, queryParamKey]);
|
|
122
|
+
useEffect2(() => {
|
|
123
|
+
const handleMessage = (event) => {
|
|
124
|
+
if (event.data?.type === "navigateTo" && event.data.route) {
|
|
125
|
+
console.log("Received navigation command from parent:", event.data.route);
|
|
126
|
+
navigate(event.data.route);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
window.addEventListener("message", handleMessage);
|
|
130
|
+
return () => window.removeEventListener("message", handleMessage);
|
|
131
|
+
}, [navigate]);
|
|
132
|
+
};
|
|
133
|
+
var ParentRouterSync = () => {
|
|
134
|
+
useParentRoutingSync();
|
|
135
|
+
return null;
|
|
136
|
+
};
|
|
137
|
+
var PluginRouter = ({ children, initialEntries }) => {
|
|
138
|
+
const context = usePluginContext();
|
|
139
|
+
const tag = context?.tag;
|
|
140
|
+
const [initialRoute, setInitialRoute] = useState2(null);
|
|
141
|
+
const [isReady, setIsReady] = useState2(false);
|
|
142
|
+
useEffect2(() => {
|
|
143
|
+
const queryParamKey = tag ? toCamelCase(tag) + "PluginRoute" : "pluginRoute";
|
|
144
|
+
const handleMessage = (event) => {
|
|
145
|
+
if (event.data?.type === "initialRoute") {
|
|
146
|
+
console.log("Received initial route from parent:", event.data.route);
|
|
147
|
+
setInitialRoute(event.data.route || "/");
|
|
148
|
+
setIsReady(true);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
window.addEventListener("message", handleMessage);
|
|
152
|
+
window.parent.postMessage(
|
|
153
|
+
{
|
|
154
|
+
type: "getInitialRoute",
|
|
155
|
+
queryParamKey
|
|
156
|
+
},
|
|
157
|
+
"*"
|
|
158
|
+
);
|
|
159
|
+
const timeout = setTimeout(() => {
|
|
160
|
+
if (!isReady) {
|
|
161
|
+
console.log("No initial route from parent, using default");
|
|
162
|
+
setInitialRoute("/");
|
|
163
|
+
setIsReady(true);
|
|
164
|
+
}
|
|
165
|
+
}, 300);
|
|
166
|
+
return () => {
|
|
167
|
+
window.removeEventListener("message", handleMessage);
|
|
168
|
+
clearTimeout(timeout);
|
|
169
|
+
};
|
|
170
|
+
}, [tag, isReady]);
|
|
171
|
+
const defaultInitialEntries = useMemo(() => {
|
|
172
|
+
if (initialRoute) {
|
|
173
|
+
return [initialRoute.startsWith("/") ? initialRoute : "/" + initialRoute];
|
|
174
|
+
}
|
|
175
|
+
if (initialEntries && initialEntries.length > 0) {
|
|
176
|
+
return initialEntries;
|
|
177
|
+
}
|
|
178
|
+
return ["/"];
|
|
179
|
+
}, [initialEntries, initialRoute]);
|
|
180
|
+
if (!isReady) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return /* @__PURE__ */ jsxs(MemoryRouter, { initialEntries: defaultInitialEntries, children: [
|
|
184
|
+
/* @__PURE__ */ jsx2(ParentRouterSync, {}),
|
|
185
|
+
children
|
|
186
|
+
] });
|
|
187
|
+
};
|
|
188
|
+
export {
|
|
189
|
+
PluginAPI,
|
|
190
|
+
PluginContextProvider,
|
|
191
|
+
PluginMessageType,
|
|
192
|
+
PluginRouter,
|
|
193
|
+
useParentRoutingSync,
|
|
194
|
+
usePluginContext
|
|
195
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jenil94/plugin-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SDK for building Harness plugins with React",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
21
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
26
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
27
|
+
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/react": "^19.2.5",
|
|
31
|
+
"@types/react-dom": "^19.2.3",
|
|
32
|
+
"react": "^19.2.0",
|
|
33
|
+
"react-dom": "^19.2.0",
|
|
34
|
+
"react-router-dom": "^7.13.0",
|
|
35
|
+
"tsup": "^8.0.0",
|
|
36
|
+
"typescript": "~5.9.3"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"harness",
|
|
40
|
+
"plugin",
|
|
41
|
+
"sdk",
|
|
42
|
+
"react"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT"
|
|
45
|
+
}
|
|
46
|
+
|