in-page-bom
v0.1.0
Published
Page-local BOM: whitelist window ops (reload/back) and read-only viewport/href inspect.
Readme
in-page-bom
页内 BOM 组件:白名单窗口操作 + 窗口级只读感知。
In-page BOM library: allowlisted window operations plus read-only viewport / URL inspect.
绑定一个 window,对其做白名单 BOM 操作,并现读视口与地址。
Binds one window for allowlisted BOM operations and live viewport / URL inspect.
| 中文 | English | |------|---------| | 1. 组件介绍 | 1. Introduction | | 2. 安装 | 2. Install | | 3. 怎么用 | 3. Quick start | | 4. 用法与参数 | 4. API and parameters | | 5. 回包、错误码与效果 | 5. Results, errors, and effects | | 6. 生命周期 | 6. Lifecycle | | 7. 本地开发 | 7. Local development |
中文
1. 组件介绍
in-page-bom 是浏览器端 npm 库。调用方传入(或默认)一个 window,库对这个浏览上下文提供:
- 只读感知
inspect():每次现读视口宽高、devicePixelRatio、完整location.href。 - 白名单操作
run():reload(location.reload())和back(history.back())。
典型用途:编排层需要刷新当前浏览上下文,或读取当前 iframe / 窗口的尺寸和 URL。reload 表示已对绑定窗口发起重载;下一份文档里有什么,由页面自己决定。命令在白名单且窗口有效时即执行。
| 项目 | 说明 |
|------|------|
| 运行环境 | 浏览器(或 iframe 的 contentWindow) |
| 模块形态 | ESM + .d.ts,零运行时依赖 |
| 公开入口 | createBom → { inspect, run } |
| 副作用 | location.reload / history.back |
| 失败 | 稳定 error.code |
2. 安装
需要能解析 ESM 的宿主,以及浏览器 Window 类型(TypeScript 请启用 DOM lib)。Node 版本 ≥ 18 仅约束本仓库的构建/测试工具链。
发布后(包名 in-page-bom):
npm install in-page-bom
# 或 pnpm add in-page-bom / yarn add in-page-bom本仓库尚未 npm publish 时,用本地路径:
cd in-page-bom
npm install
npm run build
# 在宿主项目
npm install file:../in-page-bom安装产物是 dist/(exports 指向 dist/index.js 与 dist/index.d.ts)。从 git 源码引用前先 npm run build。零运行时依赖。
3. 怎么用
最小闭环:创建实例 → 现读窗口 → 必要时 reload(成功后实例退役)。
import { createBom } from "in-page-bom";
const bom = createBom({ window });
const seen = bom.inspect();
if (!seen.ok) {
// INSTANCE_RETIRED / WINDOW_GONE
console.error(seen.error.code, seen.error.message);
} else {
// 完整 href,不要把 query 里的 secret 打进日志
const { width, height, dpr } = seen.inspect.viewport;
const href = seen.inspect.href;
}
const step = await bom.run({ type: "reload" });
if (step.ok && step.retired) {
// 不要再对同一个 bom.inspect() / run() 走成功路径
// 页面重载后重新 createBom(通常伴随文档换代)
}嵌入式 App(外壳 + iframe)传入 iframe 的 contentWindow:
const iframe = document.querySelector("iframe");
if (iframe?.contentWindow) {
const bom = createBom({ window: iframe.contentWindow });
await bom.run({ type: "reload" }); // 刷新该 iframe
}省略 window 时绑定 globalThis(浏览器里即当前页 window):
const bom = createBom();4. 用法与参数
4.1 createBom(options?)
function createBom(options?: ICreateBomOptions): IBom;| 参数 | 类型 | 必填 | 默认 | 含义 |
|------|------|------|------|------|
| options | ICreateBomOptions | 否 | {} | 创建选项 |
| options.window | Window | 否 | globalThis | 绑定的浏览上下文。所有 inspect / run 只作用于它 |
返回 IBom:
| 方法 | 同步性 | 作用 |
|------|--------|------|
| inspect() | 同步 | 现读 viewport + href |
| run(command) | Promise | 执行白名单命令;reload 发起后即 resolve,不等新文档 ready |
一个 createBom 对应一个 Window、一份 retired 标志。换窗口或换代必须再调一次 createBom。
4.2 inspect()
每次从绑定 window 现读。V1 字段来自 innerWidth / innerHeight / devicePixelRatio。
成功:
{
ok: true,
inspect: {
viewport: { width: 1280, height: 720, dpr: 2 },
href: "https://example.test/app?token=secret"
}
}| 字段 | 类型 | 来源 / 约束 |
|------|------|-------------|
| ok | true | 读到了窗口 |
| inspect.viewport.width | number | window.innerWidth |
| inspect.viewport.height | number | window.innerHeight |
| inspect.viewport.dpr | number | window.devicePixelRatio |
| inspect.href | string | location.href 真值;脱敏由编排做 |
失败(判别联合,TypeScript 可收窄):
{ ok: false, error: { code: "INSTANCE_RETIRED" | "WINDOW_GONE", message: string } }效果: 只读。两次 inspect 之间若视口或地址变了,第二次跟新值。
4.3 run(command)
type TBomCommand = { type: "reload" } | { type: "back" };
run(command: TBomCommand): Promise<IBomStepResult>| command.type | 浏览器动作 | 何时用 |
|----------------|------------|--------|
| "reload" | location.reload() | 刷新绑定窗口的当前文档 |
| "back" | history.back() | 历史可退时后退一帧 |
运行时未知 type 会得到 INVALID_COMMAND。
reload
const step = await bom.run({ type: "reload" });| 步骤 | 行为 |
|------|------|
| 校验 | 未 retired、窗口仍可读 |
| 记录 | inspectBefore = 执行前的窗口摘要 |
| 调用 | location.reload() |
| 回包 | 立刻 ok: true, retired: true,不等 新文档 load |
reload() 抛错:ok: false,retired: false,码 DISPATCH_FAILED,实例仍可继续 inspect。
back
const step = await bom.run({ type: "back" });| 条件 | 效果 |
|------|------|
| history.length <= 1 | NOT_ALLOWED,history.back() 未发出 |
| 可退且文档仍在(如同文档 pushState) | ok: true, retired: false |
| 可退且调用后窗口已不可读 | ok: true, retired: true |
| history.back() 抛错 | DISPATCH_FAILED,不退役 |
同文档 hash / pushState 的 href 变化仍视为同一代。父页绑 iframe、跨文档后退时,本步回包可能仍是 retired: false;文档 pagehide(非 bfcache)后实例退役,之后 inspect 为 INSTANCE_RETIRED。
先造可退历史再 back(playground / iframe 内):
iframe.contentWindow.history.pushState({}, "", "?step=2");
await bom.run({ type: "back" });4.4 典型组合
只读当前 iframe 尺寸和地址:
const bom = createBom({ window: iframe.contentWindow! });
const seen = bom.inspect();
if (seen.ok) {
layoutTo(seen.inspect.viewport);
}编排侧刷新:
const step = await bom.run({ type: "reload" });
if (!step.ok) {
// INVALID_COMMAND / WINDOW_GONE / INSTANCE_RETIRED / DISPATCH_FAILED
return;
}
// reload 成功后重新 createBom退役后换新实例:
await bom.run({ type: "reload" });
const next = createBom({ window: iframe.contentWindow! });
next.inspect(); // 新一代5. 回包、错误码与效果
IBomStepResult
| 字段 | 类型 | 含义 |
|------|------|------|
| ok | boolean | 本步是否完成合同副作用。校验失败必须是 false |
| command | "reload" \| "back" \| string | 实际看到的 type;未知命令保留原字符串 |
| retired | boolean | 本步是否把实例标为退役。reload 一旦调用成功必须为 true;校验失败(未调用)必须为 false |
| inspectBefore | IBomInspect \| null | 执行前摘要;窗口已不可读或校验过早失败时为 null |
| error | IBomError(可选) | ok: false 时带稳定 code |
注意:ok 不是判别联合。失败时用 step.error?.code 分支。后续 run 若已退役,回包里的 retired 仍可能是 false(表示这一步没有再调用 reload);实例已死看 INSTANCE_RETIRED。
ok / retired 对照
| 情况 | ok | retired | 副作用 |
|------|------|-----------|--------|
| reload 已调用 | true | true | 已 location.reload() |
| reload 未调用(校验失败) | false | false | 无 |
| back 成功且文档仍在 | true | false | 已 history.back() |
| back 成功且当时已能判断将卸载 | true | true | 已 history.back() |
| 未知命令 | false | false | 无 |
| history.length <= 1 | false | false | 无 |
错误码 TBomErrorCode
| code | 出现在 | 含义 | 副作用 |
|--------|--------|------|--------|
| INVALID_COMMAND | run | 未知 type | 无 |
| WINDOW_GONE | inspect / run | 窗口已 closed 或读 location / 尺寸抛错 | 无 |
| INSTANCE_RETIRED | inspect / run | 本实例已因 reload(或卸载 / pagehide)退役 | 无 |
| NOT_ALLOWED | run({ type: "back" }) | history.length <= 1 | 无 |
| DISPATCH_FAILED | run | 白名单 API 抛错,页面仍在 | 实例仍可用 |
error.message 给人看,不要当协议字段解析。
示例回包
reload 成功:
{
"ok": true,
"command": "reload",
"retired": true,
"inspectBefore": {
"viewport": { "width": 1280, "height": 720, "dpr": 2 },
"href": "https://example.test/app"
}
}无处可退:
{
"ok": false,
"command": "back",
"retired": false,
"inspectBefore": { "viewport": { "width": 1280, "height": 720, "dpr": 2 }, "href": "https://example.test/app" },
"error": { "code": "NOT_ALLOWED", "message": "history.back is not allowed because history.length <= 1." }
}6. 生命周期
createBom({ window })
→ 活着:inspect / run(reload|back)
→ run(reload) 成功:立刻 retired
→ 绑定文档 pagehide(非 bfcache):retired
→ 窗口 closed / 不可读:WINDOW_GONE(若尚未 retired)
→ 已 retired:inspect / run → INSTANCE_RETIRED
→ 新文档 / 新会话:再次 createBom| 场景 | 旧实例 |
|------|--------|
| reload 已调用 | 立即退役,不要等 load |
| 同文档 back | 通常仍活着 |
| 跨文档 back(父页绑 iframe) | 本步可能仍 retired: false;pagehide 后退役 |
| 用户在 iframe 里手动刷新 | pagehide 后退役 |
| bfcache(pagehide.persisted) | 不退役,同一文档可能被唤回 |
成功 reload 之后,同一实例按退役处理;新文档上再 createBom。
7. 本地开发
npm install
npm run typecheck
npm run lint
npm test
npm run build
npm run playgroundPlayground:父页绑定 iframe。Inspect → Reload → 再 Inspect 应为 INSTANCE_RETIRED;「重新 createBom」后才能读新文档。Back 前可先「Push iframe history」。
English
1. Introduction
in-page-bom is a browser npm library. You bind one window (or take the default). On that browsing context it provides:
- Read-only inspect via
inspect(): live viewport size,devicePixelRatio, and the fulllocation.href. - Allowlisted ops via
run():reload(location.reload()) andback(history.back()).
Typical use: refresh the current browsing context, or read iframe/window size and URL. reload means a reload was issued on the bound window; what the next document contains is up to the page. Allowlisted commands run while the window is valid.
| Item | Detail |
|------|--------|
| Runtime | Browser (or an iframe contentWindow) |
| Package | ESM + .d.ts, zero runtime dependencies |
| Public surface | createBom → { inspect, run } |
| Side effects | location.reload / history.back |
| Failures | Stable error.code |
2. Install
Needs an ESM host and the DOM Window type (enable the TypeScript DOM lib). engines.node >= 18 applies to this repo’s toolchain, not to the browser runtime.
When published (in-page-bom):
npm install in-page-bom
# or pnpm add in-page-bom / yarn add in-page-bomBefore publish, use a local path:
cd in-page-bom
npm install
npm run build
# in the host app
npm install file:../in-page-bomThe published files are dist/ (exports → dist/index.js and dist/index.d.ts). If you consume git sources, run npm run build first. Zero runtime dependencies.
3. Quick start
Create → inspect → reload when needed (the instance retires on successful reload).
import { createBom } from "in-page-bom";
const bom = createBom({ window });
const seen = bom.inspect();
if (!seen.ok) {
// INSTANCE_RETIRED / WINDOW_GONE
console.error(seen.error.code, seen.error.message);
} else {
// Full href — do not log query secrets
const { width, height, dpr } = seen.inspect.viewport;
const href = seen.inspect.href;
}
const step = await bom.run({ type: "reload" });
if (step.ok && step.retired) {
// Do not keep calling inspect/run on this instance as a live session
// createBom again after reload (usually a new document)
}Embedded apps (chrome + iframe) pass the iframe contentWindow:
const iframe = document.querySelector("iframe");
if (iframe?.contentWindow) {
const bom = createBom({ window: iframe.contentWindow });
await bom.run({ type: "reload" }); // reloads that iframe
}Omit window to bind globalThis (the page window in a browser):
const bom = createBom();4. API and parameters
4.1 createBom(options?)
function createBom(options?: ICreateBomOptions): IBom;| Parameter | Type | Required | Default | Meaning |
|-----------|------|----------|---------|---------|
| options | ICreateBomOptions | no | {} | Create options |
| options.window | Window | no | globalThis | Bound browsing context. Every inspect / run uses this object only |
IBom methods:
| Method | Sync | Role |
|--------|------|------|
| inspect() | sync | Live viewport + href |
| run(command) | Promise | Allowlisted command. reload resolves as soon as it is issued, without waiting for the next document |
One createBom call binds one Window and one retired flag. A new window or generation needs a new createBom.
4.2 inspect()
Reads the bound window every time. V1 fields come from innerWidth / innerHeight / devicePixelRatio.
Success:
{
ok: true,
inspect: {
viewport: { width: 1280, height: 720, dpr: 2 },
href: "https://example.test/app?token=secret"
}
}| Field | Type | Source / constraint |
|-------|------|---------------------|
| ok | true | Window was readable |
| inspect.viewport.width | number | window.innerWidth |
| inspect.viewport.height | number | window.innerHeight |
| inspect.viewport.dpr | number | window.devicePixelRatio |
| inspect.href | string | Live location.href; redaction is the caller’s job |
Failure (discriminated union):
{ ok: false, error: { code: "INSTANCE_RETIRED" | "WINDOW_GONE", message: string } }Effect: read-only. If size or href changes between two calls, the second call follows the new values.
4.3 run(command)
type TBomCommand = { type: "reload" } | { type: "back" };
run(command: TBomCommand): Promise<IBomStepResult>| command.type | Browser action | When |
|----------------|----------------|------|
| "reload" | location.reload() | Reload the bound document |
| "back" | history.back() | Go back one history entry when allowed |
Unknown runtime type values return INVALID_COMMAND.
reload
const step = await bom.run({ type: "reload" });| Step | Behavior |
|------|----------|
| Guard | Not retired; window still readable |
| Snapshot | inspectBefore = pre-op inspect |
| Call | location.reload() |
| Result | Immediately ok: true, retired: true — does not wait for load |
If reload() throws: ok: false, retired: false, DISPATCH_FAILED; the instance remains usable for inspect.
back
const step = await bom.run({ type: "back" });| Condition | Effect |
|-----------|--------|
| history.length <= 1 | NOT_ALLOWED; history.back() is not issued |
| Can go back; document still here (e.g. same-document pushState) | ok: true, retired: false |
| Can go back; window already unreadable after the call | ok: true, retired: true |
| history.back() throws | DISPATCH_FAILED; not retired |
Same-document hash / pushState href changes stay the same generation. For a parent-bound iframe, a cross-document back may still report retired: false on that step; after pagehide (not bfcache) the instance retires and later inspect returns INSTANCE_RETIRED.
Seed history, then back (playground / iframe):
iframe.contentWindow.history.pushState({}, "", "?step=2");
await bom.run({ type: "back" });4.4 Common recipes
Read iframe size and URL only:
const bom = createBom({ window: iframe.contentWindow! });
const seen = bom.inspect();
if (seen.ok) {
layoutTo(seen.inspect.viewport);
}Orchestrated refresh:
const step = await bom.run({ type: "reload" });
if (!step.ok) {
// INVALID_COMMAND / WINDOW_GONE / INSTANCE_RETIRED / DISPATCH_FAILED
return;
}
// After a successful reload, createBom againNew instance after retire:
await bom.run({ type: "reload" });
const next = createBom({ window: iframe.contentWindow! });
next.inspect(); // next generation5. Results, errors, and effects
IBomStepResult
| Field | Type | Meaning |
|-------|------|---------|
| ok | boolean | Whether this step completed the contracted side effect. Validation failures must be false |
| command | "reload" \| "back" \| string | Observed type; unknown commands keep the original string |
| retired | boolean | Whether this step retired the instance. Successful reload must be true; validation failure (not called) must be false |
| inspectBefore | IBomInspect \| null | Pre-op snapshot; null if the window was already unreadable |
| error | IBomError (optional) | Present on ok: false with a stable code |
ok is not a discriminated union—branch on step.error?.code. A later run on a retired instance may still report retired: false (this step did not call reload again); liveness is INSTANCE_RETIRED.
ok / retired
| Case | ok | retired | Side effect |
|------|------|-----------|-------------|
| reload invoked | true | true | location.reload() |
| reload not invoked (guard failed) | false | false | none |
| back succeeded; document still here | true | false | history.back() |
| back succeeded; unload already detectable | true | true | history.back() |
| Unknown command | false | false | none |
| history.length <= 1 | false | false | none |
Error codes
| code | Where | Meaning | Side effect |
|--------|-------|---------|-------------|
| INVALID_COMMAND | run | Unknown type | none |
| WINDOW_GONE | inspect / run | closed or reads throw | none |
| INSTANCE_RETIRED | inspect / run | Retired after reload / unload / pagehide | none |
| NOT_ALLOWED | run({ type: "back" }) | history.length <= 1 | none |
| DISPATCH_FAILED | run | Allowlisted API threw; page still here | instance still usable |
Treat error.message as human text, not a protocol field.
Example payloads
Successful reload:
{
"ok": true,
"command": "reload",
"retired": true,
"inspectBefore": {
"viewport": { "width": 1280, "height": 720, "dpr": 2 },
"href": "https://example.test/app"
}
}Nothing to go back to:
{
"ok": false,
"command": "back",
"retired": false,
"inspectBefore": { "viewport": { "width": 1280, "height": 720, "dpr": 2 }, "href": "https://example.test/app" },
"error": { "code": "NOT_ALLOWED", "message": "history.back is not allowed because history.length <= 1." }
}6. Lifecycle
createBom({ window })
→ live: inspect / run(reload|back)
→ successful run(reload): retired immediately
→ bound document pagehide (not bfcache): retired
→ window closed / unreadable: WINDOW_GONE (if not already retired)
→ retired: inspect / run → INSTANCE_RETIRED
→ new document / session: createBom again| Scenario | Old instance |
|----------|----------------|
| reload invoked | Retired immediately; do not wait for load |
| Same-document back | Usually still live |
| Cross-document back (parent holds iframe) | Step may still be retired: false; retires after pagehide |
| User refreshes the iframe | Retires after pagehide |
| bfcache (pagehide.persisted) | Not retired; the same document may return |
After a successful reload, treat this instance as retired; call createBom again on the new document.
7. Local development
npm install
npm run typecheck
npm run lint
npm test
npm run build
npm run playgroundPlayground: parent page binds the iframe. Inspect → Reload → Inspect again should be INSTANCE_RETIRED. Use “重新 createBom” for a new generation. Push iframe history before Back.
文档 / Docs
| 文档 | 用途 |
|------|------|
| docs/README.md | 文档中心 / doc hub |
| docs/AI与人类/需求拟定/ | 需求基线 / requirements (in progress) |
| docs/AI与人类/规范和原则/ | 失败方向 / fail-closed principles |
| AGENTS.md | Agent 实施入口 |
状态 / Status: P0–P2 已落地(createBom / inspect / reload / back、契约测试、ESM 骨架)。尚未 npm publish。
