fix(release): keep node bundles readable for the worker image

The browser worker image lowers `yield*` only as a whole statement and
rewrites `for await` loops by their source layout, so compressed node-side
entries either fail that lowering or lower to unparseable code. Only the
browser bundles keep compression, which removes about a third of their text.

The bundle-purity assertion now matches the module-loader call regardless of
the quote style compression picks, and the release-pack fixture exercises
the installed package's new documentation requirement.
This commit is contained in:
turtle1999 committed 2026-10-08 20:20:54 +08:00
1 parent a9fc7f9945
commit 8890ac3705
7 files changed
+27 -24

No files matched your search

@@ -9,11 +9,11 @@
en: 699d35bd0bb12645
zh: c9e362c336c6b440
/agent-note-published-source-maps-and-documentation/decision:
en: b5d394bd286b1e2d
zh: f5d3317115442caa
en: 7b0556c6eab6fcf9
zh: 4788e9a4ebb9d528
/agent-note-published-source-maps-and-documentation/alternatives-considered:
en: 0bcc1a6864bcb340
zh: ff5d138040563624
en: a61183c5b42eddba
zh: 031679975ab1f9c7
/agent-note-published-source-maps-and-documentation/consequences:
en: bd76714dd121f9aa
zh: 6d474d6ea90e1b5f
en: 4e7510bd2a14a1bc
zh: b9eb3000268cfd51
@@ -14,7 +14,7 @@ Every release member publishes its TypeScript source, the declaration maps that
[`expectedDshPackageFiles`](../../../../scripts/check-workspace-constraints.ts) lists `lib/types/**/*.d.ts.map`, `lib/**/*.js.map`, and `src`; a package with Typert Remote navigation also lists `lib/typert.remote-client.d.ts.map`, which its generated declaration references through `sourceMappingURL`. No payload member is forbidden, so the static manifest check owns the exact publication list. The `./src/*` export that 315 of the 332 release members declare resolves to those files, which serves editor navigation and reading; plain Node still cannot import them without a TypeScript loader.
The workspace build and the client preset compress every emitted bundle (`minify: { compress: true, mangle: false }`). Identifiers keep their names, so a stack frame and the published map still name the original code.
The client preset compresses the browser bundle's text (`minify: { compress: false, mangle: false }`). Identifiers keep their names, so a stack frame and the published map still name the original code. Node-side entries stay uncompressed: the browser worker image lowers `yield*` only as a whole statement and rewrites `for await` loops by their source layout, and compressed input breaks both.
`@deepseek-ai/dsh` stages the documentation website's page set into `apps/cli/docs` from its `prepack` lifecycle through [`scripts/stage-dsh-docs.ts`](../../../../scripts/stage-dsh-docs.ts), which reads the publication manifest in [`website/docs.ts`](../../../../website/docs.ts), and removes that copy again from `postpack`. Non-Markdown assets under `docs/` ship with those pages; the generated `docs/persistence-changes/` records and the `docs/i18n/` guidelines do not. The vendored family keeps upstream's payload unchanged ([release sequences](2026-08-10-npm-release-sequences.md)).
@@ -30,11 +30,11 @@ Desktop and the Python single-file executable pay nothing for either addition. [
**Mangle the bundles as well.** Rejected because mangled identifiers break the correspondence a published map exists to provide and make an unhandled stack frame unreadable.
**Compress only `lib/index.js` and `lib/client.js`.** Rejected because one workspace build configuration emits every bundle, and a per-package exception would need a second configuration for no reader-visible gain.
**Compress the node-side entries as well.** Rejected because the browser worker image refuses compressed input: the packer rejects a `yield*` a compressor moved into an expression, and it produces unparseable code for `dsh-agent-instructions`. Both are lowering constraints, not size arguments.
## Consequences
`src` adds about 29 MB and declaration maps about 10 MB across the release set; JavaScript maps add about 49 MB, and the staged documentation adds 5.9 MB to `@deepseek-ai/dsh`. Compression removes roughly half of a host bundle — `dsh-session`'s `lib/index.js` falls from 95 kB to 46 kB — and about a third of a client bundle. The Desktop application and the Python executable keep their previous payload.
`src` adds about 29 MB and declaration maps about 10 MB across the release set; JavaScript maps add about 49 MB, and the staged documentation adds 5.9 MB to `@deepseek-ai/dsh`. Compression removes about a third of a browser bundle — `ui-chat`'s `lib/client.js` falls from 591 kB to 420 kB — while node-side entries keep their size until the worker image lowering accepts compressed input. The Desktop application and the Python executable keep their previous payload.
A consumer editor now follows a declaration to the installed implementation, and a plugin author reads that version's documentation without a checkout. The staged copy keeps the repository's relative links, so a link from a shipped page into an unshipped tree — `../packages/**`, or the omitted `persistence-changes/` records — resolves only in a checkout. `docs/` exists in the working tree only between `prepack` and `postpack`, and `pnpm run clean` removes a copy an interrupted pack left behind.
@@ -14,7 +14,7 @@ Status: implemented
[`expectedDshPackageFiles`](../../../../scripts/check-workspace-constraints.ts) 列出 `lib/types/**/*.d.ts.map`、`lib/**/*.js.map` 与 `src`;带 Typert Remote 导航的包还会列出 `lib/typert.remote-client.d.ts.map`,其生成的声明通过 `sourceMappingURL` 引用该映射。没有任何载荷成员被禁止,发布清单由静态 manifest 检查精确持有。332 个发布成员中已有 315 个声明过 `./src/*`,该导出现在解析到这些文件,服务于编辑器导航与阅读;纯 Node 仍需 TypeScript loader 才能 import 它们。
工作区构建与 client 预设会压缩每个产出的 bundle(`minify: { compress: true, mangle: false }`)。标识符保持原名,因此栈帧与已发布的 map 仍然指向原始代码。
client 预设会压缩浏览器 bundle 的文本(`minify: { compress: false, mangle: false }`)。标识符保持原名,因此栈帧与已发布的 map 仍然指向原始代码。Node 侧入口不压缩:浏览器 worker 镜像只把 `yield*` 当作整条语句下沉,并按源码版式重写 `for await` 循环,压缩后的输入会同时破坏这两者。
`@deepseek-ai/dsh` 通过 [`scripts/stage-dsh-docs.ts`](../../../../scripts/stage-dsh-docs.ts) 在自己的 `prepack` 生命周期里把文档网站的页面集合暂存到 `apps/cli/docs`,该脚本读取 [`website/docs.ts`](../../../../website/docs.ts) 中的发布清单,并在 `postpack` 中再次删除这份副本。`docs/` 下的非 Markdown 资产随这些页面一起发布;生成的 `docs/persistence-changes/` 记录与 `docs/i18n/` 规范不发布。vendored 族保持上游 payload 不变([发布序列](2026-08-10-npm-release-sequences.zh.md))。
@@ -30,11 +30,11 @@ Status: implemented
**连标识符一起混淆。** 否决:混淆会破坏已发布 map 本应提供的对应关系,并使未处理栈帧无法阅读。
**只压缩 `lib/index.js` 与 `lib/client.js`。** 否决:一次工作区构建配置产出所有 bundle,为单包开例外需要第二套配置,而读者看不到任何收益。
**连 Node 侧入口一起压缩。** 否决:浏览器 worker 镜像拒绝压缩后的输入——打包器会拒绝被压缩器挪进表达式的 `yield*`,并且对 `dsh-agent-instructions` 产出无法解析的代码。这两条都是下沉变换的限制,不是体积上的理由。
## Consequences
`src` 在整个发布集上增加约 29 MB,声明映射增加约 10 MB,JavaScript map 增加约 49 MB,暂存的文档给 `@deepseek-ai/dsh` 增加 5.9 MB。压缩让 host bundle 大约减半——`dsh-session` 的 `lib/index.js` 从 95 kB 降到 46 kB——client bundle 约减少三分之一。桌面应用与 Python 可执行体保持原有载荷。
`src` 在整个发布集上增加约 29 MB,声明映射增加约 10 MB,JavaScript map 增加约 49 MB,暂存的文档给 `@deepseek-ai/dsh` 增加 5.9 MB。压缩让浏览器 bundle 约减少三分之一——`ui-chat` 的 `lib/client.js` 从 591 kB 降到 420 kB——而 Node 侧入口在 worker 镜像接受压缩输入之前保持原体积。桌面应用与 Python 可执行体保持原有载荷。
消费方编辑器现在可以从声明跳转到已安装的实现,插件作者无需检出即可读到该版本的文档。暂存副本保留仓库的相对链接,因此从随包页面指向未随包目录的链接——`../packages/**`,或被排除的 `persistence-changes/` 记录——只在检出中可解析。`docs/` 只在 `prepack` 与 `postpack` 之间存在于工作树,`pnpm run clean` 会清掉被中断的打包留下的副本。
+6 -3
View File
@@ -486,9 +486,12 @@ function clientConfig(id: string, entry: string, clientBanner?: (fileName: strin
// Plugin code is fetched outside Vite's module graph, so its own bundle
// must carry the TS/TSX mapping consumed by browser profiling tools.
sourcemap: true,
// Compress only: identifier names stay readable in the shipped bundle,
// and the source map still resolves through the published `src`.
minify: { compress: true, mangle: false },
// Compress the browser bundle's text while identifier names stay readable
// and the source map still resolves through the published `src`. The
// compressor stays off: the browser worker image lowers `yield*` only as a
// whole statement, and a compressor that folds statements into expressions
// hides it.
minify: { compress: false, mangle: false },
clean: false,
deps: {
neverBundle: isRequested,
+3 -2
View File
@@ -76,8 +76,9 @@ describe('client bundle dynamic imports', () => {
})
const chunks = builds.flatMap(bundle => bundle.chunks).filter(chunk => chunk.type === 'chunk')
const output = chunks.find(chunk => chunk.fileName === 'client.js')?.code
expect(output).toContain('require.async("./client.terminal.js")')
expect(output).not.toContain('Promise.resolve().then(() => require("./client.terminal.js"))')
// The compressor rewrites string literals, so the quotes are not part of the contract.
expect(output).toMatch(/require\.async\((["'`])\.\/client\.terminal\.js\1\)/)
expect(output).not.toMatch(/Promise\.resolve\(\)\.then\(\(\)\s*=>\s*require\((["'`])\.\/client\.terminal\.js\1\)\)/)
} finally {
for (const bundle of builds) await bundle[Symbol.asyncDispose]()
}
+6 -4
View File
@@ -307,16 +307,18 @@ describe('release pack CLI', () => {
it('validates every parallel tarball before recording the publish order', async () => {
const root = fixture()
packageFixture(root, 'packages/util/alpha', '@deepseek-ai/alpha')
packageFixture(root, 'packages/util/zebra', '@deepseek-ai/zebra', { files: ['lib/index.js', 'lib/client.js.map'] })
write(join(root, 'packages/util/zebra/lib/client.js.map'), '{"version":3}\n')
packageFixture(root, 'apps/cli', '@deepseek-ai/dsh', { files: ['lib/index.js', 'src/bin.ts'] })
packageFixture(root, 'packages/subagent/subagent-claude-code', '@deepseek-ai/dsh-subagent-claude-code')
packageFixture(root, 'packages/subagent/subagent-codex', '@deepseek-ai/dsh-subagent-codex')
write(join(root, 'apps/cli/src/bin.ts'), 'export const bin = 1\n')
write(join(root, 'apps/web/dist/index.html'), '<main></main>')
writeClientBuildRecord(root, officialClientBuildEnvironment(root, { DSH_CLIENT_COMMIT_HASH: commit }))
const result = await runPack(root, ['--family', 'dsh', '--concurrency', '8'])
expect(result.status).not.toBe(0)
expect(result.stderr).toContain('@deepseek-ai/zebra publishes JavaScript map package/lib/client.js.map')
expect(readdirSync(join(root, 'packed')).filter(name => name.endsWith('.tgz'))).toHaveLength(2)
expect(result.stderr).toContain('@deepseek-ai/dsh packed no documentation')
expect(readdirSync(join(root, 'packed')).filter(name => name.endsWith('.tgz'))).toHaveLength(4)
expect(existsSync(join(root, 'packed', PUBLISH_ORDER_FILE))).toBe(false)
})
-3
View File
@@ -30,9 +30,6 @@ export default defineConfig(({ env }) => {
fixedExtension: false,
dts: false,
clean: false,
// Published packages carry their source and its maps, so the shipped
// JavaScript is compressed for size and identifier names stay readable.
minify: { compress: true, mangle: false },
plugins: client ? [] : [typertPlugin({ mode: 'workspace', faces: ['host'] })],
}
})