@deepseek-ai/dsh-host-webserver 0.1.0-rc.8 → 0.1.1-rc.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/host/webserver/README.md
5
- README.md: 7927c791a809ad272edce1310b3c50bbe30720a5
6
- README.zh.md: 82252adb3945f22c83abddbd3bbdd11556383add
5
+ README.md: b6424262305b062f4c57774c446ac1d50e490a03
6
+ README.zh.md: ea5fd0129c121b227e4d8dbd0a8dcc162acc5f7b
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [中文](README.zh.md)
4
4
 
5
- Web HTTP and upgrade-route registration plugin (default-exported `WebServer`, config `{host, port}`): a `node:http` server that listens on activation and provides `ctx.webServer`. `register(route)` adds a named `exact`/`prefix` HTTP route; `registerUpgrade(route)` adds an upgrade route for an exact pathname. A duplicate path within either table throws because route patterns are a composition-level contract and a collision is a misconfiguration; both methods return a disposer that removes the registration. `registerFallback(handler)` registers the one handler for requests that match no named route. A second registration throws; the SPA dist server [`dsh-host-frontend-static`](../frontend-static/README.md) is the shipped owner, and the server returns 404 while none is registered. `tapIndex(transform)` adds an index.html transform, and `applyIndexTaps(html)` runs a body through the registered transforms in order; the fallback handler calls it on every index response. `port` reads the listening port (the OS-assigned value when `port` is 0), and `host` reads the configured bind host (composition-time facts other plugins adapt to, e.g. the directory-picker chooser). HTTP match order is fixed: exact over the whole table, then longest prefix, then the fallback handler. Upgrades match exactly and unmatched connections are closed; registration order carries no request-facing semantics.
5
+ Web HTTP and upgrade-route registration plugin (default-exported `WebServer`, config `{host, port}`): a `node:http` server that listens on activation and provides `ctx.webServer`. `register(route)` adds a named `exact`/`prefix` HTTP route; `registerUpgrade(route)` adds an upgrade route for an exact pathname. A duplicate path within either table throws because route patterns are a composition-level contract and a collision is a misconfiguration; both methods return a disposer that removes the registration. `registerFallback(handler)` registers the one handler for requests that match no named route. A second registration throws; the SPA dist server [`dsh-host-frontend-static`](../frontend-static/README.md) is the shipped owner, and the server returns 404 while none is registered. Index startup inputs are structured rows: `collectIndexInjections()` gathers a fresh `IndexInjection` table over one `webserver/index-inject` emit per call, and `renderIndex(html)` renders the rows into an index.html body before applying the raw `tapIndex(transform)` transforms in registration order (`applyIndexTaps(html)`, the escape hatch for markup no row expresses); the fallback handler calls `renderIndex` on every index response, and a static deployment ships the same rows over its boot payload, rendering with the exported `renderIndexInjections`. `port` reads the listening port (the OS-assigned value when `port` is 0), and `host` reads the configured bind host (composition-time facts other plugins adapt to, e.g. the directory-picker chooser). HTTP match order is fixed: exact over the whole table, then longest prefix, then the fallback handler. Upgrades match exactly and unmatched connections are closed; registration order carries no request-facing semantics.
6
6
 
7
7
  The package knows no harness concepts and serves no files: the `/api` HTTP bridge and downlink WebSockets are routes owned by the connection plugin, plugin bundles and the HMR event stream are routes owned by the modules/hmr plugins, and dist serving belongs to the fallback owner. The upgrade handler owns the protocol handshake and connection contents; the webserver only delivers the raw socket and request. `host` accepts only `127.0.0.1` (default posture) and `0.0.0.0` (deliberate network exposure). This server serves browsers only; Electron loads dist over `file://` and carries fetch over an IPC bridge. This package never prints; the URL line belongs to the shell.
8
8
 
package/README.zh.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
5
- Web HTTP 与 upgrade route 注册插件(默认导出 `WebServer`,配置为 `{host, port}`):一个在激活时开始监听的 `node:http` 服务器,提供 `ctx.webServer`。`register(route)` 添加具名的 `exact`/`prefix` HTTP route;`registerUpgrade(route)` 添加精确 pathname 的 upgrade route;同一张表内的重复路径会抛错,因为 route 模式是组合层约定,冲突即配置错误;两者返回的 disposer 都会移除注册。`registerFallback(handler)` 注册一个 handler,处理所有未被具名 route 命中的请求。第二次注册会抛错;随附的 SPA dist 服务器 [`dsh-host-frontend-static`](../frontend-static/README.md) 是该 handler 的所有者,没有注册 handler 时服务器返回 404。`tapIndex(transform)` 添加一个 index.html 转换,`applyIndexTaps(html)` 按注册顺序对一段响应体运行已注册的转换;fallback handler 在每次 index 响应时调用它。`port` 读取正在监听的端口(当 `port` 为 0 时读取 OS 分配的值),`host` 读取配置的绑定宿主(这些是其他插件据以自适应的组合期事实,例如 directory-picker 选择器)。HTTP 匹配顺序固定不变:先在整张表中匹配精确 route,再匹配最长前缀,最后交给 fallback handler。upgrade 只做精确匹配,未命中连接直接关闭;注册顺序不影响请求处理。
5
+ Web HTTP 与 upgrade route 注册插件(默认导出 `WebServer`,配置为 `{host, port}`):一个在激活时开始监听的 `node:http` 服务器,提供 `ctx.webServer`。`register(route)` 添加具名的 `exact`/`prefix` HTTP route;`registerUpgrade(route)` 添加精确 pathname 的 upgrade route;同一张表内的重复路径会抛错,因为 route 模式是组合层约定,冲突即配置错误;两者返回的 disposer 都会移除注册。`registerFallback(handler)` 注册一个 handler,处理所有未被具名 route 命中的请求。第二次注册会抛错;随附的 SPA dist 服务器 [`dsh-host-frontend-static`](../frontend-static/README.zh.md) 是该 handler 的所有者,没有注册 handler 时服务器返回 404。index 的启动输入是结构化行:`collectIndexInjections()` 每次调用经一次 `webserver/index-inject` emit 现收一张全新的 `IndexInjection` 表,`renderIndex(html)` 先把行渲染进 index.html 响应体,再按注册顺序应用原始的 `tapIndex(transform)` 转换(`applyIndexTaps(html)`,行无法表达的标记的逃生口);fallback handler 在每次 index 响应时调用 `renderIndex`,静态部署则把同一批行经 boot 载荷下发,用导出的 `renderIndexInjections` 渲染。`port` 读取正在监听的端口(当 `port` 为 0 时读取 OS 分配的值),`host` 读取配置的绑定宿主(这些是其他插件据以自适应的组合期事实,例如 directory-picker 选择器)。HTTP 匹配顺序固定不变:先在整张表中匹配精确 route,再匹配最长前缀,最后交给 fallback handler。upgrade 只做精确匹配,未命中连接直接关闭;注册顺序不影响请求处理。
6
6
 
7
7
  该包不了解任何 harness 概念,也不提供任何文件服务:`/api` HTTP 桥接与下行 WebSocket 是 connection 插件的 route,插件 bundle 与 HMR(热模块替换)事件流是 modules/hmr 插件的 route,dist 服务则属于 fallback 持有者。upgrade handler 拥有协议握手与连接内容;webserver 只交付原始 socket 与 request。`host` 只接受 `127.0.0.1`(默认安全姿态)和 `0.0.0.0`(有意向网络开放)。该服务器只服务浏览器;Electron 通过 `file://` 加载 dist,并经 IPC 桥接承载 fetch。该包从不打印内容;URL 行属于 shell。
8
8
 
package/lib/index.js CHANGED
@@ -1,12 +1,87 @@
1
1
  import { createServer } from "node:http";
2
2
  import { Service } from "@deepseek-ai/cordis";
3
3
  import z from "@deepseek-ai/schemastery";
4
+ //#region lib/types/injections.js
5
+ /**
6
+ * Structured index injections: the typed rows plugins contribute to the boot
7
+ * HTML instead of raw `tapIndex` string transforms. Rows are pure
8
+ * JSON-serializable data because one table feeds two renderers: the served
9
+ * form renders rows into the index.html text ({@link renderIndexInjections}),
10
+ * and a static worker deployment ships the same rows over its boot payload
11
+ * for a page-side interpreter. Anything not expressible as a row stays on
12
+ * `tapIndex`, which runs after row rendering.
13
+ */
14
+ /** Escape a row value before placing it in a quoted HTML attribute. */
15
+ function escapeHtmlAttribute(value) {
16
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17
+ }
18
+ function assertNever(row) {
19
+ throw new Error(`webserver: unknown index injection row ${JSON.stringify(row)}`);
20
+ }
21
+ /** Render one row to markup with its placement. */
22
+ function renderRow(row) {
23
+ switch (row.kind) {
24
+ case "global": return {
25
+ placement: "head",
26
+ markup: `<script>globalThis[${JSON.stringify(row.name).replaceAll("<", "\\u003c")}] = ${row.value === void 0 ? "undefined" : JSON.stringify(row.value).replaceAll("<", "\\u003c")}<\/script>`
27
+ };
28
+ case "script": return {
29
+ placement: row.placement,
30
+ markup: `<script>${row.text}<\/script>`
31
+ };
32
+ case "script-src": return {
33
+ placement: row.placement,
34
+ markup: `<script src="${escapeHtmlAttribute(row.src)}"><\/script>`
35
+ };
36
+ case "style": return {
37
+ placement: "head",
38
+ markup: `<style>${row.text}</style>`
39
+ };
40
+ case "html": return {
41
+ placement: row.placement,
42
+ markup: row.html
43
+ };
44
+ default: return assertNever(row);
45
+ }
46
+ }
47
+ /** Insert `markup` into `html` at `at`. */
48
+ function splice(html, at, markup) {
49
+ return `${html.slice(0, at)}${markup}${html.slice(at)}`;
50
+ }
51
+ /**
52
+ * Render rows into an index.html body: head rows immediately after the
53
+ * opening head tag, body rows immediately after the opening body tag, each
54
+ * group in table order.
55
+ * @param html - the raw index.html body.
56
+ * @param rows - the collected injection table.
57
+ * @returns the html with every row rendered.
58
+ */
59
+ function renderIndexInjections(html, rows) {
60
+ let head = "";
61
+ let body = "";
62
+ for (const row of rows) {
63
+ const rendered = renderRow(row);
64
+ if (rendered.placement === "head") head += rendered.markup;
65
+ else body += rendered.markup;
66
+ }
67
+ let out = html;
68
+ if (head !== "") {
69
+ const open = /<head(?:\s[^>]*)?>/i.exec(out);
70
+ out = open === null ? `${head}${out}` : splice(out, open.index + open[0].length, head);
71
+ }
72
+ if (body !== "") {
73
+ const open = /<body(?:\s[^>]*)?>/i.exec(out);
74
+ out = open === null ? `${out}${body}` : splice(out, open.index + open[0].length, body);
75
+ }
76
+ return out;
77
+ }
78
+ //#endregion
4
79
  //#region lib/types/index.js
5
80
  /**
6
81
  * @deepseek-ai/dsh-host-webserver — Web route-registration plugin: a node:http
7
- * server plus the `webServer` service (HTTP and upgrade route registries,
8
- * index transform taps, and the single fallback seat for everything no route
9
- * claims). Knows no harness concepts and serves no files; the composing
82
+ * server plus the `webServer` service (HTTP and upgrade route registries, the
83
+ * structured index injection table with raw transform taps behind it, and the
84
+ * single fallback seat for everything no route claims). Knows no harness concepts and serves no files; the composing
10
85
  * application's frontend plugin owns dist serving through the fallback hook.
11
86
  * Web shape only — Electron loads dist over file:// and carries fetch over an
12
87
  * IPC bridge. This package never prints: the URL line belongs to the shell.
@@ -87,8 +162,9 @@ var WebServer = class extends Service {
87
162
  };
88
163
  }
89
164
  /**
90
- * Register an index.html transform, applied by the fallback owner to every
91
- * index response ({@link applyIndexTaps}) in registration order.
165
+ * Register a raw-HTML index transform, the escape hatch for markup no
166
+ * {@link IndexInjection} row expresses: {@link renderIndex} applies taps in
167
+ * registration order after rendering the structured rows.
92
168
  * @param transform - pure html-to-html function.
93
169
  * @returns the disposer removing the transform.
94
170
  */
@@ -212,6 +288,26 @@ var WebServer = class extends Service {
212
288
  for (const transform of this.indexTaps) out = transform(out);
213
289
  return out;
214
290
  }
291
+ /**
292
+ * Gather the structured injection table: one `webserver/index-inject` emit,
293
+ * every subscriber pushes its current rows. Fresh per call, so subscribers
294
+ * read live state (module graph, theme preference) at emit time.
295
+ * @returns rows in subscriber activation order.
296
+ */
297
+ collectIndexInjections() {
298
+ const table = [];
299
+ this.ctx.emit("webserver/index-inject", table);
300
+ return table;
301
+ }
302
+ /**
303
+ * Render one index.html body: the structured injection table first, then
304
+ * the raw `tapIndex` transforms over the result.
305
+ * @param html - the raw index.html body.
306
+ * @returns the transformed body.
307
+ */
308
+ renderIndex(html) {
309
+ return this.applyIndexTaps(renderIndexInjections(html, this.collectIndexInjections()));
310
+ }
215
311
  };
216
312
  //#endregion
217
- export { WebServer, WebServer as default };
313
+ export { WebServer, WebServer as default, renderIndexInjections };
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * @deepseek-ai/dsh-host-webserver — Web route-registration plugin: a node:http
3
- * server plus the `webServer` service (HTTP and upgrade route registries,
4
- * index transform taps, and the single fallback seat for everything no route
5
- * claims). Knows no harness concepts and serves no files; the composing
3
+ * server plus the `webServer` service (HTTP and upgrade route registries, the
4
+ * structured index injection table with raw transform taps behind it, and the
5
+ * single fallback seat for everything no route claims). Knows no harness concepts and serves no files; the composing
6
6
  * application's frontend plugin owns dist serving through the fallback hook.
7
7
  * Web shape only — Electron loads dist over file:// and carries fetch over an
8
8
  * IPC bridge. This package never prints: the URL line belongs to the shell.
@@ -11,10 +11,23 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
11
11
  import type { Duplex } from 'node:stream';
12
12
  import { Context, Service } from '@deepseek-ai/cordis';
13
13
  import z from '@deepseek-ai/schemastery';
14
+ import { type IndexInjection } from './injections.ts';
15
+ export { renderIndexInjections } from './injections.ts';
16
+ export type { IndexInjection, IndexInjectionPlacement } from './injections.ts';
14
17
  declare module '@deepseek-ai/cordis' {
15
18
  interface Context {
16
19
  webServer: WebServer;
17
20
  }
21
+ interface Events {
22
+ /**
23
+ * Collect the structured index injection table. Emitted on every index
24
+ * render and every worker boot-payload request; listeners push their
25
+ * current rows, so a row's data is read fresh at emit time.
26
+ * @param table - Mutable row table; listeners append in activation order.
27
+ * @mode emit
28
+ */
29
+ 'webserver/index-inject'(table: IndexInjection[]): void;
30
+ }
18
31
  }
19
32
  /** Route match kind: 'exact' matches the pathname verbatim; 'prefix' p matches p and p/<anything>. */
20
33
  export type WebRouteKind = 'exact' | 'prefix';
@@ -87,8 +100,9 @@ export declare class WebServer extends Service {
87
100
  */
88
101
  registerFallback(handler: WebRoute['handler']): () => void;
89
102
  /**
90
- * Register an index.html transform, applied by the fallback owner to every
91
- * index response ({@link applyIndexTaps}) in registration order.
103
+ * Register a raw-HTML index transform, the escape hatch for markup no
104
+ * {@link IndexInjection} row expresses: {@link renderIndex} applies taps in
105
+ * registration order after rendering the structured rows.
92
106
  * @param transform - pure html-to-html function.
93
107
  * @returns the disposer removing the transform.
94
108
  */
@@ -104,6 +118,20 @@ export declare class WebServer extends Service {
104
118
  * @returns the transformed body.
105
119
  */
106
120
  applyIndexTaps(html: string): string;
121
+ /**
122
+ * Gather the structured injection table: one `webserver/index-inject` emit,
123
+ * every subscriber pushes its current rows. Fresh per call, so subscribers
124
+ * read live state (module graph, theme preference) at emit time.
125
+ * @returns rows in subscriber activation order.
126
+ */
127
+ collectIndexInjections(): IndexInjection[];
128
+ /**
129
+ * Render one index.html body: the structured injection table first, then
130
+ * the raw `tapIndex` transforms over the result.
131
+ * @param html - the raw index.html body.
132
+ * @returns the transformed body.
133
+ */
134
+ renderIndex(html: string): string;
107
135
  }
108
136
  export default WebServer;
109
137
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Structured index injections: the typed rows plugins contribute to the boot
3
+ * HTML instead of raw `tapIndex` string transforms. Rows are pure
4
+ * JSON-serializable data because one table feeds two renderers: the served
5
+ * form renders rows into the index.html text ({@link renderIndexInjections}),
6
+ * and a static worker deployment ships the same rows over its boot payload
7
+ * for a page-side interpreter. Anything not expressible as a row stays on
8
+ * `tapIndex`, which runs after row rendering.
9
+ */
10
+ /** Document region a rendered row lands in: after the opening head or body tag. */
11
+ export type IndexInjectionPlacement = 'head' | 'body';
12
+ /** One structured index injection row. */
13
+ export type IndexInjection =
14
+ /** Assign a JSON-serializable value to a `globalThis` property, ahead of later script rows. */
15
+ {
16
+ kind: 'global';
17
+ name: string;
18
+ value: unknown;
19
+ }
20
+ /** Inline classic script. `text` must not contain `</script`, which would close the element early. */
21
+ | {
22
+ kind: 'script';
23
+ placement: IndexInjectionPlacement;
24
+ text: string;
25
+ }
26
+ /**
27
+ * External classic script, executed in table order: a parser-blocking tag
28
+ * when served, an awaited fetch-and-execute in the worker form (whose
29
+ * loader resolves worker-only URLs such as `/plugins/...`).
30
+ */
31
+ | {
32
+ kind: 'script-src';
33
+ placement: IndexInjectionPlacement;
34
+ src: string;
35
+ }
36
+ /** A `<style>` element in the head. `text` must not contain `</style`, which would close the element early. */
37
+ | {
38
+ kind: 'style';
39
+ text: string;
40
+ }
41
+ /** Raw markup fragment. */
42
+ | {
43
+ kind: 'html';
44
+ placement: IndexInjectionPlacement;
45
+ html: string;
46
+ };
47
+ /**
48
+ * Render rows into an index.html body: head rows immediately after the
49
+ * opening head tag, body rows immediately after the opening body tag, each
50
+ * group in table order.
51
+ * @param html - the raw index.html body.
52
+ * @param rows - the collected injection table.
53
+ * @returns the html with every row rendered.
54
+ */
55
+ export declare function renderIndexInjections(html: string, rows: readonly IndexInjection[]): string;
56
+ //# sourceMappingURL=injections.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-host-webserver",
3
3
  "description": "Web route-registration plugin: HTTP and upgrade routes, index transform taps, and static dist fallback; knows no harness concepts",
4
- "version": "0.1.0-rc.8",
4
+ "version": "0.1.1-rc.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,14 +32,14 @@
32
32
  ],
33
33
  "license": "MIT",
34
34
  "peerDependencies": {
35
- "@deepseek-ai/cordis": "^4.0.1",
36
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8"
35
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
36
+ "@deepseek-ai/cordis": "^4.0.1"
37
37
  },
38
38
  "dependencies": {
39
39
  "@deepseek-ai/schemastery": "^3.18.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@deepseek-ai/cordis": "^4.0.1",
43
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8"
43
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
44
44
  }
45
45
  }