@dshkit/tsdown-client
v0.2.2
Published
Build preset and conformance suite for DeepSeek Harness (dsh) UI plugin client bundles. A transitional layer: dsh does not publish its own client build preset yet.
Maintainers
Readme
@dshkit/tsdown-client
你写的东西,第一次就出现在屏幕上。
English | 中文
你写完一个 dsh 界面插件,dsh plugin add 装上,刷新页面 —— 什么都没有。
没有报错。没有警告。控制台干净。插件在 profile 的清单里、boot 也过了, 它就是不在界面上。于是你去翻自己的组件、翻 React、翻 CSS,两个小时之后才想到去看构建配置。
这不是你的错,也不是一次意外。这是 dsh 浏览器端插件的默认失败方式:安静地什么都不发生。 生态里 1081 个插件、应用层插件是 0,这道门槛是原因之一 —— 一个人试过一次、什么都没看到,就不会试第二次了。
为什么会这样
dsh 的浏览器端产物不是一个模块,是一句把 factory 交给 loader 的话:
window.__ModuleLoader__.load({ id: "<你的包名>", factory: (require) => { … } })手工复刻这套约定,要同时把 banner、footer、intro、CJS 产物形态、externals 表、内联规则、 构建期替换、CSS 管线全部弄对。任何一处偏一点,产物照样生成、照样能装、照样能 boot —— 只是 loader 认不出它。没有任何一环会告诉你。
用起来
npm i -D @dshkit/tsdown-client tsdown发布状态: 本仓 package manifest 是
0.2.1,但 npm registry 当前只发布到0.2.0;0.2.1尚未发布。所以上面的安装命令解析到的是 registry release,不是本仓较新的 manifest。请用npm view @dshkit/tsdown-client version --json和npm view @dshkit/tsdown-client versions --json现场重取这条边界。
⚠️ 如果你同时要装 harness 那几个包,从
@next取。 本包的 peer 范围写的是 它镜像的那个 release,而 dsh 客户端那几个包的latest至今指着已废弃的0.0.1-rc.1线 —— 裸npm i @deepseek-ai/dsh-client-modules会解析到另一条版本线上去,peer 根本满足不了。 跟哪个 channel、各个包的latest今天实际服务什么,gate:drift每次都印。
// tsdown.config.ts
import { dshClientBundle } from '@dshkit/tsdown-client'
export default [
// 你自己的 node 半边,原样保留
{ entry: ['src/index.ts'], outDir: 'lib', format: ['esm'], platform: 'node', dts: false, clean: false },
dshClientBundle({ id: '@you/your-plugin', entry: 'src/client/index.ts' }),
]两条规矩是 loader 定的,不是我们定的:id 必须等于你已发布的包名(loader 用它作模块表的键);
产物文件名恒为 <outDir>/client.js(boot manifest 按这个名字寻址)。
跨插件的 @deepseek-ai/* 值导入是构建错误,这是有意的:协作请走 cordis service。
type-only 导入会被擦除,永远到不了这道闸。
它替你挡掉的七个坑
每一条弄错,表现出来都是「插件不出现」,而不是一条报错:
| # | 条款 | 弄错的后果 |
|---|---|---|
| 1 | banner 打开 __ModuleLoader__.load({ id, factory }) 交接 | id 写错:注册在一个没人问的名字下 |
| 2 | intro 声明 module/exports,footer 归还它们 | 缺失:factory 返回 undefined |
| 3 | format: 'cjs'、platform: 'browser'、entryFileNames: 'client.js'、dts: false、clean: false | dts 会把 banner 卷进 .d.cts;默认 clean 会抹掉你的 node 半边 |
| 4 | external = 平台模块表 | 内联 React 或 cordis → 运行时身份分裂,报错指向一个不相干的地方 |
| 5 | noExternal = 表外一律内联 | 被外部化的依赖会产出模块表答不上来的 require() |
| 6 | define:process.env.NODE_ENV、import.meta.env.MODE,以及裸 import.meta.env / 裸 process.env 两个命名空间 | zustand 探 import.meta.env ? .MODE : …;任何一个命名空间没被替换,CJS 产物在 boot 时抛 ReferenceError |
| 7 | CSS Modules 经 lightningcss、[hash]_[local]、注入 <style data-plugin> | loader 卸载时按这个属性回收样式;属性不对,样式就留在页面上 |
本预设刻意比官方内部那份窄 —— 不做 host/client 两趟、不做 lib/types 分期、不管 node 半边。
窄签名才能保证官方实现将来总能满足它,反过来不成立。
你不用信我们,自己验
首选的公开检查直接读取你真正打进包里的字节,不需要源码 fixture、兄弟 checkout 或构建器适配层。
jsdom 和官方 loader 是可选的公开 peer,运行检查的项目必须自行安装两者(loader 请 pin 到本包瞄准的 release):
pnpm add -D jsdom@^29.1.1 @deepseek-ai/[email protected]import { checkArtifactConformance } from '@dshkit/tsdown-client/conformance'
const { ok, checks } = await checkArtifactConformance({
id: '@you/your-plugin',
artifactPath: 'package/lib/client.js', // 从将要发布的包中解出的文件
css: true, // 可选:要求至少有一份同步样式
})它验证规范信封与 id,用 dsh 已发布的真实 ClientModuleSystem materialize 产物,并在 factory
返回时(loader 自动认领样式之前)检查同步 <style> 是否已经带上预期 data-plugin。
它使用 Acorn 解析完整产物,定位实际的规范 loader 注册及 factory,然后追踪该 factory 的
require 绑定:每个引用都必须是直接调用,且只有一个位于 CLIENT_EXTERNALS 的字符串字面量参数;
动态参数、别名、传参、赋值、遮蔽或其他逃逸一律 fail closed;原生 import() 会绕开 loader 模块表,
因此也会被拒绝。字符串、注释、正则字面量和无关属性键不会误报;Unicode 转义标识符由 parser
正确解析。作为保守的语法防线,任何语法上可见的 eval 或
Function 引用、对这两个名称的静态成员或解构访问,以及静态 constructor 访问都会被拒绝,
包括别名和 dormant code。
这是一项诚实的基线运行时导入检查,不是 JavaScript sandbox。它无法证明任意反射或计算值绝不会 到达代码生成器,而且 loader materialization 会在调用者进程内执行。对于不可信产物,必须通过具有 外部隔离的 clean-room runner 执行;artifact pass 不是安全或 boot verdict。只看一份 JavaScript 产物也无法证明任意平台库没有被复制并内联,因为内联代码未必保留可识别的 import 或包名。它能证明 所有残留运行时查找都是字面量且 loader 模块表能够应答,并会在 materialization 前拒绝上面枚举的 动态代码语法。
构建器作者套件
构建器作者还应运行 fixture 套件;它保留单独一份打包产物无法证明的构建专属检查:
import { checkConformance } from '@dshkit/tsdown-client/conformance'
const { ok, checks } = await checkConformance({
// 用你自己的构建器构建 fixture,返回产出的 client.js 绝对路径。
// 构建失败必须 reject —— 有一个 fixture 就是要求构建失败的。
buildFixture: async (fixture) => { … },
})构建器套件会让构建出的 platform/CSS 产物通过同一个公开 primitive,再追加平台 marker/外部化、
define、CSS class map、构建路径和构建期跨插件 purity 检查。两个 API 都使用取自已发布
@deepseek-ai/dsh-client-modules 的真实 ClientModuleSystem;没有 loader mock,也不探测私有源码。
| 检查 | 断言 |
|---|---|
| banner-contract | 产物带着属于自己 id 的交接信封 |
| upstream-control | 官方自己发布的 bundle 满足同一份定义 —— 证明契约没有过拟合到本构建器 |
| loader-registration | bundle 加载即注册,且一个从不注册的 bundle 会被 loader 报出来 |
| module-table-resolution | 平台模块全部经模块表应答;普通依赖已内联;表外 require 被拒绝 |
| define-substitutions | 两个裸命名空间 + 两个精确键都生效 |
| css-modules | 哈希类名映射,以及一个 loader 能回收的 <style data-plugin> |
| purity-gate | 跨插件值导入必须构建失败 |
跑它需要三个 dev 依赖:jsdom、clsx(platform fixture 故意把它声明为 dependency ——
这正是套件抓「构建器把普通依赖外部化了」的手段),以及一个显式 pin 住的
@deepseek-ai/dsh-client-modules:该包的 latest dist-tag 指向旧线(0.0.1-rc.1),
当前版本发在 next 上。pin 到本预设瞄准的那个 release —— 今天是 0.1.0-rc.8:peer 精确声明就是这个版本,本仓套件也在它上面 materialize。
rc.8 换掉了模块系统的构造方式(解析过的 boot 图、显式的注册门面、一个 bootstrap 行),
所以为 rc.6 写的 harness 在它上面跑不起来,反之亦然 —— 套件跟着常量跟的那个 release 走。
上游一动,这里就红
上面那些常量抄自尚未发布的 dsh 源码。没人看的副本会静默过期,
而最坏的情况恰恰是安静的:PLATFORM_MODULES 列少了会让 bundle 内联一份自己的 React 或 cordis ——
它不抛错,只是行为不一致,而你会去查一个根本不在那儿的 bug。
列多了则是响的那种:bundle 去要一个模块表已经没有的行,materialize 当场死。
0.1.0-rc.8 一次拿掉三项,所以两个方向都真实存在。
以下命令从 dshkit monorepo 根目录运行(它们是维护者 gate,不是随 npm 包发布的 script):
pnpm run gate:drift # 对上游比对承重常量
pnpm run gate:drift:pin # 同一比对,跑在副本抄自的那个 sha 上(正对照)每次运行的第一行都印它自己的分母:看了几条、几条相同、几条动了、几条没比上 —— 所以这页不写那个数。
退出码 0 干净、1 有漂移、2 读数有洞。2 不是「网络抖了,重试一次」:
它表示至少有一条被看的常量根本没被比较(上游读不到,或那个常量已经不在闸看的地方),
而且它压过 1 —— 一次 2 的正文里可能正躺着真漂移。要读正文。
gate:drift:pin 是正对照:在 pin 住的那个 sha 上必须条条相同。它不绿,问题在这份副本或闸的 pattern,不在上游。
这个包希望自己被删掉
dsh 自己有一份 client 构建预设 —— packages/client/tsdown.client.ts ——
但它没有发布:所在目录没有 package.json、没有任何导出,文件本身还用相对路径伸进兄弟包的源码。
等官方发布那天,本包发一个 major 版本,实现体换成对官方的 export { … } from,
消费者零改动,等价性由契约套件证明。那个结局是目的达成,不是失败 ——
要拆的一直是门槛,不是要占住一个包名。
上游相关讨论:#2269。
许可
MIT
与 DeepSeek 无隶属关系。
dshkit是独立的社区项目。
