Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [Semantic Versioning](https://semver.org/). 中文版见 [CHANGELOG_ZH.md](./CHANGELOG_ZH.md).

## [Unreleased]

### Fixed

- **DSH 0.2.0 compatibility** — the `@deepseek-ai/dsh-client-*` peer ranges ended at `<0.2.0`. DSH's own startup gate (`evaluatePluginCompatibility` in `dsh-app-boot`, which matches with `includePrerelease`) therefore accepted `0.2.0-rc.x` but **rejects `0.2.0` and every later version**. An incompatible bundle is skipped whole: the indicator, the panel and all five routes disappear, leaving only one line on `dsh web`'s stderr. The ranges now cover both lines — `>=0.1.0-rc.6 <0.2.0 || >=0.2.0-rc.1 <0.3.0`.
- Removed `@deepseek-ai/dsh-client-runtime` from `peerDependencies` and from `dsh.client.inject`: the package no longer exists in DSH 0.2.0, and this plugin only ever imported it as a type (erased at build time). `@deepseek-ai/dsh-client-web-react` and `@deepseek-ai/dsh-client-schema-form` were dropped from the client externals for the same reason.

### Tests

- New `tests/dsh-compat.test.mjs`: calls DSH's own `evaluatePluginCompatibility` against this manifest for a version matrix (0.1.x, 0.2.0-rc.x, **and the 0.2.0/0.2.1 releases**), asserts the unsupported 0.3.0 line is still rejected, and checks that every DSH package the manifest names is actually installed in the target DSH. Skips when no DSH is present, since the plugin's CI installs only the plugin.

## [1.1.7] - 2026-09-24

### Fixed
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
本文件记录本项目所有重要变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。英文版见 [CHANGELOG.md](./CHANGELOG.md)。

## [未发布]

### 修复

- **适配 DSH 0.2.0**——`@deepseek-ai/dsh-client-*` 的 peer 区间原本止于 `<0.2.0`。DSH 启动期的兼容性门禁(`dsh-app-boot` 的 `evaluatePluginCompatibility`,判定时带 `includePrerelease`)因此放行了 `0.2.0-rc.x`,但**会拒绝 `0.2.0` 及其之后的任何版本**。被拒的 bundle 会被整包跳过:指示器、面板和全部五个路由一起消失,只在 `dsh web` 的 stderr 留一行。现区间覆盖两条线——`>=0.1.0-rc.6 <0.2.0 || >=0.2.0-rc.1 <0.3.0`。
- 从 `peerDependencies` 与 `dsh.client.inject` 中移除 `@deepseek-ai/dsh-client-runtime`:该包在 DSH 0.2.0 已不存在,本插件对它也只有一个构建期即被擦除的 type-only import。同理从客户端 externals 中移除 `@deepseek-ai/dsh-client-web-react` 与 `@deepseek-ai/dsh-client-schema-form`。

### 测试

- 新增 `tests/dsh-compat.test.mjs`:直接调用 DSH 自己的 `evaluatePluginCompatibility`,对本插件的 manifest 跑一组版本矩阵(0.1.x、0.2.0-rc.x,**以及 0.2.0/0.2.1 正式版**),断言未声明支持的 0.3.0 线仍被拒绝,并校验 manifest 点名的每个 DSH 包在目标 DSH 里真实存在。环境里没有 DSH 时跳过(插件自身的 CI 只装插件)。

## [1.1.7] - 2026-09-24

### 修复
Expand Down
9 changes: 3 additions & 6 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,18 @@ const tscBin = require.resolve('typescript/bin/tsc')

const PLUGIN_ID = process.env.DSH_PLUGIN_ID ?? 'dsh-usage-chart'

// 与 @deepseek-ai/dsh-client-web/src/platform.ts 的 PLATFORM_MODULES 完全一致
// + RUNTIME_STORE_EXEMPTION(packages/client/tsdown.client.ts 的 CLIENT_EXTERNALS)。
// 运行时这些 specifier 由浏览器的模块表(loader)回答,绝不内联。
// 浏览器模块表(loader)能回答的 specifier,绝不内联。清单已按 DSH 0.2.0-rc.2 核对:
// 只保留实际存在的包——0.1.x 时代的 dsh-client-runtime / dsh-client-web-react /
// dsh-client-schema-form 在 0.2.0 已不存在(本包对它们只有 type-only import,esbuild 会擦除)。
const CLIENT_EXTERNALS = [
'react',
'react/jsx-runtime',
'react-dom',
'react-dom/client',
'@deepseek-ai/cordis',
'@deepseek-ai/dsh-client-ui-slots',
'@deepseek-ai/dsh-client-web-react',
'@deepseek-ai/dsh-client-ui-primitives',
'@deepseek-ai/dsh-client-ui-attachment',
'@deepseek-ai/dsh-client-schema-form',
'@deepseek-ai/dsh-client-runtime/client',
]

// A clean output directory prevents stale chunks or declarations from leaking
Expand Down
11 changes: 3 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@
},
"client": {
"inject": [
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-ui-conversation",
"@deepseek-ai/dsh-client-ui-slots"
Expand All @@ -87,19 +86,15 @@
},
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.1",
"@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.6 <0.2.0",
"@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.6 <0.2.0",
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.0-rc.6 <0.2.0",
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.6 <0.2.0",
"@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.6 <0.2.0 || >=0.2.0-rc.1 <0.3.0",
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.0-rc.6 <0.2.0 || >=0.2.0-rc.1 <0.3.0",
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.6 <0.2.0 || >=0.2.0-rc.1 <0.3.0",
"react": "^18.2.0"
},
"peerDependenciesMeta": {
"@deepseek-ai/cordis": {
"optional": true
},
"@deepseek-ai/dsh-client-runtime": {
"optional": true
},
"@deepseek-ai/dsh-client-locale": {
"optional": true
},
Expand Down
77 changes: 77 additions & 0 deletions tests/dsh-compat.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* DSH 运行时兼容性门禁——把"假绿"换成真检查。
*
* DSH 启动时会用 dsh-app-boot 的 evaluatePluginCompatibility 判定每个 profile bundle 的
* peerDependencies;判为不兼容的 bundle 会被整包跳过,用量面板与全部路由一起消失,
* 只在 dsh web 的 stderr 留一行。本文件直接调用同一个函数,对着插件自己的 manifest 断言
* 声明仍然成立,并且**覆盖各线正式版**(0.2.0 / 0.2.1),而不只是 rc —— "<0.2.0" 那个区间
* 正是"rc 通过、正式版被拒",也就是这里要拦住的东西。
*
* 解析不到 DSH 安装时跳过:插件自身的 CI 只装插件,不装 DSH。
*/
import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'

const root = dirname(dirname(fileURLToPath(import.meta.url)))
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))

/** 声明支持的 DSH 运行时版本(含各线正式版)。 */
const SUPPORTED = ['0.1.0-rc.6', '0.1.5', '0.1.9', '0.2.0-rc.1', '0.2.0-rc.2', '0.2.0', '0.2.1']
/** 明确不支持、必须被拒的版本;要放宽区间就先改这里。 */
const REJECTED = ['0.3.0']

/** 候选 DSH 安装根(DSH_CHECKOUT 优先,其次本机全局安装)。 */
const DSH_ROOTS = [process.env.DSH_CHECKOUT, '/usr/local/lib/node_modules/@deepseek-ai/dsh'].filter(Boolean)

/** 取真正的判定函数与提供它的 DSH 根;取不到返回 undefined(CI 上正常)。 */
async function loadDsh() {
for (const base of DSH_ROOTS) {
try {
const req = createRequire(join(base, 'package.json'))
const entry = req.resolve('@deepseek-ai/dsh-app-boot')
return { root: base, module: await import(pathToFileURL(entry).href) }
} catch {
// 换下一个候选
}
}
return undefined
}

const dsh = await loadDsh()
const skip = dsh === undefined ? 'DSH is not installed in this environment' : false

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Ensure the compatibility regression is actually exercised in CI

The current CI workflow installs only this plugin and supplies no DSH_CHECKOUT, so all three new compatibility tests always skip. At the same time, manifest.test.mjs removes the existing literal-range assertion. Reintroducing the old '<0.2.0' upper bound would therefore still produce a green standard CI run. Locally, npm run verify reports 46 passed / 3 skipped; explicitly setting DSH_CHECKOUT to a nonexistent directory also silently skips because loadDsh catches every resolution/import failure.

Please keep an unconditional range-semantics regression test or wire a pinned DSH installation into CI so these checks run. When DSH_CHECKOUT is explicitly supplied, a broken checkout/import should fail the test instead of being treated as an absent optional environment.


test('peerDependencies accept every supported DSH runtime', { skip }, () => {
for (const version of SUPPORTED) {
const issue = dsh.module.evaluatePluginCompatibility(manifest, {}, version)
assert.equal(issue, undefined, `dsh ${version} rejects ${JSON.stringify(issue?.peers ?? {})}`)
}
})

test('peerDependencies still reject runtimes outside the declared lines', { skip }, () => {
for (const version of REJECTED) {
const issue = dsh.module.evaluatePluginCompatibility(manifest, {}, version)
assert.notEqual(issue, undefined, `dsh ${version} is accepted but the plugin declares no support for it`)
}
})

test('every DSH package the manifest names is installed in the target DSH', { skip }, () => {
const named = [...Object.keys(manifest.peerDependencies ?? {}), ...(manifest.dsh?.client?.inject ?? [])]
const req = createRequire(join(dsh.root, 'package.json'))
const missing = [...new Set(named)].filter((name) => {
if (name !== '@deepseek-ai/dsh' && !name.startsWith('@deepseek-ai/dsh-')) return false
for (const specifier of [`${name}/package.json`, name]) {
try {
req.resolve(specifier)
return false
} catch {
// 试下一个 specifier
}
}
return true
})
assert.deepEqual(missing, [], `declared but absent from DSH ${dsh.module.getDshRuntimeVersion()}`)
})
7 changes: 5 additions & 2 deletions tests/manifest.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ test('package declares an installable DSH web bundle', () => {
assert.equal(pkg.dsh.bundle.patch, './cordis.patch.yml')
assert.equal(pkg.dsh.client.platform, 'web')
assert.ok(pkg.keywords.includes('dsh-plugin'))
assert.equal(pkg.peerDependencies['@deepseek-ai/dsh-client-locale'], '>=0.1.0-rc.6 <0.2.0')
assert.equal(pkg.peerDependenciesMeta['@deepseek-ai/dsh-client-locale'].optional, true)
// 区间语义由 tests/dsh-compat.test.mjs 对着 DSH 自己的门禁校验;这里只锁声明形状。
for (const name of ['@deepseek-ai/dsh-client-locale', '@deepseek-ai/dsh-client-ui-conversation', '@deepseek-ai/dsh-client-ui-slots']) {
assert.equal(typeof pkg.peerDependencies[name], 'string', name)
assert.equal(pkg.peerDependenciesMeta[name].optional, true, name)
}
})

test('all exported runtime and type files exist after build', async () => {
Expand Down
Loading