dsh-mini-utility-dock 0.5.0 → 0.6.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/CHANGELOG.en.md +24 -9
- package/CHANGELOG.md +24 -9
- package/README.en.md +22 -6
- package/README.md +22 -6
- package/bin/dsh-mini-utility-dock.js +74 -27
- package/bin/dsh-plugin-docs.js +54 -20
- package/bin/dsh-plugin-parity.js +105 -60
- package/dist/guard.js +18 -21
- package/dist/host-http.js +207 -0
- package/dist/launcher.js +47 -14
- package/dist/loopback.js +11 -14
- package/package.json +7 -2
package/CHANGELOG.en.md
CHANGED
|
@@ -3,14 +3,32 @@
|
|
|
3
3
|
Release notes are generated from the matching version section; newest first.
|
|
4
4
|
For Chinese, see [CHANGELOG.md](CHANGELOG.md).
|
|
5
5
|
|
|
6
|
+
## 0.6.0 - 2026-09-28
|
|
7
|
+
|
|
8
|
+
- New `dist/host-http.js` (marker `dsh-host-http`): the host half's reply glue becomes one block — `sendJson` (always `no-store`), the POST gate, the browser authorizer, `optionalSessionId` — so a reply policy has one definition. An already published vocabulary (`need_post`, Chinese wording, the echoed `action`) is a `policy` override, so no public identifier changes.
|
|
9
|
+
- `createBrowserAuthorizer` takes accessors, not values: a service reload reassigns a host half's Connection to `null` and later to a new instance, and the value-shaped version would keep handing requests to a destroyed Connection.
|
|
10
|
+
- The comments that ride verbatim into a consumer's repository with the launcher and host-http fragments now state the facts: no sentence misdescribes what the other copies do, and host-http's analogy to a guard-block name says where that name comes from.
|
|
11
|
+
- `dsh-mini-utility-dock`'s `sync`/`check` reject a file whose marker ranges overlap (`<A> <B> </A> </B>`) and leave it unwritten, closing off the corruption the bottom-up splice could previously produce.
|
|
12
|
+
- `dsh-plugin-parity` compares the `dsh-host-http` block byte for byte, and its block-position assertion now covers all three host blocks.
|
|
13
|
+
- The parity bin's "private implementation outside the blocks" scan matches code shapes only: both the bracketed and the destructured Fetch Metadata read count, `LOOPBACK_HOSTNAMES` is caught on assignment but not comparison, and a composite heuristic that could false-FAIL a reasonable repository is gone — so a comment that explains why a helper is not a second guard no longer reads as drift.
|
|
14
|
+
- The parity bin's block comparison strips each block's common indentation, so a consumer that indents its markers by 4 spaces or a tab is no longer reported as drift; `--fleet` naming a repository that is not a member now fails with a non-zero exit instead of the "skipped" line reading as a pass.
|
|
15
|
+
- `dsh-plugin-docs --base` no longer dies inside `git diff` on an all-zero SHA — which is what `github.event.before` carries for a new or force-pushed branch; it notes that there is no previous state and still completes the structural checks.
|
|
16
|
+
- `dsh-plugin-docs` now compares the `Unreleased` section for version, section and item structure — it is the most-edited section each round and was previously outside the check.
|
|
17
|
+
- Tests filled in: process-level behavior for the two diagnostic bins, decision-level behavior for `dist/loopback.js`, `dist/guard.js` and the new fragment, and one `FRAGMENTS` integrity assertion, with a paired case for each behavior change above.
|
|
18
|
+
- Maintenance: CI installs with `npm ci --ignore-scripts` and caches npm, both workflows run `docs:check`, syntax checks cover `bin` and `dist`, `publish.yml` gates on the tag being an ancestor of `main`, and `homepage`, `bugs` and `author` are filled in. Being dependency-free, this package now tracks `package-lock.json` so `npm ci` and the cache have one (reason in `.gitignore`).
|
|
19
|
+
|
|
20
|
+
## 0.5.1 - 2026-09-25
|
|
21
|
+
|
|
22
|
+
- Fix the launcher disappearing after a hot reload: the claim is released with its owner, which wakes the other copies to register again instead of requiring a full page reload.
|
|
23
|
+
|
|
6
24
|
## 0.5.0 - 2026-09-24
|
|
7
25
|
|
|
8
|
-
- New `dist/launcher.js` (marker `dsh-utility-launcher`):
|
|
26
|
+
- New `dist/launcher.js` (marker `dsh-utility-launcher`): one icon at the bottom-left that opens a menu of utility panels, assembled through the host's slots rather than a page-local protocol. Exactly one copy runs per page (the first to load wins a `window` mutex and declares the menu seat `createhelper.utility.item`) and every other copy adds one row to it; consumers maintain it with `launcher:sync` / `launcher:check`.
|
|
9
27
|
|
|
10
28
|
## 0.4.0 - 2026-09-24
|
|
11
29
|
|
|
12
|
-
- Remove `dist/bootstrap.js` (the dock bootstrap fragment) and its `FRAGMENTS` row
|
|
13
|
-
-
|
|
30
|
+
- Remove `dist/bootstrap.js` (the page-local dock bootstrap fragment) and its `FRAGMENTS` row: it appended a self-measuring `position:fixed` container to every consumer's `document.body`, so it necessarily floated above the page and covered the composer's own controls. Consumers now register the host's `sidebar.footer.action` seat, embed no fragment in `lib/client.js`, and have dropped `dock:sync` / `dock:check`.
|
|
31
|
+
- The two host-side fragments (loopback predicates, host guard) are unchanged, so consumers stay pinned to `0.3.0` and `loopback:check` / `guard:check` keep comparing against that version.
|
|
14
32
|
|
|
15
33
|
## 0.3.0 - 2026-09-22
|
|
16
34
|
|
|
@@ -24,18 +42,15 @@ For Chinese, see [CHANGELOG.md](CHANGELOG.md).
|
|
|
24
42
|
|
|
25
43
|
## 0.1.8 - 2026-09-21
|
|
26
44
|
|
|
27
|
-
- The two host-side fragments
|
|
28
|
-
- The fragments' bytes change, so consumers must re-run `loopback:sync` / `guard:sync` and raise their pin to 0.1.8.
|
|
45
|
+
- The two host-side fragments drop "sibling" and state the design goal directly: a plugin ships standalone, with nothing else required.
|
|
29
46
|
|
|
30
47
|
## 0.1.7 - 2026-09-20
|
|
31
48
|
|
|
32
|
-
- The two host-side fragments no longer state how many consumers exist
|
|
33
|
-
- The fragments' bytes change, so consumers must re-run `loopback:sync` / `guard:sync` and raise their pin to 0.1.7.
|
|
49
|
+
- The two host-side fragments no longer state how many consumers exist ("all three plugins", "the three disagree", "diverged three times" give way to wording that names no count): a fragment is embedded verbatim into a consumer's repository, where no reader can check that count.
|
|
34
50
|
|
|
35
51
|
## 0.1.6 - 2026-09-20
|
|
36
52
|
|
|
37
|
-
- Fix the `allowRemoteHost` note in `dist/guard.js`: it read "stop being admitted", the opposite of what the code does — that mode skips both checks, so it admits them.
|
|
38
|
-
- The fragment's bytes change, so consumers must re-run `guard:sync` and raise their pin to 0.1.6.
|
|
53
|
+
- Fix the `allowRemoteHost` note in `dist/guard.js`: it read "stop being admitted", the opposite of what the code does — that mode skips both checks, so it admits them.
|
|
39
54
|
|
|
40
55
|
## 0.1.5 - 2026-09-17
|
|
41
56
|
|
package/CHANGELOG.md
CHANGED
|
@@ -3,14 +3,32 @@
|
|
|
3
3
|
Release Notes 由对应版本段生成;最新版本在前。
|
|
4
4
|
英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
|
|
5
5
|
|
|
6
|
+
## 0.6.0 - 2026-09-28
|
|
7
|
+
|
|
8
|
+
- 新增 `dist/host-http.js`(标记 `dsh-host-http`):宿主半的响应胶水收成一块——`sendJson`(一律带 `no-store`)、POST 门、浏览器授权器、`optionalSessionId`,回复策略自此只有一份定义。已发布的词汇(`need_post`、中文文案、回带 `action`)由 `policy` 覆盖,公开标识符不变。
|
|
9
|
+
- `createBrowserAuthorizer` 收访问器而非值:宿主半的 Connection 会被服务重载置空再赋新值,捕获值的那一版会一直把请求交给一个已销毁的 Connection。
|
|
10
|
+
- launcher 与 host-http 片段随片段逐字进入消费者的注释改为直陈事实:不再有误述其余副本行为的句子,host-http 把引用守卫块名字的类比写明其来源。
|
|
11
|
+
- `dsh-mini-utility-dock` 的 `sync`/`check` 遇到标记区间交叠(`<A> <B> </A> </B>`)的文件直接拒绝且不写出,杜绝此前可能产生的损坏。
|
|
12
|
+
- `dsh-plugin-parity` 把 `dsh-host-http` 块纳入逐块比对,块落点顺序的断言同时覆盖三个 host 块。
|
|
13
|
+
- `dsh-plugin-parity` 的「块外私有实现」扫描只匹配代码形状:Fetch Metadata 的方括号取头与解构取头都算、`LOOPBACK_HOSTNAMES` 只认赋值不认比较,并删去会误伤合理仓库的复合启发式,讲清了理由的注释与散文不再被误判成漂移。
|
|
14
|
+
- `dsh-plugin-parity` 的逐块比对改为剥离块的公共缩进,按 4 空格或 Tab 缩进标记的消费者不再被误判为漂移;`--fleet` 点名不在成员表中的仓即判 FAIL 并非零退出,不再把「未测」报成「跳过」。
|
|
15
|
+
- `dsh-plugin-docs --base` 收到全 0 SHA(新建分支或强推时 `github.event.before` 的值)不再崩在 `git diff`,改为按「无可比前态」给提示并跑完结构校验。
|
|
16
|
+
- `dsh-plugin-docs` 把 `Unreleased` 段纳入版本段/分类段/条目数比较——它是每轮改动最多的段落,此前完全不在检查范围内。
|
|
17
|
+
- 补齐测试:两个诊断 bin 的进程级行为、`dist/loopback.js` / `dist/guard.js` 与新片段的判定行为、一条 `FRAGMENTS` 完整性断言,并为上述行为各补对照用例。
|
|
18
|
+
- 维护:CI 改 `npm ci --ignore-scripts` 并启用缓存、两处都跑 `docs:check`,语法检查覆盖 `bin` 与 `dist`,`publish.yml` 加「tag 必须是 `main` 的祖先」闸门,补齐 `homepage`、`bugs`、`author`;本包零依赖,故开始跟踪 `package-lock.json` 以满足 `npm ci` 与缓存(理由见 `.gitignore`)。
|
|
19
|
+
|
|
20
|
+
## 0.5.1 - 2026-09-25
|
|
21
|
+
|
|
22
|
+
- 修复 launcher 热重载后消失:归属从一次性闩锁改为可释放的认领,owner 被销毁时唤醒其余副本重新注册,不再需要刷新整页。
|
|
23
|
+
|
|
6
24
|
## 0.5.0 - 2026-09-24
|
|
7
25
|
|
|
8
|
-
- 新增 `dist/launcher.js
|
|
26
|
+
- 新增 `dist/launcher.js`(标记 `dsh-utility-launcher`):左下角一个图标,点开一个列出各插件面板的菜单,走宿主的槽位而不是页面级协议。每页只有一份装配(先加载的一方赢得 `window` 互斥并声明菜单座位 `createhelper.utility.item`),其余副本各往那个座位投一行;消费方用 `launcher:sync` / `launcher:check` 维护。
|
|
9
27
|
|
|
10
28
|
## 0.4.0 - 2026-09-24
|
|
11
29
|
|
|
12
|
-
- 移除 `dist/bootstrap.js
|
|
13
|
-
-
|
|
30
|
+
- 移除 `dist/bootstrap.js`(页面级 dock 引导片段)与其 `FRAGMENTS` 条目:它给每个消费插件在 `document.body` 上挂一个自量位置的 `position:fixed` 容器,必然浮在页面之上、压住 composer 自己的控件。消费方改为注册宿主的 `sidebar.footer.action` 座位,`lib/client.js` 不再嵌片段,各仓的 `dock:sync` / `dock:check` 随之删除。
|
|
31
|
+
- 两个 host 侧片段(loopback 判定、host 守卫)字节未变,消费方仍可 pin `0.3.0`,`loopback:check` / `guard:check` 继续按该版本比对。
|
|
14
32
|
|
|
15
33
|
## 0.3.0 - 2026-09-22
|
|
16
34
|
|
|
@@ -24,18 +42,15 @@ Release Notes 由对应版本段生成;最新版本在前。
|
|
|
24
42
|
|
|
25
43
|
## 0.1.8 - 2026-09-21
|
|
26
44
|
|
|
27
|
-
- 两个 host
|
|
28
|
-
- 片段字节因此变化,消费仓需重新运行 `loopback:sync` / `guard:sync` 并把 pin 升到 0.1.8。
|
|
45
|
+
- 两个 host 侧片段的注释去掉 `sibling`,改为直接陈述设计目标:插件独立发布,不依赖同级安装。
|
|
29
46
|
|
|
30
47
|
## 0.1.7 - 2026-09-20
|
|
31
48
|
|
|
32
|
-
- 两个 host
|
|
33
|
-
- 片段字节因此变化,消费仓需重新运行 `loopback:sync` / `guard:sync` 并把 pin 升到 0.1.7。
|
|
49
|
+
- 两个 host 侧片段的注释不再声明消费方数量(「all three plugins」「diverged three times」等改为不指名、不计数的说法):片段逐字嵌入消费者的仓库,那里的读者无从得知有几个消费方。
|
|
34
50
|
|
|
35
51
|
## 0.1.6 - 2026-09-20
|
|
36
52
|
|
|
37
|
-
- 修正 `dist/guard.js` 里 `allowRemoteHost`
|
|
38
|
-
- 该片段的字节因此变化,消费仓需重新运行 `guard:sync` 并把 pin 升到 0.1.6。
|
|
53
|
+
- 修正 `dist/guard.js` 里 `allowRemoteHost` 的行内注释:原文写作「不再被放行」,与代码相反——该模式是跳过这两项检查,即放行。
|
|
39
54
|
|
|
40
55
|
## 0.1.5 - 2026-09-17
|
|
41
56
|
|
package/README.en.md
CHANGED
|
@@ -17,6 +17,7 @@ The DSH plugin family's shared assets: source fragments, an embedding CLI, and t
|
|
|
17
17
|
| --- | --- | --- | --- |
|
|
18
18
|
| loopback predicates | `dsh-loopback-helpers` | `lib/shared.js` | `LOOPBACK_HOSTNAMES`, `normalizeHostValue`, `hostHostname`, `isLoopbackName`, `isLoopbackAddress` |
|
|
19
19
|
| host request guard | `dsh-host-guard` | `lib/shared.js` | `portOf`, `GUARD_REASONS`, `DEFAULT_GUARD_POLICY` |
|
|
20
|
+
| host HTTP glue | `dsh-host-http` | `lib/shared.js` | `sendJson`, `CONNECTION_UNAVAILABLE`, `connectionUnavailable`, `REQUIRE_POST_REASONS`, `DEFAULT_REQUIRE_POST_POLICY`, `createRequirePost`, `createBrowserAuthorizer`, `optionalSessionId` |
|
|
20
21
|
| utility launcher | `dsh-utility-launcher` | `lib/client.js` | `registerUtilityLauncher`, `UTILITY_ITEM_SLOT` |
|
|
21
22
|
|
|
22
23
|
## Usage
|
|
@@ -37,19 +38,34 @@ npm run dock:embed -- check path/to/shared.js
|
|
|
37
38
|
npm run dock:embed -- sync path/to/shared.js
|
|
38
39
|
```
|
|
39
40
|
|
|
40
|
-
Consumers invoke the same CLI as `loopback:sync` / `guard:sync` with the target fixed to their own `lib/shared.js`, and as `launcher:sync` for the `dsh-utility-launcher` block in their own `lib/client.js`.
|
|
41
|
+
Consumers invoke the same CLI as `loopback:sync` / `guard:sync` / `http:sync` with the target fixed to their own `lib/shared.js`, and as `launcher:sync` for the `dsh-utility-launcher` block in their own `lib/client.js`. One command handles the three host-half blocks together: a block is selected by the marker that exists in the file, so `http:sync` and `loopback:sync` write different blocks of the same file.
|
|
41
42
|
|
|
42
43
|
## Constraints
|
|
43
44
|
|
|
44
|
-
- `dist/` is the single source.
|
|
45
|
-
- The
|
|
45
|
+
- `dist/` is the single source. After changing a fragment, run the whole set below under "Development and verification", and have consumers re-`sync`.
|
|
46
|
+
- The three fragments in `lib/shared.js` have fixed positions: `dsh-loopback-helpers` → `dsh-host-guard` → `dsh-host-http`. The guard uses the module-scope names the preceding fragment exports, and neither redeclares nor imports them; `dsh-host-http` reads nothing from the two blocks above it and declares none of their names, so it can be embedded alone. Those positions are the `FRAGMENTS` order, and `sync` replaces blocks from the bottom of the file up; a file whose marker ranges overlap is rejected outright and left unwritten.
|
|
47
|
+
- `dsh-utility-launcher` lands in the client half: exactly one copy of the assembly runs per page (the first to load wins the `window` mutex and declares the menu seat), and every other copy contributes one row to that seat. That is a runtime fact, not a styling choice.
|
|
46
48
|
- A fragment must not contain `import` or `require`; a consumer must publish standalone.
|
|
47
49
|
- `bindGuard` is not exported — a consumer declares its own guard export in the same file. Each plugin passes its own error codes and wording through `policy`; the enforcement is shared.
|
|
50
|
+
- `createRequirePost({ respond, policy })` works the same way: the default code is `method`, and a repository's published vocabulary — `need_post`, Chinese wording, the `action` echoed back — is an override of `policy.method_not_allowed`, not a behavior difference. An unknown key throws.
|
|
51
|
+
- `createBrowserAuthorizer({ getConnection, getConnectionSeen, guard, respond })` takes accessors, not values: a host half's Connection is reassigned to `null` by a service reload, and the value-shaped version would keep admitting requests through a destroyed Connection. The semantics are fixed: a Connection that throws answers 503 `connection_unavailable`; a rejection is answered with its own code (401 → `unauthorized`, anything else → `forbidden`); a Connection that was seen but is absent now answers 503 and never falls back to the weaker guard; a host that never had one is decided by `guard`.
|
|
48
52
|
|
|
49
53
|
## Cross-repo consistency
|
|
50
54
|
|
|
51
|
-
A consumer's `npm test` runs `loopback:check` / `guard:check`, comparing its
|
|
55
|
+
A consumer's `npm test` runs `loopback:check` / `guard:check` / `http:check`, comparing the three blocks in its own `lib/shared.js` byte for byte against the dock version it pins. A published version is immutable and consumers pin an exact version, so pin agreement implies fragment agreement. The converse does not hold: `check` selects blocks by the markers it recognizes, so a consumer that has not raised its pin never looks at a block the older CLI does not know and that one check passes vacuously. After a fragment is added, a consumer re-runs all four `sync` scripts and raises its pin to the released version in the same commit.
|
|
52
56
|
|
|
53
|
-
`dsh-plugin-parity` (a bin provided by this package) checks that cross-repo property directly,
|
|
57
|
+
`dsh-plugin-parity` (a bin provided by this package) checks that cross-repo property directly: every host block compared in isolation after stripping the block's common indentation (the three, `dsh-host-http` included), pin agreement, block position, and a behavioral comparison. It is a manual diagnostic and **does not run in CI**: the property cannot hold while peers sit on a different branch. The member list is caller-supplied via `--member <repo>:<export>` — the guard factory's export name is per-repository by fragment design, so the tool presupposes no members; `--fleet <repo>` names the single member that opts into fleet mode, and only its guard is checked for "the relaxation stays bounded", while naming a repository that is not a member fails with a non-zero exit; without `--member` it runs the static checks only and discovers every checkout that embeds the guard block. `--self-test` needs no checkout at all: it exercises the block extractor and the block-dropping step against inline samples, so run it first whenever either changes.
|
|
54
58
|
|
|
55
|
-
`dsh-plugin-docs` (another bin of this package) checks the documentation pairs for structural alignment: the two READMEs share heading levels and code-fence languages, the two CHANGELOGs expose the same releases
|
|
59
|
+
`dsh-plugin-docs` (another bin of this package) checks the documentation pairs for structural alignment: the two READMEs share heading levels and code-fence languages, the two CHANGELOGs expose the same releases — the `Unreleased` section included — with sections and item counts (section titles normalized through a bilingual category map); `--base <revision>` additionally requires both files of a pair to change together — a one-sided edit is a missing translation. When that revision is the all-zero null OID — which is what `github.event.before` carries for a newly created or force-pushed branch — there is no previous state to compare, so that one requirement is skipped with a note while the structural checks still run. Repositories opt in by pointing their own `docs:check` script at it; this package runs the same check on its own bilingual docs.
|
|
60
|
+
|
|
61
|
+
## Development and verification
|
|
62
|
+
|
|
63
|
+
`dist/` is the source, so one change has to pass three layers: fragment behavior, the CLI, and the documentation pair. This is the full check of this repository (identical to the Verify block in `AGENTS.md`; the release procedure is in `RELEASING.md`):
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npm test
|
|
67
|
+
node bin/dsh-plugin-docs.js
|
|
68
|
+
for f in bin/*.js dist/*.js; do node --check "$f"; done
|
|
69
|
+
node bin/dsh-plugin-parity.js --self-test
|
|
70
|
+
npm pack --dry-run
|
|
71
|
+
```
|
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ DSH 插件族的共享资产:源码片段、嵌入 CLI,以及围绕它们的
|
|
|
17
17
|
| --- | --- | --- | --- |
|
|
18
18
|
| loopback 判定 | `dsh-loopback-helpers` | `lib/shared.js` | `LOOPBACK_HOSTNAMES`、`normalizeHostValue`、`hostHostname`、`isLoopbackName`、`isLoopbackAddress` |
|
|
19
19
|
| host 请求守卫 | `dsh-host-guard` | `lib/shared.js` | `portOf`、`GUARD_REASONS`、`DEFAULT_GUARD_POLICY` |
|
|
20
|
+
| host HTTP 胶水 | `dsh-host-http` | `lib/shared.js` | `sendJson`、`CONNECTION_UNAVAILABLE`、`connectionUnavailable`、`REQUIRE_POST_REASONS`、`DEFAULT_REQUIRE_POST_POLICY`、`createRequirePost`、`createBrowserAuthorizer`、`optionalSessionId` |
|
|
20
21
|
| utility 入口 | `dsh-utility-launcher` | `lib/client.js` | `registerUtilityLauncher`、`UTILITY_ITEM_SLOT` |
|
|
21
22
|
|
|
22
23
|
## 使用
|
|
@@ -37,19 +38,34 @@ npm run dock:embed -- check path/to/shared.js
|
|
|
37
38
|
npm run dock:embed -- sync path/to/shared.js
|
|
38
39
|
```
|
|
39
40
|
|
|
40
|
-
消费插件以 `loopback:sync` / `guard:sync` 调用同一 CLI,目标固定为自身 `lib/shared.js`;client 半以 `launcher:sync` 维护 `dsh-utility-launcher` 块,目标固定为自身 `lib/client.js
|
|
41
|
+
消费插件以 `loopback:sync` / `guard:sync` / `http:sync` 调用同一 CLI,目标固定为自身 `lib/shared.js`;client 半以 `launcher:sync` 维护 `dsh-utility-launcher` 块,目标固定为自身 `lib/client.js`。宿主半的三个块由一条命令一并处理:标记存在即被选中,因此 `http:sync` 与 `loopback:sync` 写的是同一个文件里的不同块。
|
|
41
42
|
|
|
42
43
|
## 约束
|
|
43
44
|
|
|
44
|
-
- `dist/`
|
|
45
|
-
- `lib/shared.js`
|
|
45
|
+
- `dist/` 是唯一来源。改片段后跑一遍下面「开发与验证」的命令,并让消费插件重新 `sync`。
|
|
46
|
+
- `lib/shared.js` 中三个片段的落点固定:`dsh-loopback-helpers` → `dsh-host-guard` → `dsh-host-http`。守卫直接使用前一块声明的模块级名字,既不重新声明也不 import;`dsh-host-http` 不读上面两块、也不声明它们的名字,故可单独嵌入。落点顺序即 `FRAGMENTS` 顺序;`sync` 自下而上替换块,标记区间彼此交叠的文件会被直接拒绝、不写出任何内容。
|
|
47
|
+
- `dsh-utility-launcher` 落在 client 半:每页只运行一份该装配(先加载的一方赢得 `window` 互斥并声明菜单座位),其余副本只往座位里投一行。这是运行时事实,不是样式选择。
|
|
46
48
|
- 片段不得包含 `import` 或 `require`;消费插件必须能独立发布。
|
|
47
49
|
- `bindGuard` 不从片段导出——消费插件在同一文件中声明自己的 guard 导出。各插件通过 `policy` 传入自身错误码与文案,判定逻辑共用。
|
|
50
|
+
- `createRequirePost({ respond, policy })` 同上:默认 code 是 `method`,一个仓已发布的词汇(例如 `need_post`、中文文案、回带 `action`)由 `policy.method_not_allowed` 覆盖,行为不变;未知 key 抛错。
|
|
51
|
+
- `createBrowserAuthorizer({ getConnection, getConnectionSeen, guard, respond })` 收的是访问器而非值:宿主半的 Connection 会被服务重载置空,捕获值的那一版会一直放行给一个已销毁的 Connection。语义固定:Connection 抛异常 → 503 `connection_unavailable`;给出 rejection → 以该码回绝(401 → `unauthorized`,其余 → `forbidden`);见过 Connection 而当下没有 → 503,且绝不退回更弱的 guard;从未见过 → 交给 `guard`。
|
|
48
52
|
|
|
49
53
|
## 跨仓一致性
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
消费插件的 `npm test` 用 `loopback:check` / `guard:check` / `http:check` 把自己 `lib/shared.js` 里的三个块与所 pin 的 dock 版本逐字节比对。dock 版本不可变且消费方 pin 精确版本,故「pin 一致」即「片段一致」。反向不成立:`check` 只按它能识别的标记选块,一个还没抬 pin 的消费方根本不比对新增的那个块,对它这一项是空过的。所以新增片段后,消费方要重跑四个 `sync`,并把 pin 一起抬到发布版本。
|
|
52
56
|
|
|
53
|
-
`dsh-plugin-parity`(本包提供的 bin
|
|
57
|
+
`dsh-plugin-parity`(本包提供的 bin)直接检这条跨仓性质:逐块(剥离块的公共缩进后)比对三个 host 块(含 `dsh-host-http`)、pin 一致性断言、块落点顺序断言与行为级对比。它是人工诊断工具,**不在 CI 中运行**:peer 处于不同分支时该性质本就不成立。成员表由调用方经 `--member <repo>:<export>` 提供——守卫工厂的导出名按片段设计是各仓自定的,故本工具不预设任何成员;`--fleet <repo>` 点名唯一启用 fleet 模式的成员,只对它检查「放宽有边界」,点名不在成员表中的仓即判 FAIL 并非零退出;不带 `--member` 时只跑静态检查,自动发现所有嵌入了宿主守卫块的仓。`--self-test` 不需要任何 checkout:它用内联样例检验块提取与「丢块」两步,改动这两个函数后先跑它。
|
|
54
58
|
|
|
55
|
-
`dsh-plugin-docs`(同样是本包的 bin)校验双语文档的结构对齐:README 中英标题层级与代码围栏一致,CHANGELOG
|
|
59
|
+
`dsh-plugin-docs`(同样是本包的 bin)校验双语文档的结构对齐:README 中英标题层级与代码围栏一致,CHANGELOG 中英版本段(含 `Unreleased` 段)、分类段与条目数一致(分类标题经双语映射归一);`--base <revision>` 额外要求双语对同改,单边改动即缺翻译。该 revision 是全 0 的 null OID 时(新建分支或强推时 `github.event.before` 就是这个值)没有可比的前态,这一项跳过并给出提示,其余结构校验照常。接入方式是各仓自己的 `docs:check` script 指向它——本包自身的双语文档也由此校验。
|
|
60
|
+
|
|
61
|
+
## 开发与验证
|
|
62
|
+
|
|
63
|
+
片段是唯一来源,所以一次改动要同时过片段行为、CLI 与文档这三层;下面是本仓的完整校验(与 `AGENTS.md` 的 Verify 块一致,发布步骤见 `RELEASING.md`):
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npm test
|
|
67
|
+
node bin/dsh-plugin-docs.js
|
|
68
|
+
for f in bin/*.js dist/*.js; do node --check "$f"; done
|
|
69
|
+
node bin/dsh-plugin-parity.js --self-test
|
|
70
|
+
npm pack --dry-run
|
|
71
|
+
```
|
|
@@ -1,40 +1,59 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Embed a canonical fragment from this package into a consumer file.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// of problem: the consumer must ship the code standalone (a browser classic
|
|
6
|
-
// script cannot `import`, and a host half must not depend on
|
|
4
|
+
// Every fragment is maintained the same way, because all of them have the same
|
|
5
|
+
// shape of problem: the consumer must ship the code standalone (a browser classic
|
|
6
|
+
// script cannot `import`, and a host half must not depend on this package being
|
|
7
7
|
// installed), so the code is embedded at build time and drift is a build error.
|
|
8
8
|
//
|
|
9
|
-
// dsh-mini-utility-dock -> <dsh-mini-utility-dock> ... </dsh-mini-utility-dock>
|
|
10
|
-
// the Mini Utility Dock bootstrap, embedded into a plugin's lib/client.js
|
|
11
|
-
//
|
|
12
9
|
// dsh-loopback-helpers -> <dsh-loopback-helpers> ... </dsh-loopback-helpers>
|
|
13
10
|
// the loopback predicates, embedded into a plugin's lib/shared.js
|
|
14
11
|
//
|
|
15
12
|
// dsh-host-guard -> <dsh-host-guard> ... </dsh-host-guard>
|
|
16
13
|
// the same-origin request guard, embedded into the same file
|
|
17
14
|
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
15
|
+
// dsh-host-http -> <dsh-host-http> ... </dsh-host-http>
|
|
16
|
+
// the host response glue: JSON replies, the POST gate, the browser
|
|
17
|
+
// authorizer, embedded into the same file below the guard block
|
|
18
|
+
//
|
|
19
|
+
// dsh-utility-launcher -> <dsh-utility-launcher> ... </dsh-utility-launcher>
|
|
20
|
+
// the shared launcher assembly: one icon plus the menu of utility panels it
|
|
21
|
+
// opens, embedded into a plugin's lib/client.js
|
|
22
|
+
//
|
|
23
|
+
// The host half of a plugin embeds ALL THREE shared.js fragments, in the order
|
|
24
|
+
// above, because its guard reads the predicates the block above it declares. Each
|
|
25
|
+
// is an independent block with its own markers and its own check, so a plugin that
|
|
26
|
+
// only needs the predicates can embed just those.
|
|
22
27
|
//
|
|
23
28
|
// The fragment is selected by the marker already present in the target file, so
|
|
24
|
-
// one command serves
|
|
25
|
-
// `
|
|
29
|
+
// one command serves every target and only a named block is ever touched. A
|
|
30
|
+
// consumer calls this through its own `launcher:sync` / `guard:sync` /
|
|
31
|
+
// `loopback:sync` / `http:sync` script; the paired `check` mode is what CI runs.
|
|
26
32
|
|
|
27
33
|
import { readFile, writeFile, stat } from 'node:fs/promises'
|
|
28
34
|
import { resolve } from 'node:path'
|
|
29
35
|
import { fileURLToPath } from 'node:url'
|
|
30
36
|
|
|
31
|
-
// Listed in
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
37
|
+
// Listed in the order the blocks sit in a consumer's file. That order is
|
|
38
|
+
// load-bearing for the consumer, not for this tool:
|
|
39
|
+
//
|
|
40
|
+
// 1. the guard fragment reads the predicates the loopback fragment declares, so
|
|
41
|
+
// a consumer that wants the guard syncs both, loopback first;
|
|
42
|
+
// 2. the host-http fragment lands below the guard block, where a consumer's own
|
|
43
|
+
// `lib/shared.js` glue has always lived. It depends on neither block above
|
|
44
|
+
// it and declares nothing they declare, so the three coexist in one file;
|
|
45
|
+
// 3. the launcher is independent of all of them and lands in the client half
|
|
46
|
+
// instead, so it sits last.
|
|
47
|
+
//
|
|
48
|
+
// `main()` replaces each block working from the bottom of the file upward, and it
|
|
49
|
+
// orders that pass by the line numbers it found rather than by this list, so
|
|
50
|
+
// adding a row here states where a block sits and cannot shift another block's
|
|
51
|
+
// splice. Registering a fragment whose markers sit in a different order is still
|
|
52
|
+
// worth catching — `dsh-plugin-parity` checks it across repositories.
|
|
35
53
|
const FRAGMENTS = [
|
|
36
54
|
{ name: 'dsh-loopback-helpers', source: 'loopback.js' },
|
|
37
55
|
{ name: 'dsh-host-guard', source: 'guard.js' },
|
|
56
|
+
{ name: 'dsh-host-http', source: 'host-http.js' },
|
|
38
57
|
{ name: 'dsh-utility-launcher', source: 'launcher.js' }
|
|
39
58
|
]
|
|
40
59
|
|
|
@@ -42,12 +61,19 @@ const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
|
|
|
42
61
|
const marker = (name, edge) => `// <${edge}${name}>`
|
|
43
62
|
|
|
44
63
|
function usage() {
|
|
45
|
-
const fragments = FRAGMENTS.map((f) => ` ${marker(f.name, '')} ->
|
|
64
|
+
const fragments = FRAGMENTS.map((f) => ` ${marker(f.name, '')} ... ${marker(f.name, '/')} -> dist/${f.source}`).join('\n')
|
|
46
65
|
return [
|
|
47
66
|
'Usage: dsh-mini-utility-dock <sync|check> <consumer-file>',
|
|
48
67
|
'',
|
|
49
|
-
'The
|
|
50
|
-
|
|
68
|
+
'The blocks to maintain are the ones whose markers are present in <consumer-file>;',
|
|
69
|
+
'one command handles every block in that file:',
|
|
70
|
+
fragments,
|
|
71
|
+
'',
|
|
72
|
+
'A plugin host half embeds the three lib/shared.js blocks: the loopback',
|
|
73
|
+
'predicates, the same-origin guard that reads them, and the host HTTP glue',
|
|
74
|
+
'(JSON replies, the POST gate, the browser authorizer) below the guard. The',
|
|
75
|
+
'launcher block belongs to the client half in lib/client.js.',
|
|
76
|
+
'`sync` writes them, `check` exits non-zero on drift.'
|
|
51
77
|
].join('\n')
|
|
52
78
|
}
|
|
53
79
|
|
|
@@ -70,9 +96,10 @@ function indentation(line) {
|
|
|
70
96
|
}
|
|
71
97
|
|
|
72
98
|
// Locate every marked block in the file, in fragment order. A host half embeds
|
|
73
|
-
//
|
|
74
|
-
// FRAGMENTS order also keeps the dependency order (the guard
|
|
75
|
-
// declared by the loopback block above it
|
|
99
|
+
// all three shared.js fragments, so one command must maintain all of them;
|
|
100
|
+
// processing them in FRAGMENTS order also keeps the dependency order (the guard
|
|
101
|
+
// reads the predicates declared by the loopback block above it, and the HTTP glue
|
|
102
|
+
// sits below both without reading either).
|
|
76
103
|
function locate(source) {
|
|
77
104
|
const lines = source.split(/\r?\n/)
|
|
78
105
|
const candidates = FRAGMENTS.map((fragment) => {
|
|
@@ -89,7 +116,20 @@ function locate(source) {
|
|
|
89
116
|
throw new Error(`expected exactly one marked block for ${fragment.name} (${marker(fragment.name, '')} ... ${marker(fragment.name, '/')})`)
|
|
90
117
|
}
|
|
91
118
|
}
|
|
92
|
-
|
|
119
|
+
const blocks = candidates.map(({ fragment, start, end }) => ({ fragment, start: start[0], end: end[0] }))
|
|
120
|
+
// Each block is replaced by splicing its own line range, so two different
|
|
121
|
+
// fragments' ranges must not overlap. Interleaved markers (`<A> <B> </A> </B>`)
|
|
122
|
+
// would make one pass write into the range another pass had just rewritten, so
|
|
123
|
+
// the result would be corrupted rather than merely stale; the uniqueness of each
|
|
124
|
+
// marker pair is enforced above, and this closes the case a single pair cannot.
|
|
125
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
126
|
+
for (let j = i + 1; j < blocks.length; j++) {
|
|
127
|
+
if (blocks[i].start <= blocks[j].end && blocks[j].start <= blocks[i].end) {
|
|
128
|
+
throw new Error(`marked blocks for ${blocks[i].fragment.name} and ${blocks[j].fragment.name} overlap; each marker pair must enclose its own lines`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { lines, blocks }
|
|
93
133
|
}
|
|
94
134
|
|
|
95
135
|
function rendered(lines, fragmentSource, block) {
|
|
@@ -99,9 +139,9 @@ function rendered(lines, fragmentSource, block) {
|
|
|
99
139
|
return [...lines.slice(0, block.start + 1), ...body, ...lines.slice(block.end)]
|
|
100
140
|
}
|
|
101
141
|
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
142
|
+
// One pass over the target file maintains EVERY marked block it carries, applied
|
|
143
|
+
// from the bottom of the file upward so each block's line indices are still valid
|
|
144
|
+
// when its turn comes. See the loop below for why that order is the whole trick.
|
|
105
145
|
async function main() {
|
|
106
146
|
const [command, fileName, ...rest] = process.argv.slice(2)
|
|
107
147
|
if (!['sync', 'check'].includes(command) || !fileName || rest.length) throw new Error(usage())
|
|
@@ -110,7 +150,14 @@ async function main() {
|
|
|
110
150
|
|
|
111
151
|
let lines = located.lines
|
|
112
152
|
const applied = []
|
|
113
|
-
|
|
153
|
+
// Walk by line position, the block nearest the end of the file first. Each pass
|
|
154
|
+
// replaces a line range, so every block above it must keep valid indices; going
|
|
155
|
+
// top-down would shift them and a later pass would splice the wrong lines.
|
|
156
|
+
// `locate()` has already rejected any file whose marker ranges overlap, so these
|
|
157
|
+
// blocks are disjoint: the pass can be ordered by the lines themselves rather
|
|
158
|
+
// than by the table, and a table/file order disagreement then only rewrites
|
|
159
|
+
// blocks in a different sequence, corrupting nothing.
|
|
160
|
+
for (const block of [...located.blocks].sort((a, b) => b.start - a.start)) {
|
|
114
161
|
const fragmentSource = await readFile(resolve(packageRoot, 'dist', block.fragment.source), 'utf8')
|
|
115
162
|
lines = rendered(lines, fragmentSource, block)
|
|
116
163
|
applied.unshift(block.fragment.name)
|
package/bin/dsh-plugin-docs.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Bilingual structure check for the documentation
|
|
2
|
+
// Bilingual structure check for the documentation pair a repository ships:
|
|
3
3
|
// README.md against README.en.md, CHANGELOG.md against CHANGELOG.en.md.
|
|
4
4
|
//
|
|
5
|
-
// Verifies, naming no repository and taking no file arguments (the four
|
|
6
|
-
// are
|
|
5
|
+
// Verifies, naming no repository and taking no file arguments (the four file
|
|
6
|
+
// names are fixed by convention, not passed in):
|
|
7
7
|
// * both READMEs share the same heading-level sequence and code-fence
|
|
8
8
|
// languages — content inside fences is exempt, since that is where
|
|
9
9
|
// language-specific examples live;
|
|
10
|
-
// * both CHANGELOGs expose the same releases
|
|
11
|
-
//
|
|
10
|
+
// * both CHANGELOGs expose the same releases — the `Unreleased` section
|
|
11
|
+
// included, since that is where the newest edits land — with version, date,
|
|
12
|
+
// per-section item counts, section titles normalized through a bilingual
|
|
12
13
|
// category map (新增/Added, 修复/Fixed, ...);
|
|
13
14
|
// * with `--base <revision>`, both files of each pair changed together
|
|
14
|
-
// since that revision — a one-sided edit is a missing translation.
|
|
15
|
+
// since that revision — a one-sided edit is a missing translation. An
|
|
16
|
+
// all-zero revision is the null OID, which is what `github.event.before`
|
|
17
|
+
// carries for a branch that was just created or force-pushed: there is no
|
|
18
|
+
// previous revision to diff against, so that one requirement is skipped
|
|
19
|
+
// with a note instead of dying inside `git diff`.
|
|
15
20
|
//
|
|
16
21
|
// Repositories opt in through their own `docs:check` script; this package
|
|
17
22
|
// runs the same check on its own bilingual docs.
|
|
@@ -47,12 +52,26 @@ function changelogShape(file) {
|
|
|
47
52
|
let release
|
|
48
53
|
let section
|
|
49
54
|
|
|
55
|
+
// A `## ...` heading opens a release and clears the current section, so bullets
|
|
56
|
+
// that follow it before any `###` still count — into an anonymous leading
|
|
57
|
+
// section — instead of being silently dropped.
|
|
58
|
+
const openRelease = (version, date) => {
|
|
59
|
+
release = { version, date, sections: [] }
|
|
60
|
+
releases.push(release)
|
|
61
|
+
section = undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
for (const line of read(file).split('\n')) {
|
|
51
65
|
const version = line.match(/^##\s+(\d+\.\d+\.\d+)(?:\s+-\s+(\d{4}-\d{2}-\d{2}))?\s*$/)
|
|
52
66
|
if (version) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
openRelease(version[1], version[2] ?? '')
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
// `Unreleased` is the version-less top release and usually the most-edited
|
|
71
|
+
// section, so it is compared with the same shape as a numbered one. Both
|
|
72
|
+
// languages keep the literal heading, so the labels match directly.
|
|
73
|
+
if (/^##\s+Unreleased\s*$/i.test(line)) {
|
|
74
|
+
openRelease('Unreleased', '')
|
|
56
75
|
continue
|
|
57
76
|
}
|
|
58
77
|
if (!release) continue
|
|
@@ -63,7 +82,13 @@ function changelogShape(file) {
|
|
|
63
82
|
release.sections.push(section)
|
|
64
83
|
continue
|
|
65
84
|
}
|
|
66
|
-
if (
|
|
85
|
+
if (/^\s*-\s+/.test(line)) {
|
|
86
|
+
if (!section) {
|
|
87
|
+
section = { title: '', items: 0 }
|
|
88
|
+
release.sections.push(section)
|
|
89
|
+
}
|
|
90
|
+
section.items += 1
|
|
91
|
+
}
|
|
67
92
|
}
|
|
68
93
|
|
|
69
94
|
return releases
|
|
@@ -105,16 +130,25 @@ if (baseIndex !== -1) {
|
|
|
105
130
|
const base = process.argv[baseIndex + 1]
|
|
106
131
|
if (!base) throw new Error('--base requires a Git revision')
|
|
107
132
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
// The null OID, in either hash length. A workflow that passes
|
|
134
|
+
// `github.event.before` here gets it whenever a branch is created or
|
|
135
|
+
// force-pushed, and `git diff 000... HEAD` is not a comparison with no base —
|
|
136
|
+
// it is an error. So the honest reading is the one the push event states: there
|
|
137
|
+
// is nothing before this push, and the pair test cannot run.
|
|
138
|
+
if (/^0+$/.test(base)) {
|
|
139
|
+
console.log(`note: --base ${base} is the null OID (new branch or force-push) — pair-change check skipped`)
|
|
140
|
+
} else {
|
|
141
|
+
const changed = new Set(execFileSync('git', ['diff', '--name-only', base, 'HEAD'], { encoding: 'utf8' })
|
|
142
|
+
.split(/\r?\n/)
|
|
143
|
+
.filter(Boolean))
|
|
144
|
+
|
|
145
|
+
for (const [primary, translation] of [
|
|
146
|
+
['README.md', 'README.en.md'],
|
|
147
|
+
['CHANGELOG.md', 'CHANGELOG.en.md'],
|
|
148
|
+
]) {
|
|
149
|
+
if (changed.has(primary) !== changed.has(translation)) {
|
|
150
|
+
throw new Error(`${primary} and ${translation} must change together`)
|
|
151
|
+
}
|
|
118
152
|
}
|
|
119
153
|
}
|
|
120
154
|
}
|