npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@playcraft/iframe-bridge

v0.0.6

Published

PlayCraft iframe postMessage bridge SDK for parent ↔ child communication

Readme

@playcraft/iframe-bridge

PlayCraft iframe 通讯协议 SDK — 基于 window.postMessage,适用于 平台 (Parent) ↔ iframe 内游戏/编辑器 (Child) 场景。

发送方 bridge.emit(name, data);接收方 bridge.on(name, fn)。需要返回结果时 callback 返回数据(SDK 自动发 res);仅通知、无需回包时 callback 处理完即可。是否等待 res 由 wire name 决定,API 形状相同。

版本说明

每条 envelope 的 __playcraft__ 字段等于发送方 npm 包 version。接收端不拒绝版本不一致的消息,只要 envelope 结构合法就会处理;版本不同时 SDK 会在控制台按对端版本各打印一次 soft warn,例如:

[playcraft] protocol version mismatch: local 0.0.3, peer 0.0.2. Messages are still processed; align @playcraft/iframe-bridge versions when possible.

建议:Parent 与 Child 尽量使用相同版本,避免 wire 协议或 payload 语义漂移(如 image.aiGenerate 返回格式变更)。出现功能异常时,先对比两侧 PROTOCOL_VERSION / package.json 并升级较低一侧。


安装

# pnpm
pnpm add @playcraft/iframe-bridge

# npm
npm install @playcraft/iframe-bridge

# yarn
yarn add @playcraft/iframe-bridge

Child 侧用法

Child 运行在 iframe 内,通过 bridge.emit 与 Parent 通讯。

1. 初始化 Bridge

推荐使用 setupChildBridge,它会创建 Bridge、注册 host.init 监听,并在嵌入模式下自动发送 child.ready

import { setupChildBridge, BridgeMethods } from '@playcraft/iframe-bridge';

// Parent 打开 iframe 时应在 URL 带上 ?parentOrigin=<parent-origin>
const parentOrigin = new URLSearchParams(location.search).get('parentOrigin') ?? location.origin;

const { bridge, whenParentReady } = setupChildBridge({
  parentOrigin,
  // readyEvent: BridgeMethods.host.init,      // 默认 — Parent 发来的就绪信号
  // announceEvent: BridgeMethods.child.ready, // 默认 — Child 发出的 bridge 就绪通知
  debug: true, // 或 (direction, envelope) => { ... } 记录 Timeline
});

// 等待 Parent 发送 host.init 后再调用其他 wire name
const init = await whenParentReady();
// init.params — Parent 可选传入的初始化参数

若不用 setupChildBridge,需自行 new Bridge({ peer: () => window.parent, ... }) 并手动处理握手。

2. 调用(emit)

// 批量读路径(req 仅为 path 字符串数组,不含文件内容)
const { paths } = await bridge.emit(BridgeMethods.form.get, ['scene.bg', 'scene.role']);
// paths[i].data — string

// 批量写路径
await bridge.emit(BridgeMethods.form.set, {
  paths: [{ path: 'scene.role', data: '...' }],
});

// AI 生图 — res 的 data 为 data URL,可直接用于 <img src>
const imageUrl = await bridge.emit(BridgeMethods.image.aiGenerate, {
  prompt: '赛博朋克风格的城市夜景',
  referenceImageBase64: ['data:image/png;base64,<base64>'],
});
// <img src={imageUrl} alt="" />

// 通知 Parent 关闭 iframe(无 res,emit 立即返回)
await bridge.emit(BridgeMethods.dialog.close, { reason: 'user_done' });

3. 批量调用与超时

bridge.emit 的数组形式用于编排多个独立调用(可混合不同 wire name、各自 payload)。
读多个 path 请用上文单次 form.get + path 数组(1 次往返);不要拆成多次 form.get

// 串行批量(默认)— 混合不同 wire name,按顺序执行
const [getRes, setRes, aiRes] = await bridge.emit([
  [BridgeMethods.form.get, ['scene.bg', 'scene.role']],
  [BridgeMethods.form.set, { paths: [{ path: 'scene.role', data: '...' }] }],
  [
    BridgeMethods.image.aiGenerate,
    { prompt: '赛博朋克城市', referenceImageBase64: ['data:image/png;base64,<base64>'] },
    { timeout: 60_000 }, // 可选:该 call 单独超时
  ],
]);

// 并行批量 — 互不依赖的 call 可同时发出
const parallel = await bridge.emit(
  [
    [BridgeMethods.form.get, ['scene.bg']],
    [BridgeMethods.form.get, ['scene.ui']],
  ],
  { mode: 'parallel' },
);
// parallel[0] 成功时为 { paths: [...] };失败项为 { error: PlaycraftBridgeError }

// 单 call 超时 / 取消
const controller = new AbortController();
await bridge.emit(BridgeMethods.image.aiGenerate, payload, {
  timeout: 60_000,
  signal: controller.signal,
});

4. 生命周期

// 页面卸载时销毁 Bridge,移除 message 监听
bridge.destroy();

Child 侧典型时序

Child                          Parent
  | child.ready (auto)    -->    |
  |                       <--    | host.init
  | form.get / set / ...  -->    | on → res
  | dialog.close          -->    | on(无 res)→ 关闭 iframe

Parent 侧用法

Parent 挂载 iframe、注册 bridge.on、响应握手与 dialog.close。DOM 由业务方管理;Bridge 与握手推荐用 setupParentBridge

1. 初始化 Bridge

import { setupParentBridge, buildChildIframeUrl, BridgeMethods } from '@playcraft/iframe-bridge';

const iframe = document.createElement('iframe');
iframe.src = buildChildIframeUrl(childUrl, { lang: 'zh' }).toString();
container.appendChild(iframe);

const { bridge, whenChildReady } = setupParentBridge({
  iframe,
  childOrigin: new URL(childUrl).origin,
  init: { params: { theme: 'dark' } }, // 收到 child.ready 后自动 emit host.init
  autoDetectChildReady: true, // iframe load / document complete 也视为 ready
  debug: true,
});

await whenChildReady();

buildChildIframeUrl 负责构建 Child iframe 的 src URL:固定追加 parentOrigin,其余键值对作为 query param(如 langdebug);iframe 样式、挂载时机仍由业务控制。

若不用 setupParentBridge,需自行 new Bridge({ peer: () => iframe.contentWindow, ... }) 并手动处理握手。

2. 注册 on

bridge.on(BridgeMethods.form.get, async (pathKeys) => ({
  paths: await Promise.all(
    pathKeys.map(async (path) => ({ path, data: await loadPathData(path) })),
  ),
}));

bridge.on(BridgeMethods.form.set, async ({ paths }) => {
  for (const entry of paths) {
    await savePathData(entry.path, entry.data);
  }
  return { paths: paths.map(({ path }) => path) };
});

// 无 res — callback 处理完即可,不 return 结果
bridge.on(BridgeMethods.dialog.close, () => {
  iframe.remove();
  bridge.destroy();
});

完整字段与错误约定见下文 API 参考

3. 生命周期

bridge.destroy();

Parent 侧典型时序

Child                          Parent
  | child.ready (auto)    -->    | whenChildReady() resolve
  |                       <--    | host.init (auto emit)
  | form.get / set / ...  -->    | on → res
  | dialog.close          -->    | on(无 res)→ 关闭 iframe

Parent 侧职责小结

| 职责 | API | | ---------------- | -------------------------------------- | | iframe DOM / URL | 业务方(可用 buildChildIframeUrl) | | 握手 | setupParentBridge + whenChildReady | | 业务逻辑 | bridge.on(有/无 res 均可) | | 销毁 | bridge.destroy() |


调试

URL 带 ?debug?debug=1 时,setupChildBridge / setupParentBridge 会开启 envelope 日志。默认输出包含 Parent / Child 标识发送方向Child → Parent / Parent → Child),并用不同颜色区分两端(Parent 靛蓝、Child 琥珀色):

[Parent] Child → Parent · recv · req · child.ready
[Child]  Parent → Child · recv · req · host.init

也可传入自定义 DebugLogger 接管日志(如 Playground 的 Bridge Timeline):

import { isDebugFromUrl } from '@playcraft/iframe-bridge';

setupChildBridge({
  parentOrigin,
  debug: isDebugFromUrl() ? (dir, env) => console.log(dir, env) : false,
});

协议概览

| 概念 | 说明 | | ------------------- | ---------------------------------------------------------------------------------- | | Envelope | { __playcraft__, kind, id, name, data?, ret?, msg? } — 所有消息的统一信封 | | __playcraft__ | 发送方协议版本,与 npm 包 version 一致;不一致时控制台 soft warn,消息仍会被处理 | | req | 请求 envelope;接收方 bridge.on 处理 | | res | 响应 envelope(可选)。ret === 0data 为结果;失败时 msg 描述原因 | | PathEntry | { path, data }data 恒为 string |

出站bridge.emit(name, data) — 需要 res 的 wire name 会 await;否则立即返回。

入站bridge.on(name, fn) — callback 返回数据则发 res;无返回则不回包。callback 支持 async / Promise

Wire 名称统一为 ${domain}.${action} 格式,在 BridgeMethods 中维护。


API 参考

SDK 导出

| 导出 | 说明 | | ------------------------------------------------ | --------------------------------------- | | Bridgeemit / on) | postMessage 封装 + 出站/入站 API | | setupChildBridge | Child 侧一键初始化 + 握手 | | setupParentBridge / buildChildIframeUrl | Parent 侧绑定 iframe + 握手 | | emitOne / normalizeInvokeCall 等 | 批量 emit 辅助(高级) | | PlaycraftBridgeError / BridgeErrorCodes | 结构化错误 | | PROTOCOL_VERSION | 当前协议版本字符串 | | BridgeMethods / bridgeMethod | 全部 wire 名称(${domain}.${action}) | | BRIDGE_RPC_NAMES | 需要 res 的 wire 名称列表 | | RetCodes / errorCodeToRet / retToErrorCode | Envelope ret 与错误码映射 | | isPlaycraftEnvelope | 校验未知 postMessage 是否为本协议 |

Wire 名称在 BridgeMethods 中维护:

import { BridgeMethods, bridgeMethod } from '@playcraft/iframe-bridge';

BridgeMethods.form.get; // 'form.get' — emit 等待 res
BridgeMethods.dialog.close; // 'dialog.close' — emit 立即返回
bridgeMethod('form', 'get'); // 'form.get'

修改 wire 名称时只需改 BridgeMethodsbridgeMethod('domain', 'action'))一处;MethodMapEventMapInvokeMap 等均从此派生。


form.get

批量读取路径内容。

方向:Child → Parent(Child emit,Parent on

Request string[] — path 列表,不含 data

['scene.bg', 'scene.role'];

Response { paths: PathEntry[] }

{
  paths: [
    { path: 'scene.bg', data: '...' },
    { path: 'scene.role', data: '...' },
  ];
}

form.set

批量写入路径。

方向:Child → Parent(Child emit,Parent on

Request { paths: PathEntry[] }

{
  paths: [{ path: 'scene.role', data: '...' }],
}

Responseret === 0 时){ paths: string[] } — 已成功写入的 path 列表

{
  paths: ['scene.role'],
}

任一路径写入失败时 Parent handler 应 throw PlaycraftBridgeError,Child 侧 emit 收到 ret !== 0msg


image.aiGenerate

Request AiGenerateImagePayload

| 字段 | 类型 | 必填 | 说明 | | ---------------------- | ---------- | ---- | -------------------------------------------------------------------------------------------------------- | | prompt | string | | 生成提示词 | | referenceImageBase64 | string[] | | 参考图列表;支持 data:<mime>;base64,... 或纯 base64(无前缀) | | path | string | | theme.data 字段路径(如 gamePlay.mainScene.tableTop);提供时 Parent 将图片写入 assets/ 并更新该路径 | | aspectRatio | string | | 宽高比(如 16:9);未传时 Parent 从 schema xplatform.ext.ratio 读取 | | width | number | | 输出宽度(px);未传时 Parent 从 schema resolution / 默认值解析 | | height | number | | 输出高度(px);未传时 Parent 从 schema resolution / 默认值解析 | | fitMode | string | | contain | cover | fill;未传时 Parent 从 schema / 资产路径推断 | | format | string | | png | webp | jpeg;未传时 Parent 从 schema mime_type 解析 | | removeBackground | boolean | | 生成后是否抠图;未传时 Parent 从 schema ext.remove_background 读取 | | paddingColor | string | | contain 留边色(如 transparent#RRGGBB) | | imageModelRef | string | | 指定生图模型 ref | | teamId | string | | 素材库 team;未传时 Parent 使用当前活跃 team |

promptreferenceImageBase64 至少提供一个。其余字段均为可选;未传时 Parent 侧与 Remix AI 生图对话框一致,从 schema xplatform 协议推导默认值后再调用 generate-image-preview

Response string — 生成图片的 data URLdata:<mime>;base64,...

Envelope 的 data 字段即为 data URL 字符串,Child 可直接用于 <img src>

const imageUrl = await bridge.emit(BridgeMethods.image.aiGenerate, { prompt: '...' });
// <img src={imageUrl} alt="" />
{
  "kind": "res",
  "name": "image.aiGenerate",
  "ret": 0,
  "data": "data:image/png;base64,iVBORw0KGgo..."
}

form.get / form.set 的 PathEntry.data 仍为纯 base64(无 data: 前缀);仅 image.aiGenerate 的响应使用 data URL。


child.ready

方向:Child → Parent · res:无
Payload{}

setupChildBridge 在嵌入模式下自动 emit。Parent 侧 setupParentBridge 收到后自动 emit host.init


host.init

方向:Parent → Child · res:无
Payload{ params?: Record<string, unknown> }

Child 侧 whenParentReady() / Parent 侧 whenChildReady() 在握手完成后 resolve。

若对端未集成 SDK,默认在 DEFAULT_HANDSHAKE_TIMEOUT_MS(10s)后仍 resolve 空 payload,并在控制台 console.warn 提示;可通过 handshakeTimeout 调整等待时间。


dialog.close

方向:Child → Parent · res:无
Payload{ reason?: string }

Parent 用 bridge.on 注册(无 return),通常在此移除 iframe 并 bridge.destroy()


错误码

res 的调用失败时 ret !== 0msg 为错误描述;retBridgeErrorCodes 的映射见 RetCodes

| ret | Code | 含义 | | --- | ------------------ | ------------------ | | 0 | — | 成功 | | 1 | INVALID_PARAM | 参数不合法 | | 2 | TIMEOUT | 等待响应超时 | | 3 | ABORTED | 调用方 abort | | 4 | REQUEST_FAILED | 通用失败 | | 5 | INTERNAL_ERROR | Handler 内部错误 | | 6 | PEER_UNAVAILABLE | 对端 window 不可用 | | 7 | DESTROYED | Bridge 已销毁 | | 8 | METHOD_NOT_FOUND | 方法未注册 |

Client 侧 bridge.emit 对有 res 的 wire name 失败时会 rejectPlaycraftBridgeError(由 ret 反查 code)。


Envelope 示例

请求(有 res)

{
  "__playcraft__": "0.0.1",
  "kind": "req",
  "id": "1702819473736-0",
  "name": "form.get",
  "data": ["scene.bg", "scene.role"]
}

成功响应

{
  "__playcraft__": "0.0.1",
  "kind": "res",
  "id": "1702819473736-0",
  "name": "form.get",
  "ret": 0,
  "data": { "paths": [{ "path": "scene.bg", "data": "..." }] }
}

失败响应

{
  "__playcraft__": "0.0.1",
  "kind": "res",
  "id": "1702819473736-0",
  "name": "form.set",
  "ret": 1,
  "msg": "Invalid path: scene.unknown"
}