fix(web-app): defer to a user-configured client-hmr row
The whole-tree name scan (entries() recurses into subtrees) already skips creation when any patch layer carries the row, including a disabled one; make that contract explicit in the comment, pin it with a test, and record it in the Agent Note.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.md
|
||||
2026-08-11-cmdline-seam-trim.md: e9d30c94baed0e0e76c36d7561f50353a4b3eace
|
||||
2026-08-11-cmdline-seam-trim.zh.md: c4ee26d25b72b4d77d6ec2affbb4647e08c7cb1a
|
||||
2026-08-11-cmdline-seam-trim.md: 3fb2f3e0941ad4cf6e2fb7e1afbe6cf31af92f41
|
||||
2026-08-11-cmdline-seam-trim.zh.md: 002a76fc57e1d26e15c2619a380874a0eecd435f
|
||||
@@ -12,7 +12,7 @@ The app-owned command line ([note](2026-08-06-app-owned-command-line.md)) shippe
|
||||
|
||||
Express all three with interfaces that already exist:
|
||||
|
||||
- **Conditional dev row.** `dsh-web-app` no longer ships a disabled `client-hmr` row; in development mode its runtime plugin creates the row in the root tree after Loader settlement with plain `loader.create`, guarded for reload idempotence. A root-tree row is outside the include, so user-patch reapplication cannot restore it to disabled — the property the in-memory override existed for. The incremental client-module scan adds it to the roster before any page loads; a browser arrives only after a human reads the URL line, and its `EventSource` reconnects by spec. `Entry.enableRuntime`, its two state fields, and `enableRow` are deleted.
|
||||
- **Conditional dev row.** `dsh-web-app` no longer ships a disabled `client-hmr` row; in development mode its runtime plugin creates the row in the root tree after Loader settlement with plain `loader.create`; a whole-tree name scan makes the creation reload-idempotent and defers to a user-configured `dsh-client-hmr` row (even a disabled one). A root-tree row is outside the include, so user-patch reapplication cannot restore it to disabled — the property the in-memory override existed for. The incremental client-module scan adds it to the roster before any page loads; a browser arrives only after a human reads the URL line, and its `EventSource` reconnects by spec. `Entry.enableRuntime`, its two state fields, and `enableRow` are deleted.
|
||||
- **Tree-carrier config.** Include declares the existing `EntryGroup.key` marker instead of implementing `EntryConfigResolver`; the Loader hook keeps every tree carrier's config literal. Include's own `path` loses `!!js` support — no configuration ever used it, and the pinning test now asserts the literal tree-carrier contract instead.
|
||||
- **Launcher app-knowledge.** The launcher recognizes no app row. SIGTERM is a supervisor's ordinary stop request and exits 0 on every surface (SIGINT stays 130); the launcher cannot know whether the app considered its work complete, and the previous 143 depended on naming the headless row. Every boot watches its user patch layers — a one-shot surface exits through bounded shutdown, which disposes the watchers before the loop drains. The headless runner exits through `ctx.appExit` like any other app; its output streams are a package-internal `internals` test seam, and `ctx.headlessIo` is deleted.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
三者全部改用已经存在的接口表达:
|
||||
|
||||
- **条件 dev 行。** `dsh-web-app` 不再随附禁用的 `client-hmr` 行;开发模式下其 runtime 插件在 Loader 结算后用普通的 `loader.create` 在根树中创建该行,并带重载幂等保护。根树的行在 include 之外,用户 patch 的重新应用无法把它恢复为禁用 —— 这正是内存覆盖机制存在的理由。增量式客户端模块扫描会在任何页面加载之前把它加入名录;浏览器只会在人读到 URL 行之后到来,其 `EventSource` 按规范自动重连。`Entry.enableRuntime`、它的两个状态字段和 `enableRow` 一并删除。
|
||||
- **条件 dev 行。** `dsh-web-app` 不再随附禁用的 `client-hmr` 行;开发模式下其 runtime 插件在 Loader 结算后用普通的 `loader.create` 在根树中创建该行;全树名称扫描让创建具备重载幂等性,并让位于用户自行配置的 `dsh-client-hmr` 行(即便该行被禁用)。根树的行在 include 之外,用户 patch 的重新应用无法把它恢复为禁用 —— 这正是内存覆盖机制存在的理由。增量式客户端模块扫描会在任何页面加载之前把它加入名录;浏览器只会在人读到 URL 行之后到来,其 `EventSource` 按规范自动重连。`Entry.enableRuntime`、它的两个状态字段和 `enableRow` 一并删除。
|
||||
- **树载体配置。** Include 改为声明已有的 `EntryGroup.key` 标记,不再实现 `EntryConfigResolver`;Loader 钩子让每个树载体的配置保持字面值。Include 自己的 `path` 失去 `!!js` 支持 —— 从未有配置用过它,固定该行为的测试改为断言字面值树载体约定。
|
||||
- **启动器的应用知识。** 启动器不再识别任何应用行。SIGTERM 是监督进程的普通停止请求,在所有 surface 上以 0 退出(SIGINT 仍为 130);启动器无从知道应用是否认为工作已完成,而之前的 143 依赖于点名 headless 行。每次启动都监视用户 patch 层 —— 一次性 surface 经由有界关闭退出,关闭会先 dispose 监视器再排空事件循环。headless runner 像任何应用一样经 `ctx.appExit` 退出;其输出流是包内 `internals` 测试接缝,`ctx.headlessIo` 删除。
|
||||
|
||||
|
||||
@@ -160,8 +160,11 @@ export function apply(ctx: Context, config: Config): void {
|
||||
} else {
|
||||
void loader.await().then(async () => {
|
||||
// The tree can be disposed while settlement was in flight (early
|
||||
// SIGTERM); re-check before mutating it. A reload of this fiber must
|
||||
// not duplicate the row a previous generation created.
|
||||
// SIGTERM); re-check before mutating it. The name scan spans every
|
||||
// tree (entries() recurses into subtrees), so a row the user
|
||||
// configured in a patch layer — enabled, reconfigured, or
|
||||
// deliberately disabled — wins over this default, and a reload of
|
||||
// this fiber never duplicates the row a previous generation created.
|
||||
if (ctx.get('loader') === undefined) return
|
||||
const mounted = [...ctx.loader.entries()].some(entry => entry.options.name === HMR_ROW_NAME)
|
||||
if (!mounted) await ctx.loader.create({ name: HMR_ROW_NAME })
|
||||
|
||||
@@ -187,6 +187,28 @@ describe('web-app runtime glue', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('defers to a user-configured client-hmr row anywhere in the tree', async () => {
|
||||
stageDist()
|
||||
const ctx = new Context()
|
||||
ctx.provide('httpServer', fakeHttpServer().server)
|
||||
const created: string[] = []
|
||||
// The user's own row — possibly patched into an include subtree and even
|
||||
// disabled there — already carries the name; the runtime must not create
|
||||
// a second one beside it.
|
||||
ctx.provide('loader', {
|
||||
entries: () => [{ options: { id: 'my-hmr', name: '@deepseek-ai/dsh-client-hmr', disabled: true } }][Symbol.iterator](),
|
||||
create: (options: { name: string }) => {
|
||||
created.push(options.name)
|
||||
return Promise.resolve(options.name)
|
||||
},
|
||||
await: () => Promise.resolve(),
|
||||
} as never)
|
||||
apply(ctx, new Config({ mode: 'development', printUrl: false, surfaceContext: false, trustedHosts: [] }))
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
expect(created).toEqual([])
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('skips the dev row when the tree is disposed during settlement and logs a creation failure', async () => {
|
||||
stageDist()
|
||||
const raced = new Context()
|
||||
|
||||
Reference in New Issue
Block a user