@deepseek-ai/dsh-client-web 0.1.5-rc.2 → 0.1.6-alpha.2

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/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/web/README.md
5
- README.md: e99c83af3a0226ff4091509c300bd8e7d085a25a
6
- README.zh.md: 9a15ca0df65ac7fffe78f5fed49dd08281260b2b
5
+ README.md: 2cedd1748973e13ad1b9a3b0ef89c0101d4ffb09
6
+ README.zh.md: 882c338339b725f82aae4d931ab85a6d14650b42
package/README.md CHANGED
@@ -27,6 +27,8 @@ English | [中文](README.zh.md)
27
27
 
28
28
  Use it when you assemble the browser application: `apps/web`'s Vite entry runs `new AppWebEntry(container).run()` against the mount point, and the boot page carries the user through activation. Ordinary browser callers pass no options. A pre-injected page transport is the default ahead of the `seams` override: when `globalThis.__DSH_TRANSPORT__` carries `loadBundle`, the module stage adopts it as the bundle transport and skips the immediate-tier HTTP prefetch, while explicit `seams` still win (for example jsdom tests, where external `<script>` execution cannot reach the page context).
29
29
 
30
+ Static application pages install `__DSH_BOOT_READY__` before the entry runs. The boot page renders immediately while `run()` waits; the page owner applies the Host rows with `applyIndexInjections` (also exported from `./injections`) and resolves the deferred after all scripts finish. A rejected deferred renders a boot failure unless the caller supplies `run(onFailure)` to present the error externally while retaining the loading page. Desktop uses this callback to request native recovery. Desktop and WebWorker share the injection interpreter; server-side `tapIndex` HTML transforms apply only to served documents.
31
+
30
32
  The shell base styles apply automatic CJK/Latin spacing to ordinary content in supporting browsers. Semantic code and terminal, diff, read, and search output containers retain literal source spacing and column alignment; browsers without `text-autospace` support ignore both declarations.
31
33
 
32
34
  ### What boot looks like
@@ -35,7 +37,7 @@ Boot runs in two stages: the module stage adopts the parser-loaded bootstrap bat
35
37
 
36
38
  ### The boot page
37
39
 
38
- The boot page uses plain DOM and local CSS, so bundle and plugin-activation failures remain visible: it shows one spinner node whose CSS arc grows as entries activate, and reports per-entry status. The spinner and its animation phase persist until the full UI replaces the boot page. A plugin that fails import or activation is reported by name with the reason (missing service, import error, or state) instead of a blank page.
40
+ The boot page uses plain DOM and local CSS, so bundle and plugin-activation failures remain visible: it shows one spinner node whose CSS arc grows as entries activate, and reports per-entry status. The spinner and its animation phase persist until the full UI replaces the boot page. A plugin that fails import or activation is reported by name with the reason (missing service, import failure, or state) instead of a blank page. The console contains the original import error.
39
41
 
40
42
  ### The shared module table
41
43
 
@@ -67,12 +69,16 @@ The kernel owns exactly three things: the module system, the Cordis Loader, and
67
69
 
68
70
  The boot page is plain DOM with local CSS whose fallback fonts and colors match the theme tokens that arrive during loading. `internal/status` events drive one spinner node and per-entry labels; hydration preserves the node and animation phase through the application commit, and `fail()` renders the thrown reason. React mounting, slot rendering, and assembly live in `ui-renderer`; `ui-layout` owns the assembled browser-title projection.
69
71
 
72
+ The boot kernel delegates manifest entry creation to Client Modules so live graph synchronization owns the same entry identities after startup. The initial activation audit remains strict; later page-local failures appear in Settings → Plugins → Plugin list.
73
+
70
74
  ### Source map
71
75
 
72
76
  | File | Role |
73
77
  |---|---|
74
78
  | [`src/index.ts`](src/index.ts) | Library entry: `AppWebEntry`, `getStaticModules`, platform tables |
75
- | [`src/boot.ts`](src/boot.ts) | `AppWebEntry`: two-stage boot, activation audit, renderer handoff |
79
+ | [`src/boot.ts`](src/boot.ts) | `AppWebEntry`: module stage, boot page, immediate-tier prefetch, then `bootClient` + `mountClient` |
80
+ | [`src/boot-client.ts`](src/boot-client.ts) | `bootClient` / `assertEntriesActive`: Loader mount, one entry per manifest row, activation audit |
81
+ | [`src/mount.ts`](src/mount.ts) | `mountClient`: renderer handoff through a `uiRenderer` dependency fiber |
76
82
  | [`src/boot-page.ts`](src/boot-page.ts) | Framework-free boot page: spinner, per-entry status, failure rendering |
77
83
  | [`src/platform.ts`](src/platform.ts) | `PLATFORM_MODULES` / `PRELOADED_CLIENT_EXTERNALS`: the implicit external baseline |
78
84
  | [`src/seed.ts`](src/seed.ts) | Static module table handed to the loader at boot |
@@ -122,4 +128,4 @@ None.
122
128
 
123
129
  </details>
124
130
 
125
- **Runtime invariant:** No companion is published. The vite entry shell — boot glue and module-table seeding with no cordis events and no cross-plugin mutable state; the boot chain (loading page → settled → one-flip UI) is asserted by the web smoke e2e against the real carrier.
131
+ **Runtime invariant:** No companion is published. The Vite entry shell provides boot glue and module-table seeding, emits no Cordis events, and holds no cross-plugin mutable state; the boot chain (loading page → settled → one-flip UI) is verified by the web smoke e2e against the real carrier.
package/README.zh.md CHANGED
@@ -25,7 +25,9 @@ kind: "package-library"
25
25
  <a id="use-this-package"></a>
26
26
  ## 使用本包
27
27
 
28
- 组装浏览器应用时使用它:`apps/web` 的 Vite 入口对挂载点运行 `new AppWebEntry(container).run()`,启动页承载用户度过激活过程。普通浏览器调用方不传任何选项。预注入的页面传输是 `seams` 覆盖之前的默认:当 `globalThis.__DSH_TRANSPORT__` 携带 `loadBundle` 时,模块阶段将其采纳为 bundle 传输并跳过 `immediately` 层级的 HTTP 预取,而显式 `seams` 仍然优先(例如外部 `<script>` 执行无法到达页面上下文的 jsdom 测试)。
28
+ 组装浏览器应用时使用它:`apps/web` 的 Vite 入口对挂载点运行 `new AppWebEntry(container).run()`,启动页会在激活过程中向用户展示进度。普通浏览器调用方不传任何选项。默认使用预注入的页面传输,除非提供 `seams` 覆盖:当 `globalThis.__DSH_TRANSPORT__` 携带 `loadBundle` 时,模块阶段将其采纳为 bundle 传输并跳过 `immediately` 层级的 HTTP 预取,而显式 `seams` 仍然优先(例如外部 `<script>` 执行无法到达页面上下文的 jsdom 测试)。
29
+
30
+ 静态应用页面在入口运行前安装 `__DSH_BOOT_READY__`。`run()` 等待期间会立即显示启动页;页面所有者通过 `applyIndexInjections`(也从 `./injections` 导出)应用 Host 注入项,并在所有脚本完成后兑现延迟对象。延迟对象拒绝时显示启动失败;若调用方提供 `run(onFailure)`,则由外部呈现错误并保留加载页。Desktop 使用该回调请求原生恢复。Desktop 与 WebWorker 共享注入解释器;服务端 `tapIndex` HTML 转换仅适用于服务端提供的文档。
29
31
 
30
32
  外壳基础样式会在支持的浏览器中为普通内容自动添加中西文间距。语义化代码以及终端、diff、读取和搜索输出容器会保留源码中的原始间距和列对齐;不支持 `text-autospace` 的浏览器会忽略这两项声明。
31
33
 
@@ -35,11 +37,11 @@ kind: "package-library"
35
37
 
36
38
  ### 启动页
37
39
 
38
- 启动页只使用原生 DOM 与本地 CSS,因此 bundle 与插件激活失败保持可见:它显示一个 spinner 节点,其 CSS 圆弧随 entry 激活而增长,并逐 entry 报告状态。spinner 及其动画相位会一直保留,直到完整 UI 替换启动页。导入或激活失败的插件会按名称报告并给出原因(缺失服务、导入错误或状态),而不是白屏。
40
+ 启动页只使用原生 DOM 与本地 CSS,因此 bundle 与插件激活失败保持可见:它显示一个 spinner 节点,其 CSS 圆弧随 entry 激活而增长,并逐 entry 报告状态。spinner 及其动画相位会一直保留,直到完整 UI 替换启动页。导入或激活失败的插件会按名称报告并给出原因(缺失服务、导入失败或状态),而不是白屏。控制台包含原始导入错误。
39
41
 
40
42
  ### 共享模块表
41
43
 
42
- `PLATFORM_MODULES`(位于 `src/platform.ts`)列出外壳播种的共享模块——React、Cordis 与静态 UI 库——并与 `PRELOADED_CLIENT_EXTERNALS`(parser 预载的 runtime 行)一起定义每个动态 bundle 解析所依据的隐式 external 基座。`dsh.client.external` 只添加基座之外的精确请求;参见[共享模块与模块图](../AGENTS.md#shared-modules-and-the-module-graph)。
44
+ `PLATFORM_MODULES`(位于 `src/platform.ts`)列出外壳预置的共享模块——React、Cordis 与静态 UI 库——并与 `PRELOADED_CLIENT_EXTERNALS`(parser 预载的运行时行)一起定义每个动态 bundle 解析所依据的隐式 external 基座。`dsh.client.external` 只添加基座之外的精确请求;参见[共享模块与模块图](../AGENTS.md#shared-modules-and-the-module-graph)。
43
45
 
44
46
  ### 配置
45
47
 
@@ -57,22 +59,26 @@ kind: "package-library"
57
59
 
58
60
  ### 设计理念
59
61
 
60
- 内核恰好拥有三样东西:模块系统、Cordis Loader 与启动页。Graph、批次 preload 与 loader facade 归 Host 所有,因此 `AppWebEntry` 永不感知 bootstrap package id,也不解析协议格式。动态 UI 渲染器只在每个客户端 entry 激活后收到挂载点。
62
+ 内核恰好拥有三样东西:模块系统、Cordis Loader 与启动页。Graph、批次 preload 与 loader facade 归 Host 所有,因此 `AppWebEntry` 永不感知 bootstrap 包 id,也不解析协议格式(wire format)。动态 UI 渲染器只在每个客户端 entry 激活后收到挂载点。
61
63
 
62
64
  ### 两阶段启动
63
65
 
64
- `run()` 调用 Host 安装的 `window.__ModuleLoader__.create({ boot, staticModules, ...seams })`;facade 接纳 parser 已加载的 bootstrap 批次后返回构造好的模块系统与已解析 manifest。模块阶段通过一个共享的 application 批次 URL 预取 `immediately` 层级。插件阶段挂载 Loader、把 `loader.internal` 赋为 `modules`、统一创建全部图 entry、等待完全停稳,然后审计激活:任何导入失败、因缺失服务而 pending,或落入其他非 active 状态的 entry,都会抛出一个聚合错误,点名每个失败 entry。
66
+ `run()` 调用 Host 安装的 `window.__ModuleLoader__.create({ boot, staticModules, ...seams })`;facade 接纳 parser 已加载的 bootstrap 批次后返回构造好的模块系统与已解析 manifest(元数据清单)。模块阶段通过一个共享的 application 批次 URL 预取 `immediately` 层级。插件阶段挂载 Loader、把 `loader.internal` 赋为 `modules`、统一创建全部图 entry、等待完全停稳,然后审计激活:任何导入失败、因缺失服务而 pending,或落入其他非 active 状态的 entry,都会抛出一个聚合错误,点名每个失败 entry。
65
67
 
66
68
  ### 启动页机制
67
69
 
68
70
  启动页是原生 DOM 加本地 CSS,其回退字体与颜色匹配加载期间到达的主题 token。`internal/status` 事件驱动一个 spinner 节点与逐 entry 标签;hydrate 会保留该节点与动画相位直到应用提交,`fail()` 渲染抛出的原因。React 挂载、slot 渲染与应用组装位于 `ui-renderer`;`ui-layout` 拥有组装后的浏览器标题投影。
69
71
 
72
+ 启动内核把清单条目创建交给 Client Modules,使启动后的动态图同步继续持有相同的条目身份。初始激活审计仍然严格;后续页面本地失败显示在「设置 → 插件 → 插件列表」。
73
+
70
74
  ### 源码地图
71
75
 
72
76
  | 文件 | 职责 |
73
77
  |---|---|
74
78
  | [`src/index.ts`](src/index.ts) | 库入口:`AppWebEntry`、`getStaticModules`、平台表 |
75
- | [`src/boot.ts`](src/boot.ts) | `AppWebEntry`:两阶段启动、激活审计、渲染器交接 |
79
+ | [`src/boot.ts`](src/boot.ts) | `AppWebEntry`:模块阶段、启动页、immediately 层级预取,随后调用 `bootClient` + `mountClient` |
80
+ | [`src/boot-client.ts`](src/boot-client.ts) | `bootClient` / `assertEntriesActive`:挂载 Loader、每个 manifest 行一个 entry、激活审计 |
81
+ | [`src/mount.ts`](src/mount.ts) | `mountClient`:经 `uiRenderer` 依赖 fiber 完成渲染器交接 |
76
82
  | [`src/boot-page.ts`](src/boot-page.ts) | 无框架启动页:spinner、逐 entry 状态、失败渲染 |
77
83
  | [`src/platform.ts`](src/platform.ts) | `PLATFORM_MODULES` / `PRELOADED_CLIENT_EXTERNALS`:隐式 external 基座 |
78
84
  | [`src/seed.ts`](src/seed.ts) | 启动时交给 loader 的静态模块表 |
@@ -110,7 +116,7 @@ kind: "package-library"
110
116
 
111
117
  这些限制说明启动内核不支持什么。它们是当前包约束,不是任务积压。
112
118
 
113
- - **应用会等待完整名册**——只要一个 entry 失败,无框架启动页就会保留并逐项报告;不支持部分 UI 可用。
119
+ - **应用会等待全部 entry 就绪**——只要一个 entry 失败,无框架启动页就会保留并逐项报告;不支持部分 UI 可用。
114
120
 
115
121
  <a id="dev-note"></a>
116
122
  ### 开发备注
@@ -122,4 +128,4 @@ kind: "package-library"
122
128
 
123
129
  </details>
124
130
 
125
- **运行时不变式:** 不发布伴生入口。这是 Vite entry shell,只负责 boot glue 与 module-table seeding,不发出 Cordis 事件或持有跨插件可变状态;boot chain 由真实 carrier 的 web smoke e2e 覆盖。
131
+ **运行时不变式:** 不发布伴生入口。这是 Vite entry shell,只负责 boot glue 与 module-table seeding,不发出 Cordis 事件或持有跨插件可变状态;boot chain(加载页 → 启动就绪 → 一次切换至 UI)由真实 carrier 上的 web e2e 冒烟测试验证。
@@ -0,0 +1,40 @@
1
+ //#region lib/types/apply-injections.js
2
+ function assertNever(row) {
3
+ throw new Error(`web boot: unknown index injection row ${JSON.stringify(row)}`);
4
+ }
5
+ /**
6
+ * Execute every row in table order.
7
+ * @param rows - Injection table from the boot payload.
8
+ * @param loadScript - Executes one script-src row through the page owner's asset transport.
9
+ */
10
+ async function applyIndexInjections(rows, loadScript) {
11
+ for (const row of rows) switch (row.kind) {
12
+ case "global":
13
+ globalThis[row.name] = row.value;
14
+ break;
15
+ case "script": {
16
+ const el = document.createElement("script");
17
+ el.textContent = row.text;
18
+ (row.placement === "head" ? document.head : document.body).append(el);
19
+ break;
20
+ }
21
+ case "script-src":
22
+ await loadScript(row.src);
23
+ break;
24
+ case "script-preload": break;
25
+ case "style": {
26
+ const el = document.createElement("style");
27
+ el.textContent = row.text;
28
+ document.head.append(el);
29
+ break;
30
+ }
31
+ case "html":
32
+ (row.placement === "head" ? document.head : document.body).insertAdjacentHTML("beforeend", row.html);
33
+ break;
34
+ default: assertNever(row);
35
+ }
36
+ }
37
+ //#endregion
38
+ export { applyIndexInjections };
39
+
40
+ //# sourceMappingURL=apply-injections.js.map
package/lib/base.css CHANGED
@@ -32,6 +32,13 @@ body {
32
32
  text-autospace: normal;
33
33
  }
34
34
 
35
+ /* macOS desktop (the Electron preload sets data-platform): the window's
36
+ sidebar vibrancy shows only through a transparent page background. */
37
+ html[data-platform='darwin'],
38
+ html[data-platform='darwin'] body {
39
+ background: transparent;
40
+ }
41
+
35
42
  code,
36
43
  pre,
37
44
  [data-diff],
@@ -1,7 +1,6 @@
1
1
  /* The framework-free boot page cannot depend on theme delivery succeeding. */
2
2
 
3
3
  .boot {
4
- --dsh-boot-bg: #fff;
5
4
  --dsh-boot-label-primary: #0f1115;
6
5
  --dsh-boot-label-secondary: #61666b;
7
6
  --dsh-boot-label-tertiary: #81858c;
@@ -11,11 +10,10 @@
11
10
  height: 100%;
12
11
  display: grid;
13
12
  place-items: center;
14
- background: var(--dsw-alias-bg-base, var(--dsh-boot-bg));
13
+ background: var(--dsw-alias-bg-base, var(--dsh-boot-bg, Canvas));
15
14
  }
16
15
 
17
16
  :global(body[data-ds-dark-theme]) .boot {
18
- --dsh-boot-bg: #151517;
19
17
  --dsh-boot-label-primary: #f9fafb;
20
18
  --dsh-boot-label-secondary: #cfd3d6;
21
19
  --dsh-boot-label-tertiary: #adb2b8;
package/lib/index.js CHANGED
@@ -11,6 +11,80 @@ import * as UiSlots from "@deepseek-ai/dsh-client-ui-slots";
11
11
  import * as UiPrimitives from "@deepseek-ai/dsh-client-ui-primitives";
12
12
  import * as UiDockkit from "@deepseek-ai/dsh-client-ui-dockkit";
13
13
  import "./base.css";
14
+ //#region lib/types/loader-status.js
15
+ /**
16
+ * Value mirror of cordis's `FiberState` const enum: a const enum has no
17
+ * runtime object to import (and esbuild-based pipelines cannot inline it
18
+ * across modules), so these values mirror the pinned vendored definition
19
+ * while retaining its type (same rationale as dsh-tool-cordis's mirror).
20
+ */
21
+ const FIBER_STATE = {
22
+ PENDING: 0,
23
+ LOADING: 1,
24
+ ACTIVE: 2,
25
+ FAILED: 3,
26
+ DISPOSED: 4,
27
+ UNLOADING: 5
28
+ };
29
+ /** Label for each fiber state, keyed by member (inlining-safe — no reverse mapping). */
30
+ const STATE_LABELS = {
31
+ [FIBER_STATE.PENDING]: "pending",
32
+ [FIBER_STATE.LOADING]: "loading",
33
+ [FIBER_STATE.ACTIVE]: "active",
34
+ [FIBER_STATE.FAILED]: "failed",
35
+ [FIBER_STATE.DISPOSED]: "disposed",
36
+ [FIBER_STATE.UNLOADING]: "unloading"
37
+ };
38
+ //#endregion
39
+ //#region lib/types/boot-client.js
40
+ /**
41
+ * Compose the client: `ctx.plugin(Loader)`, `loader.internal = modules`, one
42
+ * `loader.create({ name })` per manifest row, `loader.await()`, then
43
+ * {@link assertEntriesActive}. A row whose module cannot be imported is marked
44
+ * failed; the Loader logs its import error and the audit rejects startup.
45
+ * @param options - context, module system, manifest, optional progress sink.
46
+ * @returns resolves after every entry is active; rejects with the audit report otherwise.
47
+ */
48
+ async function bootClient(options) {
49
+ const { ctx, manifest, onEntryState } = options;
50
+ await ctx.plugin(Loader);
51
+ const loader = ctx.loader;
52
+ loader.internal = options.modules;
53
+ ctx.on("internal/status", (fiber) => {
54
+ const entry = fiber.entry;
55
+ if (entry === void 0 || entry.fiber === void 0) return;
56
+ onEntryState?.(entry.options.name, STATE_LABELS[entry.fiber.state]);
57
+ });
58
+ const rows = manifest.plugins.map((row) => row.id);
59
+ for (const name of rows) onEntryState?.(name, "loading");
60
+ await options.modules.entries.start(loader, manifest);
61
+ for (const entry of loader.entries()) if (entry.fiber === void 0) onEntryState?.(entry.options.name, "failed");
62
+ await loader.await();
63
+ assertEntriesActive(ctx);
64
+ }
65
+ /**
66
+ * Reject entries that failed import/apply or still wait on missing services.
67
+ * @param ctx - root Context carrying the Loader.
68
+ * @throws {Error} listing every non-active entry with its reason.
69
+ */
70
+ function assertEntriesActive(ctx) {
71
+ const failures = [];
72
+ for (const entry of ctx.loader.entries()) {
73
+ const name = entry.options.name;
74
+ if (entry.fiber === void 0) {
75
+ failures.push(`${name}: import failed (see console for the import error)`);
76
+ continue;
77
+ }
78
+ const state = STATE_LABELS[entry.fiber.state];
79
+ if (state === "active") continue;
80
+ if (state === "pending") {
81
+ const missing = Object.keys(entry.fiber.inject).filter((service) => ctx.get(service) === void 0);
82
+ failures.push(`${name}: pending (waiting for service${missing.length === 1 ? "" : "s"}: ${missing.join(", ") || "unknown"})`);
83
+ } else failures.push(`${name}: ${state}`);
84
+ }
85
+ if (failures.length > 0) throw new Error(`web boot: ${String(failures.length)} entr${failures.length === 1 ? "y" : "ies"} did not activate\n${failures.join("\n")}`);
86
+ }
87
+ //#endregion
14
88
  //#region lib/types/boot-page.js
15
89
  /** Create a div with one module class and optional text. */
16
90
  function div(className, text) {
@@ -98,6 +172,23 @@ var BootPage = class {
98
172
  }
99
173
  };
100
174
  //#endregion
175
+ //#region lib/types/mount.js
176
+ /**
177
+ * Mount the UI renderer into `container` through a dependency fiber on
178
+ * `uiRenderer`: the mount effect installs when the service is provided and
179
+ * reinstalls when it is replaced.
180
+ * @param ctx - booted root Context.
181
+ * @param container - application mount point.
182
+ * @returns resolves once the dependency fiber exists; with `uiRenderer`
183
+ * already provided (as after `bootClient`) the mount effect is installed by
184
+ * then, otherwise it installs when the service arrives.
185
+ */
186
+ async function mountClient(ctx, container) {
187
+ await ctx.inject(["uiRenderer"], (scope) => {
188
+ scope.effect(() => scope.uiRenderer.mount(container), "web boot: application mount");
189
+ });
190
+ }
191
+ //#endregion
101
192
  //#region lib/types/seed.js
102
193
  /**
103
194
  * Platform-singleton module-table. These are the ONLY entities the shell
@@ -125,35 +216,11 @@ function getStaticModules() {
125
216
  };
126
217
  }
127
218
  //#endregion
128
- //#region lib/types/loader-status.js
129
- /**
130
- * Value mirror of cordis's `FiberState` const enum: a const enum has no
131
- * runtime object to import (and esbuild-based pipelines cannot inline it
132
- * across modules), so these values mirror the pinned vendored definition
133
- * while retaining its type (same rationale as dsh-tool-cordis's mirror).
134
- */
135
- const FIBER_STATE = {
136
- PENDING: 0,
137
- LOADING: 1,
138
- ACTIVE: 2,
139
- FAILED: 3,
140
- DISPOSED: 4,
141
- UNLOADING: 5
142
- };
143
- /** Label for each fiber state, keyed by member (inlining-safe — no reverse mapping). */
144
- const STATE_LABELS = {
145
- [FIBER_STATE.PENDING]: "pending",
146
- [FIBER_STATE.LOADING]: "loading",
147
- [FIBER_STATE.ACTIVE]: "active",
148
- [FIBER_STATE.FAILED]: "failed",
149
- [FIBER_STATE.DISPOSED]: "disposed",
150
- [FIBER_STATE.UNLOADING]: "unloading"
151
- };
152
- //#endregion
153
219
  //#region lib/types/boot.js
154
220
  /**
155
221
  * Web boot kernel. It owns only the module system, Cordis loader, and a
156
- * framework-free boot page. The dynamic UI renderer receives the mount
222
+ * framework-free boot page; plugin composition and the renderer handoff are
223
+ * `bootClient` and `mountClient`. The dynamic UI renderer receives the mount
157
224
  * point after every client entry activates.
158
225
  * @module @deepseek-ai/dsh-client-web/src/boot
159
226
  */
@@ -178,9 +245,10 @@ var AppWebEntry = class {
178
245
  /**
179
246
  * Load and activate every client entry, then hand the mount point to the
180
247
  * UI renderer. Plugin failures remain visible on the boot page.
181
- * @returns Resolves after application mount or failure rendering.
248
+ * @param onFailure - Optional carrier-owned fatal presentation; keeps the boot page visible.
249
+ * @returns Resolves after application mount or failure reporting.
182
250
  */
183
- async run() {
251
+ async run(onFailure) {
184
252
  try {
185
253
  await globalThis.__DSH_BOOT_READY__?.promise;
186
254
  const win = globalThis;
@@ -197,11 +265,21 @@ var AppWebEntry = class {
197
265
  const prefetching = this.prefetchImmediateTier();
198
266
  const ctx = new Context();
199
267
  this.ctx = ctx;
200
- await this.runPluginBoot(ctx, prefetching);
201
- await this.mountApp(ctx);
268
+ this.page.setTotal(this.manifest.plugins.length);
269
+ await prefetching;
270
+ await bootClient({
271
+ ctx,
272
+ modules: this.modules,
273
+ manifest: this.manifest,
274
+ onEntryState: (name, state) => {
275
+ if (onFailure === void 0 || state !== "failed") this.page.setState(name, state);
276
+ }
277
+ });
278
+ await mountClient(ctx, this.container);
202
279
  } catch (reason) {
203
280
  console.error(reason);
204
- this.page.fail(reason instanceof Error ? reason.message : String(reason));
281
+ if (onFailure !== void 0) onFailure(reason);
282
+ else this.page.fail(reason instanceof Error ? reason.message : String(reason));
205
283
  }
206
284
  }
207
285
  /** Dispose the client plugin tree and whichever page owns the mount point. */
@@ -211,55 +289,10 @@ var AppWebEntry = class {
211
289
  if (ctx !== void 0) await ctx.fiber.dispose();
212
290
  this.page.dispose();
213
291
  }
214
- /** Mount through a dependency fiber so replacing uiRenderer remounts the application. */
215
- async mountApp(ctx) {
216
- await ctx.inject(["uiRenderer"], (scope) => {
217
- scope.effect(() => scope.uiRenderer.mount(this.container), "web boot: application mount");
218
- });
219
- }
220
292
  /** Prefetch stage-one bundles and their dynamic requests before concurrent plugin imports. */
221
293
  async prefetchImmediateTier() {
222
294
  await Promise.all(this.manifest.plugins.filter((row) => row.immediately).map((row) => this.modules.prefetch(row.id).catch((_prefetchError) => {})));
223
295
  }
224
- /** Mount the Loader, create all graph entries, await quiescence, and audit activation. */
225
- async runPluginBoot(ctx, prefetching) {
226
- await ctx.plugin(Loader);
227
- const loader = ctx.loader;
228
- loader.internal = this.modules;
229
- ctx.on("internal/status", (fiber) => {
230
- const entry = fiber.entry;
231
- if (entry === void 0 || entry.fiber === void 0) return;
232
- this.page.setState(entry.options.name, STATE_LABELS[entry.fiber.state]);
233
- });
234
- const rows = this.manifest.plugins.map((row) => row.id);
235
- this.page.setTotal(rows.length);
236
- await prefetching;
237
- await Promise.all(rows.map(async (name) => {
238
- this.page.setState(name, "loading");
239
- const id = await loader.create({ name });
240
- if (loader.resolve(id).fiber === void 0) this.page.setState(name, "failed");
241
- }));
242
- await loader.await();
243
- this.assertEntriesActive(ctx);
244
- }
245
- /** Reject entries that failed import/apply or still wait on missing services. */
246
- assertEntriesActive(ctx) {
247
- const failures = [];
248
- for (const entry of ctx.loader.entries()) {
249
- const name = entry.options.name;
250
- if (entry.fiber === void 0) {
251
- failures.push(`${name}: import failed (see console for the import error)`);
252
- continue;
253
- }
254
- const state = STATE_LABELS[entry.fiber.state];
255
- if (state === "active") continue;
256
- if (state === "pending") {
257
- const missing = Object.keys(entry.fiber.inject).filter((service) => ctx.get(service) === void 0);
258
- failures.push(`${name}: pending (waiting for service${missing.length === 1 ? "" : "s"}: ${missing.join(", ") || "unknown"})`);
259
- } else failures.push(`${name}: ${state}`);
260
- }
261
- if (failures.length > 0) throw new Error(`web boot: ${String(failures.length)} entr${failures.length === 1 ? "y" : "ies"} did not activate\n${failures.join("\n")}`);
262
- }
263
296
  };
264
297
  //#endregion
265
298
  //#region lib/types/platform.js
@@ -283,6 +316,43 @@ const PLATFORM_MODULES = [
283
316
  /** Client-bundle specifiers whose factories the parser preloads before the shell starts. */
284
317
  const PRELOADED_CLIENT_EXTERNALS = [];
285
318
  //#endregion
286
- export { AppWebEntry, PLATFORM_MODULES, PRELOADED_CLIENT_EXTERNALS, getStaticModules };
319
+ //#region lib/types/apply-injections.js
320
+ function assertNever(row) {
321
+ throw new Error(`web boot: unknown index injection row ${JSON.stringify(row)}`);
322
+ }
323
+ /**
324
+ * Execute every row in table order.
325
+ * @param rows - Injection table from the boot payload.
326
+ * @param loadScript - Executes one script-src row through the page owner's asset transport.
327
+ */
328
+ async function applyIndexInjections(rows, loadScript) {
329
+ for (const row of rows) switch (row.kind) {
330
+ case "global":
331
+ globalThis[row.name] = row.value;
332
+ break;
333
+ case "script": {
334
+ const el = document.createElement("script");
335
+ el.textContent = row.text;
336
+ (row.placement === "head" ? document.head : document.body).append(el);
337
+ break;
338
+ }
339
+ case "script-src":
340
+ await loadScript(row.src);
341
+ break;
342
+ case "script-preload": break;
343
+ case "style": {
344
+ const el = document.createElement("style");
345
+ el.textContent = row.text;
346
+ document.head.append(el);
347
+ break;
348
+ }
349
+ case "html":
350
+ (row.placement === "head" ? document.head : document.body).insertAdjacentHTML("beforeend", row.html);
351
+ break;
352
+ default: assertNever(row);
353
+ }
354
+ }
355
+ //#endregion
356
+ export { AppWebEntry, PLATFORM_MODULES, PRELOADED_CLIENT_EXTERNALS, applyIndexInjections, getStaticModules };
287
357
 
288
358
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Page-side interpreter for the structured index injection table. The served
3
+ * form renders the same rows into index.html text; a static worker page has
4
+ * no served HTML, so it executes the table directly. Rows execute strictly in
5
+ * table order, so a global row lands before the scripts that read it.
6
+ */
7
+ import type { IndexInjection } from '@deepseek-ai/dsh-host-webserver';
8
+ /**
9
+ * Execute every row in table order.
10
+ * @param rows - Injection table from the boot payload.
11
+ * @param loadScript - Executes one script-src row through the page owner's asset transport.
12
+ */
13
+ export declare function applyIndexInjections(rows: readonly IndexInjection[], loadScript: (src: string) => Promise<void>): Promise<void>;
14
+ //# sourceMappingURL=apply-injections.d.ts.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Production client composition without the page: mount the Loader over a
3
+ * module system, create every manifest row, wait for quiescence, and audit
4
+ * activation. `AppWebEntry` and the whole-client test carrier both call it.
5
+ * @module @deepseek-ai/dsh-client-web/src/boot-client
6
+ */
7
+ import type { Context } from '@deepseek-ai/cordis';
8
+ import type { BootManifest, ClientModuleLoader } from '@deepseek-ai/dsh-client-modules/client';
9
+ import { STATE_LABELS } from './loader-status.ts';
10
+ /** Entry state label as the boot page renders it. */
11
+ export type EntryStateLabel = (typeof STATE_LABELS)[keyof typeof STATE_LABELS] | 'loading' | 'failed';
12
+ /** Inputs of {@link bootClient}. */
13
+ export interface ClientBootOptions {
14
+ /** Fresh root Context that will own the plugin tree. */
15
+ readonly ctx: Context;
16
+ /** Module system installed as `loader.internal`. */
17
+ readonly modules: ClientModuleLoader;
18
+ /** Parsed manifest whose `plugins` rows become Loader entries (entry name = row id). */
19
+ readonly manifest: BootManifest;
20
+ /** Per-entry state reporting (the boot page); omitted when no one renders progress. */
21
+ readonly onEntryState?: (name: string, state: EntryStateLabel) => void;
22
+ }
23
+ /**
24
+ * Compose the client: `ctx.plugin(Loader)`, `loader.internal = modules`, one
25
+ * `loader.create({ name })` per manifest row, `loader.await()`, then
26
+ * {@link assertEntriesActive}. A row whose module cannot be imported is marked
27
+ * failed; the Loader logs its import error and the audit rejects startup.
28
+ * @param options - context, module system, manifest, optional progress sink.
29
+ * @returns resolves after every entry is active; rejects with the audit report otherwise.
30
+ */
31
+ export declare function bootClient(options: ClientBootOptions): Promise<void>;
32
+ /**
33
+ * Reject entries that failed import/apply or still wait on missing services.
34
+ * @param ctx - root Context carrying the Loader.
35
+ * @throws {Error} listing every non-active entry with its reason.
36
+ */
37
+ export declare function assertEntriesActive(ctx: Context): void;
38
+ //# sourceMappingURL=boot-client.d.ts.map
@@ -19,18 +19,13 @@ export declare class AppWebEntry {
19
19
  /**
20
20
  * Load and activate every client entry, then hand the mount point to the
21
21
  * UI renderer. Plugin failures remain visible on the boot page.
22
- * @returns Resolves after application mount or failure rendering.
22
+ * @param onFailure - Optional carrier-owned fatal presentation; keeps the boot page visible.
23
+ * @returns Resolves after application mount or failure reporting.
23
24
  */
24
- run(): Promise<void>;
25
+ run(onFailure?: (reason: unknown) => void): Promise<void>;
25
26
  /** Dispose the client plugin tree and whichever page owns the mount point. */
26
27
  dispose(): Promise<void>;
27
- /** Mount through a dependency fiber so replacing uiRenderer remounts the application. */
28
- private mountApp;
29
28
  /** Prefetch stage-one bundles and their dynamic requests before concurrent plugin imports. */
30
29
  private prefetchImmediateTier;
31
- /** Mount the Loader, create all graph entries, await quiescence, and audit activation. */
32
- private runPluginBoot;
33
- /** Reject entries that failed import/apply or still wait on missing services. */
34
- private assertEntriesActive;
35
30
  }
36
31
  //# sourceMappingURL=boot.d.ts.map
@@ -8,4 +8,5 @@
8
8
  export { AppWebEntry, type BootSeams } from './boot.ts';
9
9
  export { getStaticModules } from './seed.ts';
10
10
  export { PLATFORM_MODULES, PRELOADED_CLIENT_EXTERNALS, type PlatformModule } from './platform.ts';
11
+ export { applyIndexInjections } from './apply-injections.ts';
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Application mount through a dependency fiber, so replacing `uiRenderer`
3
+ * remounts the application. Shared by `AppWebEntry` and the test carrier.
4
+ * @module @deepseek-ai/dsh-client-web/src/mount
5
+ */
6
+ import type { Context } from '@deepseek-ai/cordis';
7
+ /**
8
+ * Mount the UI renderer into `container` through a dependency fiber on
9
+ * `uiRenderer`: the mount effect installs when the service is provided and
10
+ * reinstalls when it is replaced.
11
+ * @param ctx - booted root Context.
12
+ * @param container - application mount point.
13
+ * @returns resolves once the dependency fiber exists; with `uiRenderer`
14
+ * already provided (as after `bootClient`) the mount effect is installed by
15
+ * then, otherwise it installs when the service arrives.
16
+ */
17
+ export declare function mountClient(ctx: Context, container: HTMLElement): Promise<void>;
18
+ //# sourceMappingURL=mount.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-web",
3
3
  "description": "Web boot kernel: static module table, Cordis loader, framework-free boot page, and UI-renderer handoff",
4
- "version": "0.1.5-rc.2",
4
+ "version": "0.1.6-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -19,7 +19,11 @@
19
19
  "default": "./lib/index.js"
20
20
  },
21
21
  "./src/*": "./src/*",
22
- "./package.json": "./package.json"
22
+ "./package.json": "./package.json",
23
+ "./injections": {
24
+ "types": "./lib/types/apply-injections.d.ts",
25
+ "default": "./lib/apply-injections.js"
26
+ }
23
27
  },
24
28
  "license": "MIT",
25
29
  "devDependencies": {
@@ -29,13 +33,14 @@
29
33
  "react-dom": "^18.2.0",
30
34
  "typescript": "^6.0.3",
31
35
  "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
32
- "@deepseek-ai/dsh-client-store": "^0.1.5-rc.2",
33
- "@deepseek-ai/dsh-client-ui-dockkit": "^0.1.5-rc.2",
34
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
35
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
36
- "@deepseek-ai/dsh-client-modules": "^0.1.5-rc.2",
37
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
38
- "@deepseek-ai/cordis": "^4.0.2"
36
+ "@deepseek-ai/dsh-client-modules": "^0.1.6-alpha.2",
37
+ "@deepseek-ai/dsh-client-store": "^0.1.6-alpha.2",
38
+ "@deepseek-ai/dsh-client-ui-dockkit": "^0.1.6-alpha.2",
39
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.6-alpha.2",
40
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.6-alpha.2",
41
+ "@deepseek-ai/cordis": "^4.0.2",
42
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.6-alpha.2",
43
+ "@deepseek-ai/dsh-host-webserver": "^0.1.6-alpha.2"
39
44
  },
40
45
  "peerDependencies": {
41
46
  "@deepseek-ai/cordis": "^4.0.2"
@@ -43,6 +48,7 @@
43
48
  "files": [
44
49
  "lib/index.js",
45
50
  "lib/**/*.css",
51
+ "lib/apply-injections.js",
46
52
  "lib/types/**/*.d.ts"
47
53
  ]
48
54
  }