@playcraft/iframe-bridge 0.0.1-alpha.100
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 +428 -0
- package/dist/index.d.ts +298 -0
- package/dist/index.js +463 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# @playcraft/iframe-bridge
|
|
2
|
+
|
|
3
|
+
PlayCraft iframe 通讯协议 SDK — 基于 `window.postMessage`,适用于 **平台 (Parent) ↔ iframe 内游戏/编辑器 (Child)** 场景。
|
|
4
|
+
|
|
5
|
+
发送方 **`bridge.emit(name, data)`**;接收方 **`bridge.on(name, fn)`**。需要返回结果时 callback 返回数据(SDK 自动发 `res`);仅通知、无需回包时 callback 处理完即可。是否等待 `res` 由 wire name 决定,API 形状相同。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# pnpm
|
|
13
|
+
pnpm add @playcraft/iframe-bridge
|
|
14
|
+
|
|
15
|
+
# npm
|
|
16
|
+
npm install @playcraft/iframe-bridge
|
|
17
|
+
|
|
18
|
+
# yarn
|
|
19
|
+
yarn add @playcraft/iframe-bridge
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Child 侧用法
|
|
25
|
+
|
|
26
|
+
Child 运行在 iframe 内,通过 `bridge.emit` 与 Parent 通讯。
|
|
27
|
+
|
|
28
|
+
### 1. 初始化 Bridge
|
|
29
|
+
|
|
30
|
+
推荐使用 `setupChildBridge`,它会创建 `Bridge`、注册 `host.init` 监听,并在嵌入模式下自动发送 `child.ready`:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { setupChildBridge, BridgeMethods } from '@playcraft/iframe-bridge';
|
|
34
|
+
|
|
35
|
+
// Parent 打开 iframe 时应在 URL 带上 ?parentOrigin=<parent-origin>
|
|
36
|
+
const parentOrigin = new URLSearchParams(location.search).get('parentOrigin') ?? location.origin;
|
|
37
|
+
|
|
38
|
+
const { bridge, whenParentReady } = setupChildBridge({
|
|
39
|
+
parentOrigin,
|
|
40
|
+
// readyEvent: BridgeMethods.host.init, // 默认 — Parent 发来的就绪信号
|
|
41
|
+
// announceEvent: BridgeMethods.child.ready, // 默认 — Child 发出的 bridge 就绪通知
|
|
42
|
+
debug: true, // 或 (direction, envelope) => { ... } 记录 Timeline
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 等待 Parent 发送 host.init 后再调用其他 wire name
|
|
46
|
+
const init = await whenParentReady();
|
|
47
|
+
// init.params — Parent 可选传入的初始化参数
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
若不用 `setupChildBridge`,需自行 `new Bridge({ peer: () => window.parent, ... })` 并手动处理握手。
|
|
51
|
+
|
|
52
|
+
### 2. 调用(emit)
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// 批量读路径(req 仅为 path 字符串数组,不含文件内容)
|
|
56
|
+
const { paths } = await bridge.emit(BridgeMethods.form.get, ['scene.bg', 'scene.role']);
|
|
57
|
+
// paths[i].data — string
|
|
58
|
+
|
|
59
|
+
// 批量写路径
|
|
60
|
+
await bridge.emit(BridgeMethods.form.set, {
|
|
61
|
+
paths: [{ path: 'scene.role', data: '...' }],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// AI 生图
|
|
65
|
+
await bridge.emit(BridgeMethods.image.aiGenerate, {
|
|
66
|
+
prompt: '赛博朋克风格的城市夜景',
|
|
67
|
+
referenceImageBase64: ['<base64>'],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 通知 Parent 关闭 iframe(无 res,emit 立即返回)
|
|
71
|
+
await bridge.emit(BridgeMethods.dialog.close, { reason: 'user_done' });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3. 批量调用与超时
|
|
75
|
+
|
|
76
|
+
`bridge.emit` 的数组形式用于**编排多个独立调用**(可混合不同 wire name、各自 payload)。
|
|
77
|
+
读多个 path 请用上文单次 `form.get` + path 数组(1 次往返);不要拆成多次 `form.get`。
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// 串行批量(默认)— 混合不同 wire name,按顺序执行
|
|
81
|
+
const [getRes, setRes, aiRes] = await bridge.emit([
|
|
82
|
+
[BridgeMethods.form.get, ['scene.bg', 'scene.role']],
|
|
83
|
+
[BridgeMethods.form.set, { paths: [{ path: 'scene.role', data: '...' }] }],
|
|
84
|
+
[
|
|
85
|
+
BridgeMethods.image.aiGenerate,
|
|
86
|
+
{ prompt: '赛博朋克城市', referenceImageBase64: ['<base64>'] },
|
|
87
|
+
{ timeout: 60_000 }, // 可选:该 call 单独超时
|
|
88
|
+
],
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
// 并行批量 — 互不依赖的 call 可同时发出
|
|
92
|
+
const parallel = await bridge.emit(
|
|
93
|
+
[
|
|
94
|
+
[BridgeMethods.form.get, ['scene.bg']],
|
|
95
|
+
[BridgeMethods.form.get, ['scene.ui']],
|
|
96
|
+
],
|
|
97
|
+
{ mode: 'parallel' },
|
|
98
|
+
);
|
|
99
|
+
// parallel[0] 成功时为 { paths: [...] };失败项为 { error: PlaycraftBridgeError }
|
|
100
|
+
|
|
101
|
+
// 单 call 超时 / 取消
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
await bridge.emit(BridgeMethods.image.aiGenerate, payload, {
|
|
104
|
+
timeout: 60_000,
|
|
105
|
+
signal: controller.signal,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 4. 生命周期
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// 页面卸载时销毁 Bridge,移除 message 监听
|
|
113
|
+
bridge.destroy();
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Child 侧典型时序
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
Child Parent
|
|
120
|
+
| child.ready (auto) --> |
|
|
121
|
+
| <-- | host.init
|
|
122
|
+
| form.get / set / ... --> | on → res
|
|
123
|
+
| dialog.close --> | on(无 res)→ 关闭 iframe
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Parent 侧用法
|
|
129
|
+
|
|
130
|
+
Parent 挂载 iframe、注册 `bridge.on`、响应握手与 `dialog.close`。DOM 由业务方管理;Bridge 与握手推荐用 `setupParentBridge`。
|
|
131
|
+
|
|
132
|
+
### 1. 初始化 Bridge
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { setupParentBridge, withParentOrigin, BridgeMethods } from '@playcraft/iframe-bridge';
|
|
136
|
+
|
|
137
|
+
const iframe = document.createElement('iframe');
|
|
138
|
+
iframe.src = withParentOrigin(childUrl).toString();
|
|
139
|
+
container.appendChild(iframe);
|
|
140
|
+
|
|
141
|
+
const { bridge, whenChildReady } = setupParentBridge({
|
|
142
|
+
iframe,
|
|
143
|
+
childOrigin: new URL(childUrl).origin,
|
|
144
|
+
init: { params: { theme: 'dark' } }, // 收到 child.ready 后自动 emit host.init
|
|
145
|
+
debug: true,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await whenChildReady();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`withParentOrigin` 仅负责给 Child URL 追加 `parentOrigin` query;iframe 样式、挂载时机仍由业务控制。
|
|
152
|
+
|
|
153
|
+
若不用 `setupParentBridge`,需自行 `new Bridge({ peer: () => iframe.contentWindow, ... })` 并手动处理握手。
|
|
154
|
+
|
|
155
|
+
### 2. 注册 on
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
bridge.on(BridgeMethods.form.get, async (pathKeys) => ({
|
|
159
|
+
paths: await Promise.all(
|
|
160
|
+
pathKeys.map(async (path) => ({ path, data: await loadPathData(path) })),
|
|
161
|
+
),
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
bridge.on(BridgeMethods.form.set, async ({ paths }) => {
|
|
165
|
+
for (const entry of paths) {
|
|
166
|
+
await savePathData(entry.path, entry.data);
|
|
167
|
+
}
|
|
168
|
+
return { paths: paths.map(({ path }) => path) };
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// 无 res — callback 处理完即可,不 return 结果
|
|
172
|
+
bridge.on(BridgeMethods.dialog.close, () => {
|
|
173
|
+
iframe.remove();
|
|
174
|
+
bridge.destroy();
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
完整字段与错误约定见下文 [API 参考](#api-参考)。
|
|
179
|
+
|
|
180
|
+
### 3. 生命周期
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
bridge.destroy();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Parent 侧典型时序
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
Child Parent
|
|
190
|
+
| child.ready (auto) --> | whenChildReady() resolve
|
|
191
|
+
| <-- | host.init (auto emit)
|
|
192
|
+
| form.get / set / ... --> | on → res
|
|
193
|
+
| dialog.close --> | on(无 res)→ 关闭 iframe
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Parent 侧职责小结
|
|
197
|
+
|
|
198
|
+
| 职责 | API |
|
|
199
|
+
| ---------------- | -------------------------------------- |
|
|
200
|
+
| iframe DOM / URL | 业务方(可用 `withParentOrigin`) |
|
|
201
|
+
| 握手 | `setupParentBridge` + `whenChildReady` |
|
|
202
|
+
| 业务逻辑 | `bridge.on`(有/无 res 均可) |
|
|
203
|
+
| 销毁 | `bridge.destroy()` |
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 调试
|
|
208
|
+
|
|
209
|
+
URL 带 `?debug` 或 `?debug=1` 时,`setupChildBridge` / `setupParentBridge` 会开启 envelope 日志:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { isDebugFromUrl } from '@playcraft/iframe-bridge';
|
|
213
|
+
|
|
214
|
+
setupChildBridge({
|
|
215
|
+
parentOrigin,
|
|
216
|
+
debug: isDebugFromUrl() ? (dir, env) => console.log(dir, env) : false,
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 协议概览
|
|
223
|
+
|
|
224
|
+
| 概念 | 说明 |
|
|
225
|
+
| ------------------- | --------------------------------------------------------------------------- |
|
|
226
|
+
| **Envelope** | `{ __playcraft__, kind, id, name, data?, ret?, msg? }` — 所有消息的统一信封 |
|
|
227
|
+
| **`__playcraft__`** | 协议版本,与 npm 包 `version` 字段一致 |
|
|
228
|
+
| **`req`** | 请求 envelope;接收方 `bridge.on` 处理 |
|
|
229
|
+
| **`res`** | 响应 envelope(可选)。`ret === 0` 时 `data` 为结果;失败时 `msg` 描述原因 |
|
|
230
|
+
| **PathEntry** | `{ path, data }` — `data` 恒为 **string** |
|
|
231
|
+
|
|
232
|
+
**出站**:`bridge.emit(name, data)` — 需要 `res` 的 wire name 会 await;否则立即返回。
|
|
233
|
+
|
|
234
|
+
**入站**:`bridge.on(name, fn)` — callback 返回数据则发 `res`;无返回则不回包。callback 支持 `async` / `Promise`。
|
|
235
|
+
|
|
236
|
+
Wire 名称统一为 `${domain}.${action}` 格式,在 `BridgeMethods` 中维护。
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## API 参考
|
|
241
|
+
|
|
242
|
+
### SDK 导出
|
|
243
|
+
|
|
244
|
+
| 导出 | 说明 |
|
|
245
|
+
| ------------------------------------------------ | --------------------------------------- |
|
|
246
|
+
| `Bridge`(`emit` / `on`) | postMessage 封装 + 出站/入站 API |
|
|
247
|
+
| `setupChildBridge` | Child 侧一键初始化 + 握手 |
|
|
248
|
+
| `setupParentBridge` / `withParentOrigin` | Parent 侧绑定 iframe + 握手 |
|
|
249
|
+
| `emitOne` / `normalizeInvokeCall` 等 | 批量 emit 辅助(高级) |
|
|
250
|
+
| `PlaycraftBridgeError` / `BridgeErrorCodes` | 结构化错误 |
|
|
251
|
+
| `PROTOCOL_VERSION` | 当前协议版本字符串 |
|
|
252
|
+
| `BridgeMethods` / `bridgeMethod` | 全部 wire 名称(`${domain}.${action}`) |
|
|
253
|
+
| `BRIDGE_RPC_NAMES` | 需要 `res` 的 wire 名称列表 |
|
|
254
|
+
| `RetCodes` / `errorCodeToRet` / `retToErrorCode` | Envelope `ret` 与错误码映射 |
|
|
255
|
+
| `isPlaycraftEnvelope` | 校验未知 postMessage 是否为本协议 |
|
|
256
|
+
|
|
257
|
+
Wire 名称在 `BridgeMethods` 中维护:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { BridgeMethods, bridgeMethod } from '@playcraft/iframe-bridge';
|
|
261
|
+
|
|
262
|
+
BridgeMethods.form.get; // 'form.get' — emit 等待 res
|
|
263
|
+
BridgeMethods.dialog.close; // 'dialog.close' — emit 立即返回
|
|
264
|
+
bridgeMethod('form', 'get'); // 'form.get'
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
修改 wire 名称时只需改 `BridgeMethods`(`bridgeMethod('domain', 'action')`)一处;`MethodMap`、`EventMap`、`InvokeMap` 等均从此派生。
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### `form.get`
|
|
272
|
+
|
|
273
|
+
批量读取路径内容。
|
|
274
|
+
|
|
275
|
+
**方向**:Child → Parent(Child `emit`,Parent `on`)
|
|
276
|
+
|
|
277
|
+
**Request** `string[]` — path 列表,不含 data
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
['scene.bg', 'scene.role'];
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Response** `{ paths: PathEntry[] }`
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
{
|
|
287
|
+
paths: [
|
|
288
|
+
{ path: 'scene.bg', data: '...' },
|
|
289
|
+
{ path: 'scene.role', data: '...' },
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
### `form.set`
|
|
297
|
+
|
|
298
|
+
批量写入路径。
|
|
299
|
+
|
|
300
|
+
**方向**:Child → Parent(Child `emit`,Parent `on`)
|
|
301
|
+
|
|
302
|
+
**Request** `{ paths: PathEntry[] }`
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
{
|
|
306
|
+
paths: [{ path: 'scene.role', data: '...' }],
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Response**(`ret === 0` 时)`{ paths: string[] }` — 已成功写入的 path 列表
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
{
|
|
314
|
+
paths: ['scene.role'],
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
任一路径写入失败时 Parent handler 应 `throw PlaycraftBridgeError`,Child 侧 `emit` 收到 `ret !== 0` 与 `msg`。
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
### `image.aiGenerate`
|
|
323
|
+
|
|
324
|
+
**Request** `AiGenerateImagePayload`
|
|
325
|
+
|
|
326
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
327
|
+
| ---------------------- | ----------- | ---- | -------------------------------------- |
|
|
328
|
+
| `prompt` | `string` | | 生成提示词 |
|
|
329
|
+
| `referenceImageBase64` | `string[]` | | 参考图 base64 列表(无 `data:` 前缀) |
|
|
330
|
+
|
|
331
|
+
**Response** `{ paths: PathEntry[] }`
|
|
332
|
+
|
|
333
|
+
生成结果已写入 `path`(多张时 Parent 可能使用 `path#0`、`path#1` 等衍生路径)。`data` 为 base64 字符串。
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
{
|
|
337
|
+
paths: [{ path: 'scene.bg', data: '<base64>' }];
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
### `child.ready`
|
|
344
|
+
|
|
345
|
+
**方向**:Child → Parent · **res**:无
|
|
346
|
+
**Payload**:`{}`
|
|
347
|
+
|
|
348
|
+
`setupChildBridge` 在嵌入模式下自动 `emit`。Parent 侧 `setupParentBridge` 收到后自动 `emit` `host.init`。
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
### `host.init`
|
|
353
|
+
|
|
354
|
+
**方向**:Parent → Child · **res**:无
|
|
355
|
+
**Payload**:`{ params?: Record<string, unknown> }`
|
|
356
|
+
|
|
357
|
+
Child 侧 `whenParentReady()` / Parent 侧 `whenChildReady()` 在握手完成后 resolve。
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
### `dialog.close`
|
|
362
|
+
|
|
363
|
+
**方向**:Child → Parent · **res**:无
|
|
364
|
+
**Payload**:`{ reason?: string }`
|
|
365
|
+
|
|
366
|
+
Parent 用 `bridge.on` 注册(无 return),通常在此移除 iframe 并 `bridge.destroy()`。
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
### 错误码
|
|
371
|
+
|
|
372
|
+
有 `res` 的调用失败时 `ret !== 0`,`msg` 为错误描述;`ret` 与 `BridgeErrorCodes` 的映射见 `RetCodes`:
|
|
373
|
+
|
|
374
|
+
| ret | Code | 含义 |
|
|
375
|
+
| --- | ------------------ | ------------------ |
|
|
376
|
+
| `0` | — | 成功 |
|
|
377
|
+
| `1` | `INVALID_PARAM` | 参数不合法 |
|
|
378
|
+
| `2` | `TIMEOUT` | 等待响应超时 |
|
|
379
|
+
| `3` | `ABORTED` | 调用方 abort |
|
|
380
|
+
| `4` | `REQUEST_FAILED` | 通用失败 |
|
|
381
|
+
| `5` | `INTERNAL_ERROR` | Handler 内部错误 |
|
|
382
|
+
| `6` | `PEER_UNAVAILABLE` | 对端 window 不可用 |
|
|
383
|
+
| `7` | `DESTROYED` | Bridge 已销毁 |
|
|
384
|
+
| `8` | `METHOD_NOT_FOUND` | 方法未注册 |
|
|
385
|
+
|
|
386
|
+
Client 侧 `bridge.emit` 对有 `res` 的 wire name 失败时会 `reject` 为 `PlaycraftBridgeError`(由 `ret` 反查 `code`)。
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
### Envelope 示例
|
|
391
|
+
|
|
392
|
+
**请求(有 res)**
|
|
393
|
+
|
|
394
|
+
```json
|
|
395
|
+
{
|
|
396
|
+
"__playcraft__": "0.0.1",
|
|
397
|
+
"kind": "req",
|
|
398
|
+
"id": "1702819473736-0",
|
|
399
|
+
"name": "form.get",
|
|
400
|
+
"data": ["scene.bg", "scene.role"]
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**成功响应**
|
|
405
|
+
|
|
406
|
+
```json
|
|
407
|
+
{
|
|
408
|
+
"__playcraft__": "0.0.1",
|
|
409
|
+
"kind": "res",
|
|
410
|
+
"id": "1702819473736-0",
|
|
411
|
+
"name": "form.get",
|
|
412
|
+
"ret": 0,
|
|
413
|
+
"data": { "paths": [{ "path": "scene.bg", "data": "..." }] }
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**失败响应**
|
|
418
|
+
|
|
419
|
+
```json
|
|
420
|
+
{
|
|
421
|
+
"__playcraft__": "0.0.1",
|
|
422
|
+
"kind": "res",
|
|
423
|
+
"id": "1702819473736-0",
|
|
424
|
+
"name": "form.set",
|
|
425
|
+
"ret": 1,
|
|
426
|
+
"msg": "Invalid path: scene.unknown"
|
|
427
|
+
}
|
|
428
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
type Kind = 'req' | 'res';
|
|
2
|
+
/** RPC failure — maps to envelope `ret` / `msg` on the wire. */
|
|
3
|
+
type RpcError = {
|
|
4
|
+
ret: number;
|
|
5
|
+
msg: string;
|
|
6
|
+
};
|
|
7
|
+
type Envelope<T = unknown> = {
|
|
8
|
+
/** Package version from package.json */
|
|
9
|
+
__playcraft__: string;
|
|
10
|
+
kind: Kind;
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
data?: T;
|
|
14
|
+
/** RPC response only — 0 success, non-zero failure */
|
|
15
|
+
ret?: number;
|
|
16
|
+
/** RPC error message when ret !== 0 */
|
|
17
|
+
msg?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** All wire names — format `${domain}.${action}` (RPC + one-way events). */
|
|
21
|
+
declare const BridgeMethods: {
|
|
22
|
+
readonly form: {
|
|
23
|
+
readonly get: "form.get";
|
|
24
|
+
readonly set: "form.set";
|
|
25
|
+
};
|
|
26
|
+
readonly image: {
|
|
27
|
+
readonly aiGenerate: "image.aiGenerate";
|
|
28
|
+
};
|
|
29
|
+
readonly dialog: {
|
|
30
|
+
readonly close: "dialog.close";
|
|
31
|
+
};
|
|
32
|
+
readonly child: {
|
|
33
|
+
readonly ready: "child.ready";
|
|
34
|
+
};
|
|
35
|
+
readonly host: {
|
|
36
|
+
readonly init: "host.init";
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
type BridgeMethodDomain = keyof typeof BridgeMethods;
|
|
40
|
+
/** One-way events (no `res` envelope). */
|
|
41
|
+
declare const BRIDGE_EVENT_NAMES: readonly ["child.ready", "host.init", "dialog.close"];
|
|
42
|
+
type BridgeEventName = (typeof BRIDGE_EVENT_NAMES)[number];
|
|
43
|
+
/** RPC methods (request/response). */
|
|
44
|
+
declare const BRIDGE_RPC_NAMES: readonly ["form.get", "form.set", "image.aiGenerate"];
|
|
45
|
+
type BridgeRpcName = (typeof BRIDGE_RPC_NAMES)[number];
|
|
46
|
+
/** Build a wire name from domain + action keys. */
|
|
47
|
+
declare function bridgeMethod<D extends BridgeMethodDomain>(domain: D, action: keyof (typeof BridgeMethods)[D] & string): string;
|
|
48
|
+
|
|
49
|
+
type ChildReadyEvt = Record<string, never>;
|
|
50
|
+
type DialogCloseEvt = {
|
|
51
|
+
reason?: string;
|
|
52
|
+
};
|
|
53
|
+
type HostInitEvt = {
|
|
54
|
+
params?: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
type EventMap = {
|
|
57
|
+
[BridgeMethods.child.ready]: ChildReadyEvt;
|
|
58
|
+
[BridgeMethods.host.init]: HostInitEvt;
|
|
59
|
+
[BridgeMethods.dialog.close]: DialogCloseEvt;
|
|
60
|
+
};
|
|
61
|
+
type EventName = keyof EventMap;
|
|
62
|
+
|
|
63
|
+
/** Path payload in responses and form.set requests — always a string. */
|
|
64
|
+
type PathEntry = {
|
|
65
|
+
path: string;
|
|
66
|
+
/** JSON string for structured config; base64 (no data: prefix) for binary/image assets. */
|
|
67
|
+
data: string;
|
|
68
|
+
};
|
|
69
|
+
/** Path keys only — no data on read. */
|
|
70
|
+
type PathsGetReq = string[];
|
|
71
|
+
type PathsGetRes = {
|
|
72
|
+
paths: PathEntry[];
|
|
73
|
+
};
|
|
74
|
+
type PathsSetReq = {
|
|
75
|
+
paths: PathEntry[];
|
|
76
|
+
};
|
|
77
|
+
type PathsSetRes = {
|
|
78
|
+
/** Paths successfully written — present when envelope `ret === 0`. */
|
|
79
|
+
paths: string[];
|
|
80
|
+
};
|
|
81
|
+
/** Request body for `image.aiGenerate`. */
|
|
82
|
+
type AiGenerateImagePayload = {
|
|
83
|
+
/** User prompt — optional when reference images are provided. */
|
|
84
|
+
prompt?: string;
|
|
85
|
+
/** Reference images as base64 strings (no `data:` prefix). */
|
|
86
|
+
referenceImageBase64?: string[];
|
|
87
|
+
};
|
|
88
|
+
type AiGenerateImageResult = {
|
|
89
|
+
paths: PathEntry[];
|
|
90
|
+
};
|
|
91
|
+
type MethodMap = {
|
|
92
|
+
[BridgeMethods.form.get]: {
|
|
93
|
+
req: PathsGetReq;
|
|
94
|
+
res: PathsGetRes;
|
|
95
|
+
};
|
|
96
|
+
[BridgeMethods.form.set]: {
|
|
97
|
+
req: PathsSetReq;
|
|
98
|
+
res: PathsSetRes;
|
|
99
|
+
};
|
|
100
|
+
[BridgeMethods.image.aiGenerate]: {
|
|
101
|
+
req: AiGenerateImagePayload;
|
|
102
|
+
res: AiGenerateImageResult;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
type MethodName = keyof MethodMap;
|
|
106
|
+
|
|
107
|
+
/** RPC methods + one-way events — unified outbound API via `invoke`. */
|
|
108
|
+
type InvokeMap = MethodMap & {
|
|
109
|
+
[K in EventName]: {
|
|
110
|
+
req: EventMap[K];
|
|
111
|
+
res: void;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
type InvokeName = keyof InvokeMap;
|
|
115
|
+
|
|
116
|
+
type DebugLogger = (event: 'send' | 'recv', msg: Envelope) => void;
|
|
117
|
+
type RequestOptions = {
|
|
118
|
+
timeout?: number;
|
|
119
|
+
signal?: AbortSignal;
|
|
120
|
+
};
|
|
121
|
+
type InvokeBatchOptions = RequestOptions & {
|
|
122
|
+
mode?: 'serial' | 'parallel';
|
|
123
|
+
};
|
|
124
|
+
type InvokeCall = {
|
|
125
|
+
[K in MethodName]: {
|
|
126
|
+
name: K;
|
|
127
|
+
data: MethodMap[K]['req'];
|
|
128
|
+
options?: RequestOptions;
|
|
129
|
+
};
|
|
130
|
+
}[MethodName];
|
|
131
|
+
type InvokeTuple = [MethodName, MethodMap[MethodName]['req']] | [MethodName, MethodMap[MethodName]['req'], RequestOptions?];
|
|
132
|
+
type InvokeGroup = {
|
|
133
|
+
mode?: 'serial' | 'parallel';
|
|
134
|
+
calls: readonly (InvokeCall | InvokeTuple)[];
|
|
135
|
+
};
|
|
136
|
+
type InvokeBatchInput = readonly (InvokeCall | InvokeTuple)[];
|
|
137
|
+
type InvokeGroupsInput = readonly InvokeGroup[];
|
|
138
|
+
type BridgeOptions = {
|
|
139
|
+
peer: () => Window | null;
|
|
140
|
+
targetOrigin: string;
|
|
141
|
+
allowedOrigins: string[];
|
|
142
|
+
timeout?: number;
|
|
143
|
+
debug?: boolean | DebugLogger;
|
|
144
|
+
/** Defaults to window; inject MockMessageTarget in tests */
|
|
145
|
+
messageTarget?: EventTarget;
|
|
146
|
+
};
|
|
147
|
+
type Handler<K extends MethodName> = (data: MethodMap[K]['req'], ctx: {
|
|
148
|
+
id: string;
|
|
149
|
+
}) => MethodMap[K]['res'] | Promise<MethodMap[K]['res']>;
|
|
150
|
+
/** Handler for wire names that do not send `res` — same as `handle`, no reply. */
|
|
151
|
+
type NotifyHandler<K extends EventName> = (data: EventMap[K]) => void | Promise<void>;
|
|
152
|
+
type BridgeMessageEvent = {
|
|
153
|
+
data: Envelope;
|
|
154
|
+
origin: string;
|
|
155
|
+
source: MessageEventSource | null;
|
|
156
|
+
};
|
|
157
|
+
type PendingReq = {
|
|
158
|
+
resolve: (value: unknown) => void;
|
|
159
|
+
reject: (reason: unknown) => void;
|
|
160
|
+
timer: ReturnType<typeof setTimeout>;
|
|
161
|
+
cleanupAbortListener?: () => void;
|
|
162
|
+
offMessage: () => void;
|
|
163
|
+
};
|
|
164
|
+
/** parallel batch item: success is normal res, failure is { error: RpcError } */
|
|
165
|
+
type InvokeParallelResult<T> = T | {
|
|
166
|
+
error: RpcError;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
declare class Bridge {
|
|
170
|
+
private readonly options;
|
|
171
|
+
readonly targetOrigin: string;
|
|
172
|
+
private readonly listeners;
|
|
173
|
+
private destroyed;
|
|
174
|
+
private readonly messageTarget;
|
|
175
|
+
constructor(options: BridgeOptions);
|
|
176
|
+
emit<K extends InvokeName>(name: K, data: InvokeMap[K]['req'], options?: RequestOptions): Promise<InvokeMap[K]['res']>;
|
|
177
|
+
emit(calls: InvokeBatchInput, options?: InvokeBatchOptions): Promise<unknown[] | InvokeParallelResult<unknown>[]>;
|
|
178
|
+
emit(groups: InvokeGroupsInput, options?: InvokeBatchOptions): Promise<unknown[][] | InvokeParallelResult<unknown[]>[]>;
|
|
179
|
+
on<K extends MethodName>(name: K, fn: Handler<K>): () => void;
|
|
180
|
+
on<K extends EventName>(name: K, fn: NotifyHandler<K>): () => void;
|
|
181
|
+
postMessage(message: Envelope, targetOrigin?: string, transfer?: Transferable[]): void;
|
|
182
|
+
addEventListener(type: 'message', listener: (event: BridgeMessageEvent) => void): () => void;
|
|
183
|
+
destroy(): void;
|
|
184
|
+
private assertAlive;
|
|
185
|
+
private log;
|
|
186
|
+
private onWindowMessage;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
declare function normalizeInvokeCall(call: InvokeCall | InvokeTuple): InvokeCall;
|
|
190
|
+
declare function isInvokeGroups(input: unknown): input is InvokeGroupsInput;
|
|
191
|
+
declare function settleParallel<T>(promises: readonly Promise<T>[]): Promise<InvokeParallelResult<T>[]>;
|
|
192
|
+
declare function emitOne<K extends InvokeName>(bridge: Bridge, name: K, data: InvokeMap[K]['req'], options?: RequestOptions, defaultTimeout?: number): Promise<InvokeMap[K]['res']>;
|
|
193
|
+
|
|
194
|
+
/** Default one-way events used by the child-side handshake. */
|
|
195
|
+
type ChildBridgeDefaultEvents = {
|
|
196
|
+
readyEvent: typeof BridgeMethods.host.init;
|
|
197
|
+
announceEvent: typeof BridgeMethods.child.ready;
|
|
198
|
+
};
|
|
199
|
+
type SetupChildBridgeOptions = {
|
|
200
|
+
parentOrigin: string;
|
|
201
|
+
/** Parent → child event that resolves {@link SetupChildBridgeResult.whenParentReady}. */
|
|
202
|
+
readyEvent?: ChildBridgeDefaultEvents['readyEvent'];
|
|
203
|
+
/** Child → parent event announcing bridge readiness. */
|
|
204
|
+
announceEvent?: ChildBridgeDefaultEvents['announceEvent'];
|
|
205
|
+
debug?: boolean | DebugLogger;
|
|
206
|
+
};
|
|
207
|
+
type SetupChildBridgeResult = {
|
|
208
|
+
bridge: Bridge;
|
|
209
|
+
whenParentReady: () => Promise<EventMap[ChildBridgeDefaultEvents['readyEvent']]>;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/** Default one-way events used by the child-side handshake. */
|
|
213
|
+
declare const ChildBridgeDefaults: {
|
|
214
|
+
readonly readyEvent: "host.init";
|
|
215
|
+
readonly announceEvent: "child.ready";
|
|
216
|
+
};
|
|
217
|
+
declare function setupChildBridge(options: SetupChildBridgeOptions | string): SetupChildBridgeResult;
|
|
218
|
+
|
|
219
|
+
/** Default one-way events used by the parent-side handshake. */
|
|
220
|
+
type ParentBridgeDefaultEvents = {
|
|
221
|
+
announceEvent: typeof BridgeMethods.child.ready;
|
|
222
|
+
readyEvent: typeof BridgeMethods.host.init;
|
|
223
|
+
};
|
|
224
|
+
type SetupParentBridgeOptions = {
|
|
225
|
+
/** Existing iframe element — DOM lifecycle stays with the caller. */
|
|
226
|
+
iframe: HTMLIFrameElement;
|
|
227
|
+
/** Child frame origin for postMessage targetOrigin / allowedOrigins. */
|
|
228
|
+
childOrigin: string;
|
|
229
|
+
/** Payload sent via invoke(readyEvent) when announceEvent arrives. */
|
|
230
|
+
init?: HostInitEvt;
|
|
231
|
+
/** Child → parent event that resolves {@link SetupParentBridgeResult.whenChildReady}. */
|
|
232
|
+
announceEvent?: ParentBridgeDefaultEvents['announceEvent'];
|
|
233
|
+
/** Parent → child event sent automatically after announceEvent. */
|
|
234
|
+
readyEvent?: ParentBridgeDefaultEvents['readyEvent'];
|
|
235
|
+
debug?: boolean | DebugLogger;
|
|
236
|
+
};
|
|
237
|
+
type SetupParentBridgeResult = {
|
|
238
|
+
bridge: Bridge;
|
|
239
|
+
whenChildReady: () => Promise<EventMap[ParentBridgeDefaultEvents['announceEvent']]>;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** Default one-way events used by the parent-side handshake. */
|
|
243
|
+
declare const ParentBridgeDefaults: {
|
|
244
|
+
readonly announceEvent: "child.ready";
|
|
245
|
+
readonly readyEvent: "host.init";
|
|
246
|
+
};
|
|
247
|
+
declare function setupParentBridge(options: SetupParentBridgeOptions): SetupParentBridgeResult;
|
|
248
|
+
/** Append `parentOrigin` query param for child iframe URLs. */
|
|
249
|
+
declare function withParentOrigin(childUrl: string | URL, parentOrigin?: string): URL;
|
|
250
|
+
|
|
251
|
+
/** Protocol marker on every envelope; matches package.json version */
|
|
252
|
+
declare const PROTOCOL_VERSION: string;
|
|
253
|
+
declare const DEFAULT_REQUEST_TIMEOUT_MS = 150000;
|
|
254
|
+
declare const BridgeErrorCodes: {
|
|
255
|
+
readonly ABORTED: "ABORTED";
|
|
256
|
+
readonly DESTROYED: "DESTROYED";
|
|
257
|
+
readonly INTERNAL_ERROR: "INTERNAL_ERROR";
|
|
258
|
+
readonly INVALID_PARAM: "INVALID_PARAM";
|
|
259
|
+
readonly METHOD_NOT_FOUND: "METHOD_NOT_FOUND";
|
|
260
|
+
readonly PEER_UNAVAILABLE: "PEER_UNAVAILABLE";
|
|
261
|
+
readonly REQUEST_FAILED: "REQUEST_FAILED";
|
|
262
|
+
readonly TIMEOUT: "TIMEOUT";
|
|
263
|
+
};
|
|
264
|
+
type BridgeErrorCode = (typeof BridgeErrorCodes)[keyof typeof BridgeErrorCodes];
|
|
265
|
+
/** Envelope `ret` on RPC responses — 0 means success, non-zero means failure. */
|
|
266
|
+
declare const RetCodes: {
|
|
267
|
+
readonly OK: 0;
|
|
268
|
+
readonly INVALID_PARAM: 1;
|
|
269
|
+
readonly TIMEOUT: 2;
|
|
270
|
+
readonly ABORTED: 3;
|
|
271
|
+
readonly REQUEST_FAILED: 4;
|
|
272
|
+
readonly INTERNAL_ERROR: 5;
|
|
273
|
+
readonly PEER_UNAVAILABLE: 6;
|
|
274
|
+
readonly DESTROYED: 7;
|
|
275
|
+
readonly METHOD_NOT_FOUND: 8;
|
|
276
|
+
};
|
|
277
|
+
type RetCode = (typeof RetCodes)[keyof typeof RetCodes];
|
|
278
|
+
declare function errorCodeToRet(code: string): RetCode;
|
|
279
|
+
declare function retToErrorCode(ret: number): BridgeErrorCode;
|
|
280
|
+
|
|
281
|
+
/** True when `debug` query param is set (e.g. `?debug`, `?debug=1`, `?debug=true`). */
|
|
282
|
+
declare function isDebugFromUrl(search?: string | URLSearchParams): boolean;
|
|
283
|
+
|
|
284
|
+
declare function createEnvelope(kind: Kind, id: string, name: string, data?: unknown): Envelope;
|
|
285
|
+
declare function createSuccessResponse(id: string, name: string, data: unknown): Envelope;
|
|
286
|
+
declare function createErrorResponse(id: string, name: string, error: RpcError): Envelope;
|
|
287
|
+
declare function isPlaycraftEnvelope(data: unknown): data is Envelope;
|
|
288
|
+
|
|
289
|
+
declare class PlaycraftBridgeError extends Error {
|
|
290
|
+
readonly code: string;
|
|
291
|
+
readonly detail?: unknown;
|
|
292
|
+
constructor(code: BridgeErrorCode | string, message: string, detail?: unknown);
|
|
293
|
+
}
|
|
294
|
+
/** Normalize thrown/unknown errors to wire-shaped `{ ret, msg }`. */
|
|
295
|
+
declare function toPlaycraftError(err: unknown): RpcError;
|
|
296
|
+
|
|
297
|
+
export { BRIDGE_EVENT_NAMES, BRIDGE_RPC_NAMES, Bridge, BridgeErrorCodes, BridgeMethods, ChildBridgeDefaults, DEFAULT_REQUEST_TIMEOUT_MS, PROTOCOL_VERSION, ParentBridgeDefaults, PlaycraftBridgeError, RetCodes, bridgeMethod, createEnvelope, createErrorResponse, createSuccessResponse, emitOne, errorCodeToRet, isDebugFromUrl, isInvokeGroups, isPlaycraftEnvelope, normalizeInvokeCall, retToErrorCode, settleParallel, setupChildBridge, setupParentBridge, toPlaycraftError, withParentOrigin };
|
|
298
|
+
export type { AiGenerateImagePayload, AiGenerateImageResult, BridgeErrorCode, BridgeEventName, BridgeMessageEvent, BridgeMethodDomain, BridgeOptions, BridgeRpcName, ChildBridgeDefaultEvents, ChildReadyEvt, DebugLogger, DialogCloseEvt, Envelope, EventMap, EventName, Handler, HostInitEvt, InvokeBatchInput, InvokeBatchOptions, InvokeCall, InvokeGroup, InvokeGroupsInput, InvokeMap, InvokeName, InvokeParallelResult, InvokeTuple, Kind, MethodMap, MethodName, NotifyHandler, ParentBridgeDefaultEvents, PathEntry, PathsGetReq, PathsGetRes, PathsSetReq, PathsSetRes, PendingReq, RequestOptions, RetCode, RpcError, SetupChildBridgeOptions, SetupChildBridgeResult, SetupParentBridgeOptions, SetupParentBridgeResult };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
var version = "0.0.1-alpha.100";
|
|
2
|
+
var pkg = {
|
|
3
|
+
version: version};
|
|
4
|
+
|
|
5
|
+
/** Protocol marker on every envelope; matches package.json version */ const PROTOCOL_VERSION = pkg.version;
|
|
6
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 150_000;
|
|
7
|
+
const BridgeErrorCodes = {
|
|
8
|
+
ABORTED: 'ABORTED',
|
|
9
|
+
DESTROYED: 'DESTROYED',
|
|
10
|
+
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
11
|
+
INVALID_PARAM: 'INVALID_PARAM',
|
|
12
|
+
METHOD_NOT_FOUND: 'METHOD_NOT_FOUND',
|
|
13
|
+
PEER_UNAVAILABLE: 'PEER_UNAVAILABLE',
|
|
14
|
+
REQUEST_FAILED: 'REQUEST_FAILED',
|
|
15
|
+
TIMEOUT: 'TIMEOUT'
|
|
16
|
+
};
|
|
17
|
+
/** Envelope `ret` on RPC responses — 0 means success, non-zero means failure. */ const RetCodes = {
|
|
18
|
+
OK: 0,
|
|
19
|
+
INVALID_PARAM: 1,
|
|
20
|
+
TIMEOUT: 2,
|
|
21
|
+
ABORTED: 3,
|
|
22
|
+
REQUEST_FAILED: 4,
|
|
23
|
+
INTERNAL_ERROR: 5,
|
|
24
|
+
PEER_UNAVAILABLE: 6,
|
|
25
|
+
DESTROYED: 7,
|
|
26
|
+
METHOD_NOT_FOUND: 8
|
|
27
|
+
};
|
|
28
|
+
const ERROR_CODE_TO_RET = {
|
|
29
|
+
[BridgeErrorCodes.INVALID_PARAM]: RetCodes.INVALID_PARAM,
|
|
30
|
+
[BridgeErrorCodes.TIMEOUT]: RetCodes.TIMEOUT,
|
|
31
|
+
[BridgeErrorCodes.ABORTED]: RetCodes.ABORTED,
|
|
32
|
+
[BridgeErrorCodes.REQUEST_FAILED]: RetCodes.REQUEST_FAILED,
|
|
33
|
+
[BridgeErrorCodes.INTERNAL_ERROR]: RetCodes.INTERNAL_ERROR,
|
|
34
|
+
[BridgeErrorCodes.PEER_UNAVAILABLE]: RetCodes.PEER_UNAVAILABLE,
|
|
35
|
+
[BridgeErrorCodes.DESTROYED]: RetCodes.DESTROYED,
|
|
36
|
+
[BridgeErrorCodes.METHOD_NOT_FOUND]: RetCodes.METHOD_NOT_FOUND
|
|
37
|
+
};
|
|
38
|
+
const RET_TO_ERROR_CODE = {
|
|
39
|
+
[RetCodes.INVALID_PARAM]: BridgeErrorCodes.INVALID_PARAM,
|
|
40
|
+
[RetCodes.TIMEOUT]: BridgeErrorCodes.TIMEOUT,
|
|
41
|
+
[RetCodes.ABORTED]: BridgeErrorCodes.ABORTED,
|
|
42
|
+
[RetCodes.REQUEST_FAILED]: BridgeErrorCodes.REQUEST_FAILED,
|
|
43
|
+
[RetCodes.INTERNAL_ERROR]: BridgeErrorCodes.INTERNAL_ERROR,
|
|
44
|
+
[RetCodes.PEER_UNAVAILABLE]: BridgeErrorCodes.PEER_UNAVAILABLE,
|
|
45
|
+
[RetCodes.DESTROYED]: BridgeErrorCodes.DESTROYED,
|
|
46
|
+
[RetCodes.METHOD_NOT_FOUND]: BridgeErrorCodes.METHOD_NOT_FOUND
|
|
47
|
+
};
|
|
48
|
+
function errorCodeToRet(code) {
|
|
49
|
+
return ERROR_CODE_TO_RET[code] ?? RetCodes.REQUEST_FAILED;
|
|
50
|
+
}
|
|
51
|
+
function retToErrorCode(ret) {
|
|
52
|
+
return RET_TO_ERROR_CODE[ret] ?? BridgeErrorCodes.REQUEST_FAILED;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createEnvelope(kind, id, name, data) {
|
|
56
|
+
return {
|
|
57
|
+
__playcraft__: PROTOCOL_VERSION,
|
|
58
|
+
kind,
|
|
59
|
+
id,
|
|
60
|
+
name,
|
|
61
|
+
data
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function createSuccessResponse(id, name, data) {
|
|
65
|
+
return {
|
|
66
|
+
...createEnvelope('res', id, name),
|
|
67
|
+
ret: RetCodes.OK,
|
|
68
|
+
data
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function createErrorResponse(id, name, error) {
|
|
72
|
+
return {
|
|
73
|
+
...createEnvelope('res', id, name),
|
|
74
|
+
ret: error.ret,
|
|
75
|
+
msg: error.msg
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function isPlaycraftEnvelope(data) {
|
|
79
|
+
return !!data && typeof data === 'object' && data.__playcraft__ === PROTOCOL_VERSION;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class PlaycraftBridgeError extends Error {
|
|
83
|
+
constructor(code, message, detail){
|
|
84
|
+
super(message);
|
|
85
|
+
this.name = 'PlaycraftBridgeError';
|
|
86
|
+
this.code = code;
|
|
87
|
+
this.detail = detail;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function isRpcError(value) {
|
|
91
|
+
return !!value && typeof value === 'object' && 'ret' in value && 'msg' in value && typeof value.ret === 'number' && typeof value.msg === 'string';
|
|
92
|
+
}
|
|
93
|
+
function isLegacyCodeMessageError(value) {
|
|
94
|
+
return !!value && typeof value === 'object' && 'code' in value && 'message' in value && typeof value.code === 'string' && typeof value.message === 'string';
|
|
95
|
+
}
|
|
96
|
+
/** Normalize thrown/unknown errors to wire-shaped `{ ret, msg }`. */ function toPlaycraftError(err) {
|
|
97
|
+
if (isRpcError(err)) {
|
|
98
|
+
return err;
|
|
99
|
+
}
|
|
100
|
+
if (err instanceof PlaycraftBridgeError) {
|
|
101
|
+
return {
|
|
102
|
+
ret: errorCodeToRet(err.code),
|
|
103
|
+
msg: err.message
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (isLegacyCodeMessageError(err)) {
|
|
107
|
+
return {
|
|
108
|
+
ret: errorCodeToRet(err.code),
|
|
109
|
+
msg: err.message
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
ret: RetCodes.INTERNAL_ERROR,
|
|
114
|
+
msg: err instanceof Error ? err.message : String(err)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** All wire names — format `${domain}.${action}` (RPC + one-way events). */ const BridgeMethods = {
|
|
119
|
+
form: {
|
|
120
|
+
get: 'form.get',
|
|
121
|
+
set: 'form.set'
|
|
122
|
+
},
|
|
123
|
+
image: {
|
|
124
|
+
aiGenerate: 'image.aiGenerate'
|
|
125
|
+
},
|
|
126
|
+
dialog: {
|
|
127
|
+
close: 'dialog.close'
|
|
128
|
+
},
|
|
129
|
+
child: {
|
|
130
|
+
ready: 'child.ready'
|
|
131
|
+
},
|
|
132
|
+
host: {
|
|
133
|
+
init: 'host.init'
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
/** One-way events (no `res` envelope). */ const BRIDGE_EVENT_NAMES = [
|
|
137
|
+
BridgeMethods.child.ready,
|
|
138
|
+
BridgeMethods.host.init,
|
|
139
|
+
BridgeMethods.dialog.close
|
|
140
|
+
];
|
|
141
|
+
/** RPC methods (request/response). */ const BRIDGE_RPC_NAMES = [
|
|
142
|
+
BridgeMethods.form.get,
|
|
143
|
+
BridgeMethods.form.set,
|
|
144
|
+
BridgeMethods.image.aiGenerate
|
|
145
|
+
];
|
|
146
|
+
/** Build a wire name from domain + action keys. */ function bridgeMethod(domain, action) {
|
|
147
|
+
return BridgeMethods[domain][action];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const EVENT_NAMES = new Set(BRIDGE_EVENT_NAMES);
|
|
151
|
+
function isEventName(name) {
|
|
152
|
+
return EVENT_NAMES.has(name);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let idSeq = 0;
|
|
156
|
+
function genId() {
|
|
157
|
+
return `${Date.now()}-${idSeq++}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Send a one-way or RPC `req` envelope (no response handling). */ function sendReq(bridge, name, data) {
|
|
161
|
+
bridge.postMessage(createEnvelope('req', genId(), name, data));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeInvokeCall(call) {
|
|
165
|
+
if (Array.isArray(call)) {
|
|
166
|
+
const [name, data, options] = call;
|
|
167
|
+
return {
|
|
168
|
+
name,
|
|
169
|
+
data,
|
|
170
|
+
options
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return call;
|
|
174
|
+
}
|
|
175
|
+
function isInvokeGroups(input) {
|
|
176
|
+
return Array.isArray(input) && input.length > 0 && typeof input[0] === 'object' && input[0] !== null && 'calls' in input[0];
|
|
177
|
+
}
|
|
178
|
+
async function settleParallel(promises) {
|
|
179
|
+
const settled = await Promise.allSettled(promises);
|
|
180
|
+
return settled.map((outcome)=>outcome.status === 'fulfilled' ? outcome.value : {
|
|
181
|
+
error: toPlaycraftError(outcome.reason)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async function runInvokeCalls(bridge, calls, mode, batchOptions = {}, defaultTimeout = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
185
|
+
const normalized = calls.map(normalizeInvokeCall);
|
|
186
|
+
const shared = {
|
|
187
|
+
timeout: batchOptions.timeout,
|
|
188
|
+
signal: batchOptions.signal
|
|
189
|
+
};
|
|
190
|
+
if (mode === 'parallel') {
|
|
191
|
+
return settleParallel(normalized.map((call)=>emitOne(bridge, call.name, call.data, {
|
|
192
|
+
...shared,
|
|
193
|
+
...call.options
|
|
194
|
+
}, defaultTimeout)));
|
|
195
|
+
}
|
|
196
|
+
const results = [];
|
|
197
|
+
for (const call of normalized){
|
|
198
|
+
results.push(await emitOne(bridge, call.name, call.data, {
|
|
199
|
+
...shared,
|
|
200
|
+
...call.options
|
|
201
|
+
}, defaultTimeout));
|
|
202
|
+
}
|
|
203
|
+
return results;
|
|
204
|
+
}
|
|
205
|
+
function emitOne(bridge, name, data, options = {}, defaultTimeout = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
206
|
+
if (isEventName(name)) {
|
|
207
|
+
sendReq(bridge, name, data ?? {});
|
|
208
|
+
return Promise.resolve(undefined);
|
|
209
|
+
}
|
|
210
|
+
const timeout = options.timeout ?? defaultTimeout;
|
|
211
|
+
const id = genId();
|
|
212
|
+
return new Promise((resolve, reject)=>{
|
|
213
|
+
let pending;
|
|
214
|
+
const offMessage = bridge.addEventListener('message', (e)=>{
|
|
215
|
+
const msg = e.data;
|
|
216
|
+
if (msg.kind !== 'res' || msg.id !== id) return;
|
|
217
|
+
clearTimeout(pending.timer);
|
|
218
|
+
pending.cleanupAbortListener?.();
|
|
219
|
+
offMessage();
|
|
220
|
+
if (msg.ret === RetCodes.OK) {
|
|
221
|
+
resolve(msg.data);
|
|
222
|
+
} else {
|
|
223
|
+
reject(new PlaycraftBridgeError(retToErrorCode(msg.ret ?? RetCodes.REQUEST_FAILED), msg.msg ?? 'request failed'));
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
pending = {
|
|
227
|
+
resolve: resolve,
|
|
228
|
+
reject,
|
|
229
|
+
timer: setTimeout(()=>{
|
|
230
|
+
offMessage();
|
|
231
|
+
reject(new PlaycraftBridgeError(BridgeErrorCodes.TIMEOUT, `[playcraft] emit "${name}" timeout after ${timeout}ms`));
|
|
232
|
+
}, timeout),
|
|
233
|
+
offMessage
|
|
234
|
+
};
|
|
235
|
+
if (options.signal) {
|
|
236
|
+
const onAbort = ()=>{
|
|
237
|
+
clearTimeout(pending.timer);
|
|
238
|
+
offMessage();
|
|
239
|
+
reject(new PlaycraftBridgeError(BridgeErrorCodes.ABORTED, `[playcraft] emit "${name}" aborted`));
|
|
240
|
+
};
|
|
241
|
+
if (options.signal.aborted) {
|
|
242
|
+
onAbort();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
options.signal.addEventListener('abort', onAbort, {
|
|
246
|
+
once: true
|
|
247
|
+
});
|
|
248
|
+
pending.cleanupAbortListener = ()=>options.signal?.removeEventListener('abort', onAbort);
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
bridge.postMessage(createEnvelope('req', id, name, data));
|
|
252
|
+
} catch (err) {
|
|
253
|
+
clearTimeout(pending.timer);
|
|
254
|
+
offMessage();
|
|
255
|
+
reject(err);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
function emitForBridge(bridge, nameOrCalls, dataOrOptions, options) {
|
|
260
|
+
if (typeof nameOrCalls === 'string') {
|
|
261
|
+
return emitOne(bridge, nameOrCalls, dataOrOptions, options);
|
|
262
|
+
}
|
|
263
|
+
const batchOptions = dataOrOptions ?? {};
|
|
264
|
+
if (isInvokeGroups(nameOrCalls)) {
|
|
265
|
+
const outerMode = batchOptions.mode ?? 'serial';
|
|
266
|
+
const runGroup = (group)=>runInvokeCalls(bridge, group.calls, group.mode ?? 'serial', batchOptions);
|
|
267
|
+
if (outerMode === 'parallel') {
|
|
268
|
+
return settleParallel(nameOrCalls.map(runGroup));
|
|
269
|
+
}
|
|
270
|
+
return (async ()=>{
|
|
271
|
+
const results = [];
|
|
272
|
+
for (const group of nameOrCalls){
|
|
273
|
+
results.push(await runGroup(group));
|
|
274
|
+
}
|
|
275
|
+
return results;
|
|
276
|
+
})();
|
|
277
|
+
}
|
|
278
|
+
return runInvokeCalls(bridge, nameOrCalls, batchOptions.mode ?? 'serial', batchOptions);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function safeReply(bridge, message) {
|
|
282
|
+
try {
|
|
283
|
+
bridge.postMessage(message);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
if (err instanceof PlaycraftBridgeError && err.code === BridgeErrorCodes.PEER_UNAVAILABLE) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function onForBridge(bridge, name, fn) {
|
|
292
|
+
return bridge.addEventListener('message', async (e)=>{
|
|
293
|
+
const msg = e.data;
|
|
294
|
+
if (msg.kind !== 'req' || msg.name !== name) return;
|
|
295
|
+
if (isEventName(name)) {
|
|
296
|
+
await fn(msg.data);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const data = await fn(msg.data, {
|
|
301
|
+
id: msg.id
|
|
302
|
+
});
|
|
303
|
+
safeReply(bridge, createSuccessResponse(msg.id, msg.name, data));
|
|
304
|
+
} catch (err) {
|
|
305
|
+
safeReply(bridge, createErrorResponse(msg.id, msg.name, toPlaycraftError(err)));
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
class Bridge {
|
|
311
|
+
constructor(options){
|
|
312
|
+
this.options = options;
|
|
313
|
+
this.listeners = new Set();
|
|
314
|
+
this.destroyed = false;
|
|
315
|
+
this.onWindowMessage = (e)=>{
|
|
316
|
+
const msgEvent = e;
|
|
317
|
+
if (this.destroyed || !this.options.allowedOrigins.includes(msgEvent.origin)) return;
|
|
318
|
+
if (!isPlaycraftEnvelope(msgEvent.data)) return;
|
|
319
|
+
this.log('recv', msgEvent.data);
|
|
320
|
+
const event = {
|
|
321
|
+
data: msgEvent.data,
|
|
322
|
+
origin: msgEvent.origin,
|
|
323
|
+
source: msgEvent.source
|
|
324
|
+
};
|
|
325
|
+
this.listeners.forEach((fn)=>fn(event));
|
|
326
|
+
};
|
|
327
|
+
this.targetOrigin = options.targetOrigin;
|
|
328
|
+
this.messageTarget = options.messageTarget ?? window;
|
|
329
|
+
this.messageTarget.addEventListener('message', this.onWindowMessage);
|
|
330
|
+
}
|
|
331
|
+
emit(nameOrCalls, dataOrOptions, options) {
|
|
332
|
+
return emitForBridge(this, nameOrCalls, dataOrOptions, options);
|
|
333
|
+
}
|
|
334
|
+
on(name, fn) {
|
|
335
|
+
return onForBridge(this, name, fn);
|
|
336
|
+
}
|
|
337
|
+
postMessage(message, targetOrigin = this.options.targetOrigin, transfer) {
|
|
338
|
+
this.assertAlive();
|
|
339
|
+
const peer = this.options.peer();
|
|
340
|
+
if (!peer) {
|
|
341
|
+
throw new PlaycraftBridgeError(BridgeErrorCodes.PEER_UNAVAILABLE, '[playcraft] peer window is not available');
|
|
342
|
+
}
|
|
343
|
+
this.log('send', message);
|
|
344
|
+
peer.postMessage(message, targetOrigin, transfer ?? []);
|
|
345
|
+
}
|
|
346
|
+
addEventListener(type, listener) {
|
|
347
|
+
this.assertAlive();
|
|
348
|
+
this.listeners.add(listener);
|
|
349
|
+
return ()=>this.listeners.delete(listener);
|
|
350
|
+
}
|
|
351
|
+
destroy() {
|
|
352
|
+
if (this.destroyed) return;
|
|
353
|
+
this.destroyed = true;
|
|
354
|
+
this.messageTarget.removeEventListener('message', this.onWindowMessage);
|
|
355
|
+
this.listeners.clear();
|
|
356
|
+
}
|
|
357
|
+
assertAlive() {
|
|
358
|
+
if (this.destroyed) {
|
|
359
|
+
throw new PlaycraftBridgeError(BridgeErrorCodes.DESTROYED, '[playcraft] bridge destroyed');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
log(event, msg) {
|
|
363
|
+
const { debug } = this.options;
|
|
364
|
+
if (!debug) return;
|
|
365
|
+
if (typeof debug === 'function') debug(event, msg);
|
|
366
|
+
else console.debug(`[playcraft:${event}]`, msg);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function parseTruthyDebug(value) {
|
|
371
|
+
if (value === null) return false;
|
|
372
|
+
if ([
|
|
373
|
+
'',
|
|
374
|
+
'1',
|
|
375
|
+
'true'
|
|
376
|
+
].includes(value)) return true;
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
/** True when `debug` query param is set (e.g. `?debug`, `?debug=1`, `?debug=true`). */ function isDebugFromUrl(search) {
|
|
380
|
+
const params = search instanceof URLSearchParams ? search : typeof search === 'string' ? new URLSearchParams(search.startsWith('?') ? search.slice(1) : search) : typeof globalThis !== 'undefined' && 'location' in globalThis ? new URLSearchParams(globalThis.location.search) : new URLSearchParams();
|
|
381
|
+
if (!params.has('debug')) return false;
|
|
382
|
+
return parseTruthyDebug(params.get('debug'));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function createReadyGate() {
|
|
386
|
+
let resolve;
|
|
387
|
+
const promise = new Promise((res)=>{
|
|
388
|
+
resolve = res;
|
|
389
|
+
});
|
|
390
|
+
return {
|
|
391
|
+
promise,
|
|
392
|
+
resolve
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Default one-way events used by the child-side handshake. */ const ChildBridgeDefaults = {
|
|
397
|
+
readyEvent: BridgeMethods.host.init,
|
|
398
|
+
announceEvent: BridgeMethods.child.ready
|
|
399
|
+
};
|
|
400
|
+
function normalizeOptions(options) {
|
|
401
|
+
return typeof options === 'string' ? {
|
|
402
|
+
parentOrigin: options
|
|
403
|
+
} : options;
|
|
404
|
+
}
|
|
405
|
+
function createChildBridge(parentOrigin, debug) {
|
|
406
|
+
return new Bridge({
|
|
407
|
+
peer: ()=>window.parent,
|
|
408
|
+
targetOrigin: parentOrigin,
|
|
409
|
+
allowedOrigins: [
|
|
410
|
+
parentOrigin
|
|
411
|
+
],
|
|
412
|
+
debug
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
function setupChildBridge(options) {
|
|
416
|
+
const { parentOrigin, readyEvent = ChildBridgeDefaults.readyEvent, announceEvent = ChildBridgeDefaults.announceEvent, debug = isDebugFromUrl() } = normalizeOptions(options);
|
|
417
|
+
const bridge = createChildBridge(parentOrigin, debug);
|
|
418
|
+
const ready = createReadyGate();
|
|
419
|
+
bridge.on(readyEvent, (evt)=>ready.resolve(evt));
|
|
420
|
+
if (window.parent !== window) {
|
|
421
|
+
void bridge.emit(announceEvent, {});
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
bridge,
|
|
425
|
+
whenParentReady: ()=>ready.promise
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Default one-way events used by the parent-side handshake. */ const ParentBridgeDefaults = {
|
|
430
|
+
announceEvent: BridgeMethods.child.ready,
|
|
431
|
+
readyEvent: BridgeMethods.host.init
|
|
432
|
+
};
|
|
433
|
+
function createParentBridge(iframe, childOrigin, debug) {
|
|
434
|
+
return new Bridge({
|
|
435
|
+
peer: ()=>iframe.contentWindow,
|
|
436
|
+
targetOrigin: childOrigin,
|
|
437
|
+
allowedOrigins: [
|
|
438
|
+
childOrigin
|
|
439
|
+
],
|
|
440
|
+
debug
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
function setupParentBridge(options) {
|
|
444
|
+
const { iframe, childOrigin, init = {}, announceEvent = ParentBridgeDefaults.announceEvent, readyEvent = ParentBridgeDefaults.readyEvent, debug = isDebugFromUrl() } = options;
|
|
445
|
+
const bridge = createParentBridge(iframe, childOrigin, debug);
|
|
446
|
+
const ready = createReadyGate();
|
|
447
|
+
bridge.on(announceEvent, (evt)=>{
|
|
448
|
+
void bridge.emit(readyEvent, init);
|
|
449
|
+
ready.resolve(evt);
|
|
450
|
+
});
|
|
451
|
+
return {
|
|
452
|
+
bridge,
|
|
453
|
+
whenChildReady: ()=>ready.promise
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/** Append `parentOrigin` query param for child iframe URLs. */ function withParentOrigin(childUrl, parentOrigin = typeof location !== 'undefined' ? location.origin : '') {
|
|
457
|
+
const base = typeof location !== 'undefined' ? location.href : 'http://localhost';
|
|
458
|
+
const url = new URL(childUrl, base);
|
|
459
|
+
url.searchParams.set('parentOrigin', parentOrigin);
|
|
460
|
+
return url;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export { BRIDGE_EVENT_NAMES, BRIDGE_RPC_NAMES, Bridge, BridgeErrorCodes, BridgeMethods, ChildBridgeDefaults, DEFAULT_REQUEST_TIMEOUT_MS, PROTOCOL_VERSION, ParentBridgeDefaults, PlaycraftBridgeError, RetCodes, bridgeMethod, createEnvelope, createErrorResponse, createSuccessResponse, emitOne, errorCodeToRet, isDebugFromUrl, isInvokeGroups, isPlaycraftEnvelope, normalizeInvokeCall, retToErrorCode, settleParallel, setupChildBridge, setupParentBridge, toPlaycraftError, withParentOrigin };
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@playcraft/iframe-bridge",
|
|
3
|
+
"version": "0.0.1-alpha.100",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "PlayCraft iframe postMessage bridge SDK for parent ↔ child communication",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "rm -rf dist && rollup -c",
|
|
25
|
+
"dev": "rollup -c -w",
|
|
26
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
27
|
+
"prepublishOnly": "pnpm build",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"playground": "pnpm --filter \"@playcraft/iframe-bridge-playground-*\" --parallel dev",
|
|
31
|
+
"lint": "echo 'No eslint config yet'",
|
|
32
|
+
"release": "node ../../scripts/release-npm.mjs --packages=iframe-bridge --skip-install --skip-tests --skip-git-push"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@rollup/plugin-json": "6.1.0",
|
|
36
|
+
"@rollup/plugin-node-resolve": "16.0.3",
|
|
37
|
+
"@rollup/plugin-swc": "0.4.0",
|
|
38
|
+
"@swc/core": "1.15.3",
|
|
39
|
+
"@types/node": "^25.2.0",
|
|
40
|
+
"happy-dom": "^20.0.10",
|
|
41
|
+
"rollup": "4.57.1",
|
|
42
|
+
"rollup-plugin-dts": "6.3.0",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"vite": "^8.0.3",
|
|
45
|
+
"vitest": "^3.2.4"
|
|
46
|
+
}
|
|
47
|
+
}
|