pack-time lowering and the single-build preview
pack 期 lowering 与单构建 preview
The browser worker can neither compile modules at load nor be served by the product webserver: every module body must arrive runnable, and the page must be a static artifact. Both surfaces drifted early. The loader carried a fallback compiler, so a collector gap surfaced as a slow boot instead of a broken image — and `acorn` rode into `lib/worker.js` through the package barrel, a parser a runtime that only wraps pre-
English
Problem
The browser worker can neither compile modules at load nor be served by the product webserver: every module body must arrive runnable, and the page must be a static artifact. Both surfaces drifted early. The loader carried a fallback compiler, so a collector gap surfaced as a slow boot instead of a broken image — and acorn rode into lib/worker.js through the package barrel, a parser a runtime that only wraps pre-lowered bodies never needs. The preview was a second HTML template beside the served one, a page the served index could silently drift away from.
Decision
Lowering happens at pack time only. @deepseek-ai/dsh-experimental-webworker-packer composes the profile, materializes the closure, and lowers every JavaScript body; LOWERING_VERSION and WRAPPER_PARAMS are the pack↔worker contract and live in src/image-layout.ts beside the rest of the image layout. The loader wraps bodies exactly as the image holds them: a body still carrying module syntax is a refusal naming the image, and startWorkerHost requires the manifest's lowered to equal this build's contract before it mounts a single module. lowerModuleSource is the transform's only face and the packer its only caller; the same parse feeds reachability with statically named imports, re-exports, and dynamic imports, calls through require, and module-scope direct calls of the form createRequire(import.meta.url)('pkg') through a named node:module or module import. Stored results, CommonJS-obtained createRequire, computed request names, and other bases stay runtime-only; targets reachable only through those forms require image entry seeds. Inside the worker graph, imports name the module that owns the value — never the package barrel, which is the edge that smuggled the parser in. Source-directory exclusion applies only to workspace and vendored packages whose runtime plane is built lib/; installed third-party packages retain JavaScript under src/ and dist/ because their published entrypoints may resolve there.
The preview is the served page plus one tag. One Vite build emits dist/index.html and dist/preview.html sharing every chunk; the only difference is a prepended bootstrap entry whose module connects the worker host. Startup then converges on one protocol: whichever side applies the injection table settles the __DSH_BOOT_READY__ deferred — the served renderer resolves it in a tail script after the rendered rows, the worker bootstrap installs it before its first await and settles it after the last row — and the client entry awaits it before reading any injected state, so the chain from the stock entry onward is the served chain verbatim. Plugin combo scripts and maps travel through the tunnel; the page-side loader embeds each tunnel-only map as a Base64 data URL before executing its script Blob, preserving indexed-map component names in DevTools without another object-URL lifetime. The build uses a relative base so the output mounts under any static directory; the served form anchors deep SPA-fallback paths by rendering <base href="/"> at serve time, keeping the on-disk pages byte-shared.
The repository preview carries selectable filesystem sources. The packer emits one base image and a small overlay archive for each named built-in fixture. Without a source query, preview.html waits at a chooser for an empty filesystem, the built-in fixtures, or the separately owned WebFS provider. A valid preview-fixture=none|<built-in-id> query selects directly and skips the chooser for deterministic browser runs; its distinct name avoids the Client's existing fixture transport switch. The Worker mounts the base and then applies the selected overlays in order, restricted to home/ and workspace/, before it validates the base manifest or boots Cordis. packages/experimental/webworker-runtime/tests/fixtures/vfs-example/ supplies one built-in overlay without giving the packer Session or Workspace knowledge. Its plaintext JSONL logs use the persistence backend's real project/session directory layout, so Session Persistence reads them cold and Workspace Registry derives the Workspace from their /dsh/workspace headers. The main Session exceeds the Client's 50-message page and keeps representative tool results at its tail; persisted one-shot and continuable children exercise the subagent catalog. WebFS authorization and user data remain a separate provider and never share this fixture tree.
Both packages live in packages/experimental/ as @deepseek-ai/dsh-experimental-*, private and outside official releases. The boundary that carries product promises stays in the product packages: the injection table, __DSH_TRANSPORT__, and the /plugins bundle bytes are owned by dsh-host-webserver, dsh-client-modules, and dsh-client-connection.
Alternatives considered
A load-time transform as a safety net. It turned a broken image into a timing regression nobody attributed, and made "which path lowered this body" unanswerable from outside.
Contract constants inside the transform, trusting tree shaking. The transform functions did shake out, but acorn declares no sideEffects, so the barrel edge alone carried the whole parser into the worker bundle.
A separate preview template. The retired preview.html template duplicated the served document and drifted (language, title, entry wiring). Deriving the page from the built index at closeBundle removes the second document entirely.
Gating the stock entry on top-level await ordering instead of a deferred. Sibling module scripts do not wait for one another's top-level awaits; the ??=-installed deferred makes the handshake order-independent and lets a failed handshake reject into the boot page's failure rendering.
Generate example state in the Worker at startup. A preview-only Session or Workspace creation branch would bypass cold persistence loading and make the runtime own test data. Static image files exercise the same discovery and pagination path as existing user data.
Seed the example through WebFS. WebFS owns user-selected durable storage and its lifecycle. Coupling the built-in demonstration to it would make a static preview depend on browser persistence state and would blur which bytes came from the deployment.
Pack one complete base image per fixture. Full-image variants duplicate the runtime package closure and make combinations quadratic. Restricted overlays keep one immutable base, let the chooser compose zero or more data sources, and give future WebFS hydration the same pre-boot application point.
Consequences
lib/worker.jscontains no parser (423.5 kB → 246.3 kB at the time of the cut, before the shell process layer landed).diff dist/index.html dist/preview.htmlis exactly one script tag;packages/experimental/webworker-packer/tests/image-loadable.spec.tspins both halves of the loader contract, the transform semantic suite pinscreateRequirerequest discovery, andapps/web/tests/preview-boot.e2e.tspins preview usability (boot to an interactive page) in the web browser lane, replacing the retiredapps/web/scripts/preview/probe scripts.- The transform corpus imports every built bundle through Node before comparing its lowered exports. Its pinned exemptions name the actual non-importable bundle and fail when one becomes importable: after Win32 process primitives became the Koffi type owner,
win32-processcarries the duplicate-type exemption andsandbox-windows-acldoes not. - The served
<base href="/">anchor exists because relative asset URLs would resolve under the request directory on SPA-fallback paths; remove it only together with the relative build base. - The image ships as a deterministically gzip-compressed tar (
vfs-image.tar.gz; MTIME 0, OS byte 0xff): static hosts do not compress binary content types (type allowlists, CDN size caps), so the compression rides the artifact, and the worker inflates the fetch body through the browser's nativeDecompressionStreamwhile it downloads. - The preview waits at a pre-boot source chooser. Its built-in example opens a reproducible Workspace and cold Session corpus suitable for inspecting tool cards, subagent navigation, and backward pagination without credentials or model calls; the empty selection preserves first-run coverage. Fixture tests validate the physical logs through production readers, and browser acceptance verifies both selections.
中文
问题
浏览器 worker 既不能在装载期编译模块,也不能由产品 webserver 提供页面:每个模块体必须以可直接运行的形态到达,页面必须是静态产物。两个面早期都发生过漂移。装载器曾携带一个兜底编译器,于是收集器的缺口表现为「启动变慢」而不是「镜像坏了」——而且 acorn 经包 barrel 混进了 lib/worker.js,一个只包装预 lowered 模块体的运行时根本不需要解析器。preview 曾是服务页面旁的第二份 HTML 模板,一个 served index 可以悄悄漂离的页面。
决定
Lowering 只发生在 pack 期。 @deepseek-ai/dsh-experimental-webworker-packer 组合 profile、物化闭包、lower 每个 JavaScript 模块体;LOWERING_VERSION 与 WRAPPER_PARAMS 是 pack↔worker 的契约,与镜像布局的其余部分一起放在 src/image-layout.ts。装载器完全按镜像持有的形态包装模块体:仍带模块语法的模块体是一次点名镜像的拒绝,且 startWorkerHost 在挂载任何模块之前要求 manifest 的 lowered 等于本构建的契约。lowerModuleSource 是转换器唯一的面、packer 是它唯一的调用方;同一次解析会把具名静态 import、re-export 与动态 import、经 require 发起的调用,以及通过 node:module 或 module 具名导入在模块作用域直接发起的 createRequire(import.meta.url)('pkg') 调用送入可达性遍历。保存下来的结果、经 CommonJS 获取的 createRequire、计算得到的请求名称与其他基准只在运行时解析;只能通过这些形式触达的目标需要镜像入口种子。worker 图内部的 import 一律指向拥有该值的模块——绝不指向包 barrel,那正是把解析器偷运进来的那条边。源码目录排除只用于运行期使用已构建 lib/ 的 workspace 与 vendored 包;已安装第三方包会保留 src/ 和 dist/ 下的 JavaScript,因为其发布入口可能解析到这些位置。
preview 就是服务页面加一个标签。 一次 Vite 构建产出共享全部 chunk 的 dist/index.html 与 dist/preview.html;唯一差异是前插的一个引导入口,其模块负责连接 worker host。启动随之汇于一个协议:应用注入表的一方 settle __DSH_BOOT_READY__ deferred——served 渲染器在渲染完的行之后用尾部脚本 resolve,worker 引导段在首个 await 之前安装、末行生效后 settle——client 入口在读取任何注入状态前 await 它,因此从标准入口起的链路逐字就是 served 链路。插件 combo 脚本与 map 都通过 tunnel;页面侧 loader 会在执行脚本 Blob 前,把每个仅 tunnel 可达的 map 内嵌为 Base64 data URL,从而不依赖另一条 object URL 的生命周期,并在 DevTools 中保留 indexed map 的组件名称。构建使用相对 base,产物可挂载于任意静态目录;served 形态在 serve 期渲染 <base href="/"> 锚定深层 SPA fallback 路径,磁盘上的两个页面保持字节共享。
仓库 preview 携带可选择的文件系统来源。 Packer 产出一份基础镜像,并为每套具名内置 fixture 产出一份小型 overlay 归档。没有来源 query 时,preview.html 会停在选择面板,可选择空文件系统、内置 fixtures,或归另一实现所有的 WebFS provider。合法的 preview-fixture=none|<built-in-id> query 会直接选择并跳过面板,供确定性的浏览器流程使用;该独立名称避开 Client 既有的 fixture transport 开关。Worker 先挂载基础镜像,再按顺序把所选 overlays 应用到仅限 home/ 和 workspace/ 的路径,随后才校验基础 manifest 并启动 Cordis。packages/experimental/webworker-runtime/tests/fixtures/vfs-example/ 提供其中一套内置 overlay,Packer 无需理解 Session 或 Workspace。明文 JSONL 日志使用 persistence backend 的真实 project/session 目录布局,因此 Session Persistence 会冷读取它们,Workspace Registry 则根据其 /dsh/workspace header 派生 Workspace。主 Session 超过 Client 的 50-message page,并把代表性工具结果留在尾页;持久化的 one-shot 与 continuable child 用于验证 subagent catalog。WebFS 授权与用户数据仍属于独立 provider,绝不与该 fixture 共用目录。
两个包以 @deepseek-ai/dsh-experimental-* 名义放在 packages/experimental/,私有且在官方发布之外。承载产品承诺的边界仍在产品包里:注入表、__DSH_TRANSPORT__ 与 /plugins bundle 字节由 dsh-host-webserver、dsh-client-modules、dsh-client-connection 拥有。
曾考虑的替代方案
保留装载期转换器作安全网。 它把坏镜像变成无人归因的耗时回归,并且让「这个模块体是谁 lower 的」从外部不可回答。
契约常量留在转换器里,信任 tree shaking。 转换函数确实被摇掉了,但 acorn 未声明 sideEffects,仅 barrel 一条边就把整个解析器带进了 worker bundle。
独立的 preview 模板。 已退役的 preview.html 模板复制了服务文档并发生漂移(语言、标题、入口接线)。在 closeBundle 从 built index 派生页面则彻底消灭了第二份文档。
用顶层 await 顺序而非 deferred 去闸标准入口。 兄弟 module script 互不等待对方的顶层 await;??= 安装的 deferred 使握手与求值顺序无关,且失败的握手能 reject 进 boot 页的失败呈现。
在 Worker 启动时生成示例状态。 Preview 专用的 Session 或 Workspace 创建分支会绕过冷 persistence 读取,还会让 runtime 拥有测试数据。静态镜像文件与既有用户数据经过相同的发现和分页路径。
通过 WebFS 注入示例。 WebFS 拥有用户选定的 durable storage 及其生命周期。让内置演示依赖它,会使静态 preview 受浏览器持久化状态影响,并模糊哪些字节来自部署。
每套 fixture 各打一份完整基础镜像。 完整镜像变体会重复 runtime package closure,并使组合数量平方增长。受限 overlay 只保留一个不可变基础镜像,选择面板可组合零到多个数据源,未来 WebFS 水合也能复用同一个 pre-boot 应用点。
后果
lib/worker.js不含解析器(当刀落时为 423.5 kB → 246.3 kB,早于 shell 进程层落地)。diff dist/index.html dist/preview.html恰为一个 script 标签;packages/experimental/webworker-packer/tests/image-loadable.spec.ts钉住装载器契约的两半,transform 语义套件钉住createRequire请求发现,apps/web/tests/preview-boot.e2e.ts则在 web 浏览器车道钉住 preview 可用性(boot 到可交互页面),替代已撤编的apps/web/scripts/preview/探针脚本。- 转换 corpus 会先通过 Node 导入每个已构建 bundle,再比较 lowered export。固定豁免会点名真正不可导入的 bundle,并在其恢复可导入时失败:
win32-process是 Koffi 类型 owner 并承担重复类型豁免;sandbox-windows-acl可正常导入,不承担该豁免。 - served 的
<base href="/">锚存在的原因是:相对资产 URL 在 SPA fallback 深路径下会解析进请求目录;只有与相对构建 base 一起才可移除它。 - 镜像以确定性 gzip 压缩的 tar 交付(
vfs-image.tar.gz;MTIME 0、OS 字节 0xff):静态托管不压缩二进制 content-type(类型白名单、CDN 尺寸帽),压缩必须随制品走;worker 用浏览器原生DecompressionStream在下载的同时解压 fetch body。 - Preview 会停在 pre-boot 来源选择面板。内置示例提供可复现的 Workspace 与冷 Session 语料,无凭据、零模型调用即可检查工具卡、subagent 导航和向前分页;空白选项保留首次启动覆盖。Fixture 测试通过生产 reader 校验物理日志,浏览器验收同时验证两种选择。