dsh-ts-example 0.1.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.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-08-28
4
+
5
+ ### Added
6
+
7
+ - TypeScript 插件开发示例:server 端 TS 源码 + tsc 编译(`lib/index.js` 产物)、client 端 TS 源码 + 构建时编译(`__ModuleLoader__` bundle)
8
+ - 演示内容:`GET /ts-example/api/greeting?name=xxx` 问候语路由、`GET /ts-example/api/stats` 会话计数、侧边栏「TS 示例」页签
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bsfeng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # dsh-ts-example
2
+
3
+ [![插件生态](https://img.shields.io/badge/插件生态-topic%20dsh-4d6bfe)](https://github.com/topics/dsh)
4
+
5
+ <div align="center">
6
+ <img alt="侧边栏「TS 示例」页签:显示当前会话问候语" src="./assets/screenshot.png" width="420" />
7
+ </div>
8
+
9
+ **DSH 插件 TypeScript 开发示例插件**:演示新插件用 TypeScript 开发的全流程——server 端 TS 源码 + `tsc` 编译,client 端 TS 源码 + 构建时编译;编译期即可发现模块不存在、类型不匹配、未定义变量等错误。
10
+
11
+ ## 功能
12
+
13
+ - **问候语路由**:`GET /ts-example/api/greeting?name=xxx` → `{ "greeting": "Hello, xxx!" }`(支持 `zh` / `en` 语言配置);
14
+ - **会话计数**:`GET /ts-example/api/stats` → `{ "sessions": N }`(监听 `session/start` 事件计数);
15
+ - **侧边栏页签「TS 示例」**:显示当前会话的问候语(client 端 TS → server 端 TS 全链路),经宿主原生侧边栏扩展点注册,零第三方依赖——同时作为「原生扩展点 + TS 构建」的活样例。
16
+
17
+ ## 配置
18
+
19
+ | 配置项 | 类型 | 默认值 | 说明 |
20
+ | ---------- | -------------- | ------ | ---------- |
21
+ | `language` | `'zh' \| 'en'` | `'en'` | 问候语语言 |
22
+
23
+ ## 安装
24
+
25
+ ```bash
26
+ # npm 安装(该包尚未发布到 npm,发版后可用)
27
+ dsh plugin --profile web add dsh-ts-example --trust-lockfile
28
+
29
+ # 本地 link(当前可用方式)
30
+ git clone https://github.com/baosfeng/my-dsh-plugins.git
31
+ dsh plugin --profile web add link:<仓库路径>/plugins/dsh-ts-example
32
+ ```
33
+
34
+ ## 相关文档
35
+
36
+ → [TS 示例概述](../../docs/TS示例/概述.md) · [CHANGELOG](CHANGELOG.md)
Binary file
@@ -0,0 +1,9 @@
1
+ # dsh-ts-example bundle patch: inserts the plugin row into the profile roster.
2
+ # The row mounts the host half (greeting + stats API) and the client half
3
+ # (sidebar "TS 示例" tab). Install with:
4
+ #
5
+ # git clone https://github.com/baosfeng/my-dsh-plugins.git
6
+ # dsh plugin --profile web add link:<仓库路径>/plugins/dsh-ts-example
7
+ - insert:
8
+ - id: ts-example
9
+ name: 'dsh-ts-example'
package/lib/client.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * dsh-ts-example — client half (browser). SOURCE TEMPLATE.
3
+ *
4
+ * BUILD NOTE: 本文件是模板源码,不是 DSH 实际服务的文件。scripts/build.mjs
5
+ * 先运行 `tsc -p tsconfig.client.json` 把 src/client/index.ts 编译成
6
+ * CommonJS 单文件(lib/.client-build/index.js),再注入下方
7
+ * /*__CLIENT_BUNDLE__* / 占位符(函数式 replaceAll,避免 $&/$1 特殊解释),
8
+ * 写出 lib/client.js —— 即 DSH 实际服务的产物(单一 __ModuleLoader__ bundle)。
9
+ * 产物必须提交;CI 只对产物执行 node --check(见 .github/workflows/ci.yml)。
10
+ *
11
+ * 编译产物为 CommonJS 格式:require / exports / module 均为本 factory 作用域
12
+ * 变量(require 由 __ModuleLoader__ 注入,exports/module 为上方局部变量),
13
+ * 因此产物可直接内联。client 端 TS 源码为单文件(无运行时相对 import),
14
+ * 需要多文件/复杂打包时可用 esbuild/tsdown(官方 tsdown.client.ts 协议)。
15
+ */
16
+ window.__ModuleLoader__.load({
17
+ id: 'dsh-ts-example',
18
+ factory: (require) => {
19
+ var module = { exports: {} }
20
+ var exports = module.exports
21
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
22
+
23
+ // ── TS 编译产物(scripts/build.mjs 注入)────────────────────────
24
+ "use strict";
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.inject = void 0;
27
+ exports.apply = apply;
28
+ /**
29
+ * dsh-ts-example — client 端入口(TypeScript 源码,单文件)。
30
+ *
31
+ * 构建流程:`tsc -p tsconfig.client.json` 把本文件编译为 CommonJS 单文件
32
+ * (lib/.client-build/index.js),scripts/build.mjs 再注入
33
+ * lib/client.src.js 模板的 __CLIENT_BUNDLE__ 占位符,写出
34
+ * lib/client.js(DSH 实际服务的 __ModuleLoader__ bundle)。
35
+ *
36
+ * 约束:client 端 TS 源码为单文件(无运行时相对 import——编译产物内联进
37
+ * factory 作用域后,require 只认识 DSH 运行时注入的模块,如 react)。
38
+ * 类型声明可拆文件(import type 编译期擦除);需要多文件/复杂打包时可用
39
+ * esbuild/tsdown(官方 tsdown.client.ts 协议)。
40
+ *
41
+ * 演示内容:侧边栏页签「TS 示例」——调 server 端 /ts-example/api/greeting
42
+ * 显示问候语(client TS → server TS 全链路)。
43
+ *
44
+ * 侧边栏走**宿主原生扩展点**(issue #187 批 1),不再消费第三方
45
+ * dsh-better-sidebar 服务:
46
+ * 1. `ctx.sidebarRightTabs.register(...)` 注册页面类型(含 guide 胶囊,
47
+ * 用户从右栏指南页点开);
48
+ * 2. keyed 席位 `sidebar.right.pane.tab` 注册面板本体(key = 类型 id);
49
+ * 3. keyed 席位 `sidebar.right.pane.tab.title` 注册页签标题。
50
+ * 原生能力通过 Cordis 服务名 inject 获取(`slots` / `sidebarRightTabs`),
51
+ * 无需 require 任何 `@deepseek-ai/dsh-client-ui-*` 包 —— 官方生产范本见
52
+ * dsh-client-ui-sidebar-files/lib/client.js:681-711。
53
+ */
54
+ const react_1 = require("react");
55
+ // ── 插件体 ─────────────────────────────────────────────────────────────
56
+ /** 页签实现身份(官方惯例:包名;全局唯一)。 */
57
+ const TAB_ID = 'dsh-ts-example';
58
+ /** 页面类型判别符(沿用迁移前 better-sidebar 的 tab id)。 */
59
+ const TAB_KIND = 'dsh-ts-example:greeting';
60
+ /** 指南页相对顺序(沿用迁移前 better-sidebar 的 order)。 */
61
+ const TAB_ORDER = 90;
62
+ exports.inject = ['slots', 'sidebarRightTabs'];
63
+ function apply(ctx) {
64
+ // 服务缺失(旧宿主)时静默跳过:判空必须同时覆盖 null 与 undefined
65
+ // (typeof null 是 object,会骗过 === undefined 的写法)。
66
+ const tabs = ctx.sidebarRightTabs;
67
+ const slots = ctx.slots;
68
+ if (tabs == null || slots == null)
69
+ return;
70
+ ctx.effect(() => tabs.register({
71
+ id: TAB_ID,
72
+ kind: TAB_KIND,
73
+ title: () => 'TS 示例',
74
+ guide: [{ order: TAB_ORDER, title: () => 'TS 示例' }],
75
+ }), 'dsh-ts-example: tab');
76
+ ctx.effect(() => slots.inject('sidebar.right.pane.tab', () => slots.register({ name: 'sidebar.right.pane.tab', key: TAB_ID }, GreetingTabBody)), 'dsh-ts-example: greeting tab body');
77
+ ctx.effect(() => slots.inject('sidebar.right.pane.tab.title', () => slots.register({ name: 'sidebar.right.pane.tab.title', key: TAB_ID }, GreetingTabTitle)), 'dsh-ts-example: greeting tab title');
78
+ }
79
+ // ── 原生席位 → 面板适配 ────────────────────────────────────────────────
80
+ /**
81
+ * 原生 tab body 席位:把原生 props(`sessionId` + `useTabInfo` hook)
82
+ * 适配成面板契约(`scope.sessionId` + `visible`),面板本体零改动。
83
+ * `useTabInfo` 缺失(契约不匹配)时保守取 visible=true,只影响轮询。
84
+ */
85
+ function GreetingTabBody(props) {
86
+ const visible = props.useTabInfo?.()?.tab?.visible ?? true;
87
+ return (0, react_1.createElement)(GreetingPanel, { scope: { sessionId: props.sessionId }, visible });
88
+ }
89
+ /** 原生 tab title 席位:优先用宿主给定的标题(i18n 由文档标题决定)。 */
90
+ function GreetingTabTitle(props) {
91
+ return (0, react_1.createElement)('span', null, props.useTabInfo?.()?.tab?.title ?? 'TS 示例');
92
+ }
93
+ // ── 页面组件 ───────────────────────────────────────────────────────────
94
+ function GreetingPanel(props) {
95
+ const [greeting, setGreeting] = (0, react_1.useState)('');
96
+ const [loading, setLoading] = (0, react_1.useState)(true);
97
+ (0, react_1.useEffect)(() => {
98
+ if (!props.visible)
99
+ return;
100
+ let cancelled = false;
101
+ fetch(`/ts-example/api/greeting?name=${encodeURIComponent(props.scope?.sessionId ?? '')}`)
102
+ .then((response) => response.json())
103
+ .then((body) => {
104
+ if (!cancelled) {
105
+ setGreeting(body.greeting ?? '');
106
+ setLoading(false);
107
+ }
108
+ })
109
+ .catch(() => {
110
+ if (!cancelled) {
111
+ setGreeting('(请求失败)');
112
+ setLoading(false);
113
+ }
114
+ });
115
+ return () => {
116
+ cancelled = true;
117
+ };
118
+ }, [props.visible, props.scope?.sessionId]);
119
+ return (0, react_1.createElement)('div', { style: { padding: '12px', fontFamily: 'var(--dsw-font-sans)' } }, (0, react_1.createElement)('h3', null, 'TS 示例插件'), (0, react_1.createElement)('p', null, loading ? '加载中…' : greeting), (0, react_1.createElement)('p', { style: { color: 'var(--dsw-alias-text-tertiary)', fontSize: '12px' } }, 'server 端由 TypeScript 编写(tsc 编译),client 端由 TypeScript 编写(构建时编译)。'));
120
+ }
121
+
122
+
123
+ return module.exports
124
+ },
125
+ })
@@ -0,0 +1,28 @@
1
+ /**
2
+ * dsh-ts-example — client half (browser). SOURCE TEMPLATE.
3
+ *
4
+ * BUILD NOTE: 本文件是模板源码,不是 DSH 实际服务的文件。scripts/build.mjs
5
+ * 先运行 `tsc -p tsconfig.client.json` 把 src/client/index.ts 编译成
6
+ * CommonJS 单文件(lib/.client-build/index.js),再注入下方
7
+ * /*__CLIENT_BUNDLE__* / 占位符(函数式 replaceAll,避免 $&/$1 特殊解释),
8
+ * 写出 lib/client.js —— 即 DSH 实际服务的产物(单一 __ModuleLoader__ bundle)。
9
+ * 产物必须提交;CI 只对产物执行 node --check(见 .github/workflows/ci.yml)。
10
+ *
11
+ * 编译产物为 CommonJS 格式:require / exports / module 均为本 factory 作用域
12
+ * 变量(require 由 __ModuleLoader__ 注入,exports/module 为上方局部变量),
13
+ * 因此产物可直接内联。client 端 TS 源码为单文件(无运行时相对 import),
14
+ * 需要多文件/复杂打包时可用 esbuild/tsdown(官方 tsdown.client.ts 协议)。
15
+ */
16
+ window.__ModuleLoader__.load({
17
+ id: 'dsh-ts-example',
18
+ factory: (require) => {
19
+ var module = { exports: {} }
20
+ var exports = module.exports
21
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
22
+
23
+ // ── TS 编译产物(scripts/build.mjs 注入)────────────────────────
24
+ /*__CLIENT_BUNDLE__*/
25
+
26
+ return module.exports
27
+ },
28
+ })
@@ -0,0 +1,14 @@
1
+ /**
2
+ * dsh-ts-example — greeting 纯函数(server 端逻辑)。
3
+ *
4
+ * 独立成模块以演示:TS 模块拆分 + 类型检查(interface / 联合类型 /
5
+ * 可选参数)+ 单元测试直接 import 编译产物。
6
+ */
7
+ /** 生成问候语:空名回退默认问候,非空名去除首尾空白后拼接。 */
8
+ export function buildGreeting(options) {
9
+ const name = options.name.trim();
10
+ if (name === '') {
11
+ return options.language === 'zh' ? '你好,DSH!' : 'Hello, DSH!';
12
+ }
13
+ return options.language === 'zh' ? `你好,${name}!` : `Hello, ${name}!`;
14
+ }
package/lib/index.js ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * dsh-ts-example — server 端入口(TypeScript 源码)。
3
+ *
4
+ * 本文件是 TS 插件 server 端的示例:`tsc -p tsconfig.json` 编译为
5
+ * lib/index.js(产物必须提交,CI 只跑 node --check + 测试,不跑构建)。
6
+ *
7
+ * 演示内容:
8
+ * - 类型检查:import 不存在的模块 → TS2307 编译期报错(#39 的
9
+ * require('dsh-md-render') 类错误在 TS 下不可能发版出去);
10
+ * - 路由:GET /ts-example/api/greeting?name=xxx → { greeting };
11
+ * - 事件:session/start 计数,GET /ts-example/api/stats → { sessions };
12
+ * - 信任围栏:非 loopback 来源 403(与 /api 网关一致的契约)。
13
+ */
14
+ import { buildGreeting } from './greeting.js';
15
+ export const name = 'dsh-ts-example';
16
+ export const inject = ['webServer'];
17
+ // config 可能缺省:DSH 对未声明 config schema 的插件调用 apply(ctx) 时
18
+ // 第二个参数为 undefined(cordis 契约),必须用可选链兜底,否则 TypeError。
19
+ /**
20
+ * root 上的注册表(以 root ctx 为键)。
21
+ *
22
+ * ⚠️ 教学要点(issue #242 实测,DSH 0.1.5-rc.1):profile 插件的 fiber 会被 loader
23
+ * 在 apply 结束后回收,**注册在插件自身 ctx 上的监听器与路由会随之静默消失**——
24
+ * 事件 0 触发、路由返回 404,且**没有任何报错**。所以本样例的所有注册都挂到
25
+ * **常驻 ctx.root**,并以 root 为键去重(loader 会多次 apply 同一插件,重复注册会
26
+ * 让同一事件被处理多次)。照抄本样例时,**不要把 root 改回 ctx**。
27
+ */
28
+ const rootRegistrations = new WeakMap();
29
+ export function apply(ctx, config) {
30
+ const language = config?.language ?? 'en';
31
+ let sessionCount = 0;
32
+ const listenCtx = ctx.root ?? ctx;
33
+ for (const dispose of rootRegistrations.get(listenCtx) ?? [])
34
+ dispose();
35
+ const disposers = [];
36
+ // ── 事件监听:会话开始计数(演示 ctx.on;注册在常驻 root)──────────
37
+ disposers.push(listenCtx.on('session/start', () => {
38
+ sessionCount += 1;
39
+ }, { global: true }));
40
+ // ── 路由:greeting + stats(注册在常驻 root,自持 disposer)────────
41
+ const webServer = ctx.webServer ?? listenCtx.webServer;
42
+ if (webServer !== undefined) {
43
+ disposers.push(webServer.register({
44
+ kind: 'prefix',
45
+ path: '/ts-example/api',
46
+ handler: (request, response) => {
47
+ if (!isTrustedRequest(request)) {
48
+ writeJson(response, 403, { ok: false, error: 'forbidden' });
49
+ return;
50
+ }
51
+ const path = request.url?.split('?')[0] ?? '';
52
+ if (path === '/ts-example/api/greeting') {
53
+ const name = readQueryParam(request, 'name');
54
+ writeJson(response, 200, { greeting: buildGreeting({ name, language }) });
55
+ return;
56
+ }
57
+ if (path === '/ts-example/api/stats') {
58
+ writeJson(response, 200, { sessions: sessionCount });
59
+ return;
60
+ }
61
+ writeJson(response, 404, { ok: false, error: 'not found' });
62
+ },
63
+ }));
64
+ }
65
+ rootRegistrations.set(listenCtx, disposers);
66
+ }
67
+ /** 请求是否来自本机(loopback 信任围栏)。 */
68
+ function isTrustedRequest(request) {
69
+ const host = request.headers.host;
70
+ if (typeof host !== 'string')
71
+ return false;
72
+ try {
73
+ const hostname = new URL(`http://${host}`).hostname;
74
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ /** 读取查询参数(request.url 的 query 部分)。 */
81
+ function readQueryParam(request, key) {
82
+ const url = request.url ?? '';
83
+ const queryIndex = url.indexOf('?');
84
+ if (queryIndex < 0)
85
+ return '';
86
+ return new URLSearchParams(url.slice(queryIndex + 1)).get(key) ?? '';
87
+ }
88
+ /** 写 JSON 响应。 */
89
+ function writeJson(response, status, body) {
90
+ response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
91
+ response.end(JSON.stringify(body));
92
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "dsh-ts-example",
3
+ "version": "0.1.0",
4
+ "description": "DSH 插件 TypeScript 开发示例:server 端 tsc 编译(lib/index.js 产物)、client 端构建时编译(__ModuleLoader__ bundle),演示编译期类型检查(TS2307 模块不存在、类型不匹配等)。DSH web plugin: TypeScript development example — tsc-compiled server half + build-time-compiled client half.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "engines": {
8
+ "node": ">=22"
9
+ },
10
+ "scripts": {
11
+ "build": "npx tsc -p tsconfig.json && node scripts/build.mjs",
12
+ "typecheck": "npx tsc --noEmit -p tsconfig.json && npx tsc --noEmit -p tsconfig.client.json",
13
+ "test": "npx vitest run --coverage"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "default": "./lib/index.js"
18
+ },
19
+ "./client": {
20
+ "default": "./lib/client.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "lib",
26
+ "cordis.patch.yml",
27
+ "README.md",
28
+ "CHANGELOG.md",
29
+ "LICENSE",
30
+ "assets"
31
+ ],
32
+ "dsh": {
33
+ "bundle": {
34
+ "patch": "./cordis.patch.yml"
35
+ },
36
+ "client": {
37
+ "platform": "web"
38
+ }
39
+ },
40
+ "peerDependencies": {
41
+ "cordis": "^4.0.0-rc.10",
42
+ "react": "^18.2.0 || ^19.3.0"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "cordis": {
46
+ "optional": true
47
+ }
48
+ },
49
+ "license": "MIT"
50
+ }