Skip to content

fix(client): ship lib/client.js as the DSH browser bundle, not tsc output - #11

Open
JosephTian876 wants to merge 1 commit into
PerryLink:mainfrom
JosephTian876:fix/client-browser-bundle
Open

JosephTian876 wants to merge 1 commit into
PerryLink:mainfrom
JosephTian876:fix/client-browser-bundle

Conversation

@JosephTian876

Copy link
Copy Markdown

What this fixes

@perrylink/dsh-github@0.7.12 publishes exports["./client"].default — lib/client.js — as the Node-side tsc compile of src/client.ts: ordinary ESM with top-level import/export.

The DSH web shell does not import that file as a module. @deepseek-ai/dsh-client-modules installs it with document.createElement('script') and expects a classic script that hands its factory to window.__ModuleLoader__.load({ id, factory }). The browser therefore throws:

Uncaught SyntaxError: Cannot use import statement outside a module
  @ plugins/??…,@perrylink/dsh-github/client.js,…&rev=…

and because the host concatenates several packages into one combo script per batch, that parse error aborts the whole batch. Every client entry in the batch stays unactivated and the UI reports:

Failed to load plugins
@perrylink/dsh-github
web boot: N entries did not activate

Reproduction

Clean headless Edge against a local DSH 0.1.7-alpha.2, same profile, only the artifact swapped:

window errors failing batches Plugins page
0.7.12 artifact SyntaxError: Cannot use import statement outside a module ×3 1 Failed to load plugins
this branch none 0 card mounts

The bytes the host serves for this package are verified byte-identical to the built artifact (modulo the host's own ;\n separator and rewritten sourceMappingURL), and the host's composed source map still carries this bundle's mappings.

The change

pnpm build now emits the browser half in the loader's shape:

window.__ModuleLoader__.load({ id: "@perrylink/dsh-github", factory: (require) => {
"use strict";
var module = { exports: {} };
var exports = module.exports;
// …bundled module…
return module.exports;
} });
  • react and @deepseek-ai/dsh-client-ui-primitives stay bare require(...) calls — they are rows of the shell's frozen module table — and everything else is inlined.
  • A bare specifier in the shared-runtime families (@deepseek-ai/cordis, @deepseek-ai/dsh-client-*) that is not a table row now fails the build with an actionable message, instead of inlining a second copy of a singleton the shell already holds.
file role
scripts/client-bundle.mjs the artifact contract: module-table externals, the registration wrapper, and the checks that prove a produced file has it
scripts/build-client.mjs the build step, also exposed as pnpm build:client
scripts/prepare.mjs requires typescript and esbuild before it compiles anything — tsc alone overwrites a good committed lib/client.js with plain ESM — and the no-devDependencies git-install fallback now accepts committed artifacts only when they are a real bundle
scripts/verify-artifacts.mjs, test/client-bundle.test.ts execute the shipped file the way the shell does: as a classic script, against a stub window.__ModuleLoader__

node --check cannot catch this: the package is "type": "module", so Node parses the file as ESM and the import is legal. Executing it as a classic script reproduces the browser's own SyntaxError — which is what the new guard does, and it fails loudly when the 0.7.12 artifact is put back.

esbuild joins devDependencies. The lockfile gains only its importer row: the package was already resolved there as a vitest dependency, and pnpm-workspace.yaml already allowlists its postinstall, so no workspace-config change is needed.

lib/client.js and lib/client.js.map are rebuilt and committed, as this repository already does for every artifact. The cordis plugin face (apply, inject) and the published ./client types are unchanged.

Verification

  • pnpm install --frozen-lockfile, typecheck, typecheck:ci, test (189 passed, 3 skipped), test:coverage, lint, build, verify:self-contained, verify:artifacts, pack, check:readmes — all green.
  • Build is deterministic; a fresh prepare + build leaves git status clean, so the committed artifact is exactly what the build produces.
  • Fault injection: restoring the 0.7.12 artifact makes verify:artifacts, test/client-bundle.test.ts, and prepare (git-install fallback) all fail with the browser's own SyntaxError.
  • End-to-end: the packed tarball installed into an isolated DSH profile and driven by headless Edge — 0 window errors, 0 failing batches, card mounted; the same profile with the 0.7.12 artifact reproduces the reported failure.

Follow-up (not in this PR)

lib/types/client.d.ts — the hand-authored published ./client type face — still declares the removed settingsScope member that src/client.ts dropped in 0.7.12.

@JosephTian876

Copy link
Copy Markdown
Author

Rebased onto current main (2111b20, v0.7.15) — the branch is conflict-free again.

The conflicts were all in files this PR replaces:

  • CHANGELOG.md — the entry now sits under [Unreleased], above the 0.7.15/0.7.14 sections.
  • lib/client.js / lib/client.js.map — these are build artifacts, so I regenerated them with scripts/build-client.mjs rather than merging the text. That matters: the old artifact still carried IconLoadingOutline16, which 0.7.14 removed, so it would have shipped a call to an identifier the current primitives package no longer exports.

Verified on the rebased commit:

  • pnpm test — 189 passed, 3 skipped
  • pnpm typecheck — clean
  • pnpm lint — 0 errors (12 warnings, all pre-existing)
  • node scripts/verify-artifacts.mjs — OK
  • git merge-tree against main — no conflicts

Meanwhile the defect is still live. Re-measured across published and in-tree artifacts:

ref top-level import/export __ModuleLoader__.load
0.7.13 12 0
0.7.14 12 0
0.7.15 (latest) 12 0
current main 12 0

It has now been reported independently in #12 (host 0.1.5-rc.3, market install) — same Failed to load plugins on the next host restart. I hit it on 0.1.7-rc.2, and on a market-managed profile it also comes back after any reinstall or update, because the rollback restores the npm artifact over the local fix.

Nothing about the fix itself changed in the rebase — it just needed to be mergeable again. Happy to adjust if you'd like it split or scoped differently.

valing1837 pushed a commit to valing1837/dsh-plugin-github that referenced this pull request Oct 1, 2026
A local (`uses: ./`) action needs a checkout first, so the workflow checks out
and then calls the action with fail-on: error. It doubles as the action's
integration test: the next pull request here exercises the composite wiring,
the diff fetch, the analysis and the review posting for real.

Also worth recording: the action's two `run:` scripts were executed for real
locally through Git Bash against a live pull request (PerryLink/dsh-github#11,
10 files, +762/-301). The empty-pull-request guard exits 1, the diff fetch
returns 85 KB, the analysis reports 0 error / 3 warning / 2 note, GITHUB_OUTPUT
receives errors/warnings/notes plus the findings heredoc, and the token appears
in neither step's output. The one warning on that pull request is a true
positive: `new Function('window', 'require', 'module', 'exports', code)`.
@PerryLink

Copy link
Copy Markdown
Owner

谢谢提交. 已复核: 产物 sha256 一致, main 仍复现 (ModuleLoader 为 0), 冲突仅 CHANGELOG.md. 仲裁取这条: external 共享运行时更稳, 不带第二份单例. 席位让 #14 收窄, settingsScope 我来补. 要我接手也行.

…tput

`exports["./client"].default` published the Node-side `tsc` compile of
`src/client.ts` — top-level `import`/`export` — but `@deepseek-ai/dsh-client-modules`
does not import that file as a module. It installs it with
`document.createElement('script')` and expects the classic script to hand its
factory to `window.__ModuleLoader__.load({ id, factory })`. The browser throws
`SyntaxError: Cannot use import statement outside a module` at the file, and
because the host concatenates several packages into one combo script per batch,
that parse error aborts the whole batch: every client entry in it stays
unactivated and the UI reports **"Failed to load plugins"** naming this package.

Reproduced in a clean headless Edge against a local `0.1.7-alpha.2` host before
the change (the SyntaxError lands on the batch URL, then
`web boot: 1 entry did not activate / @perrylink/dsh-github: import failed`), and
absent after it: every served batch parses as a classic script, the page boots
with zero window errors, and the card mounts.

`pnpm build` now emits the artifact in the loader's shape:

    window.__ModuleLoader__.load({ id: "@perrylink/dsh-github", factory: (require) => { ... } })

with `react` and `@deepseek-ai/dsh-client-ui-primitives` left as `require(...)`
for the shell's module table, and everything else inlined. A bare specifier in
the shared-runtime families (`@deepseek-ai/cordis`, `@deepseek-ai/dsh-client-*`)
that is not a table row fails the build instead of inlining a second copy of a
singleton the shell already holds.

- `scripts/client-bundle.mjs` — the artifact contract: module-table externals,
  the registration wrapper, and the checks that prove a produced file has it.
- `scripts/build-client.mjs` — the build step, also exposed as `pnpm build:client`.
- `scripts/prepare.mjs` — requires `typescript` **and** `esbuild` before it
  compiles anything, because `tsc` alone overwrites a good committed
  `lib/client.js` with plain ESM; the git-install fallback (no devDependencies)
  now accepts committed artifacts only when they are a real bundle.
- `scripts/verify-artifacts.mjs` and `test/client-bundle.test.ts` — execute the
  shipped file the way the shell does, as a classic script against a stub
  `window.__ModuleLoader__`, so a leftover `import` fails with the browser's own
  `SyntaxError` rather than passing a text scan. `node --check` cannot see this:
  the package is `"type": "module"`, which makes that parse the file as ESM.

`esbuild` joins `devDependencies`. The lockfile gains only its importer row —
the package was already resolved there as a vitest dependency, and
`pnpm-workspace.yaml` already allowlists its postinstall.

`lib/client.js` and `lib/client.js.map` are rebuilt and committed, as this
repository already does for every artifact. The cordis plugin face (`apply`,
`inject`) and the published `./client` types are unchanged.
@JosephTian876
JosephTian876 force-pushed the fix/client-browser-bundle branch from 46d60ac to 6ad589e Compare October 8, 2026 13:06
@JosephTian876

Copy link
Copy Markdown
Author

重写后已推到 fix/client-browser-bundle,新 head 6ad589e。本轮只做三件你点名范围内的事:rebase 到当前 main、修 CHANGELOG 冲突、重建产物。src/ 一个字节没动(git diff --name-only origin/main HEAD -- src/ 为空,src/client.ts blob 与 main 相同)—— 席位和 settingsScope 都留给你,我没有碰。

1. Rebase

基点 2111b20 → 现在 5bd360b(v0.7.19),跨 16 个提交。冲突如你所说只有 CHANGELOG.md:main 侧的 0.7.18/0.7.17/0.7.16 三段原文一字未改,两条 Fixed 挂到 ## [Unreleased] 之下。git diff origin/main -- CHANGELOG.md 只剩本 PR 的新增。

rebase 后 mergeable 从 CONFLICTING 变成 MERGEABLE。

2. 产物:与上轮你复核过的那份逐字节相同

这点是对你 sha256 复核的回应——重写没有让它失效:

blob
rebase 前 46d60ac:lib/client.js 62bcb7f1…
rebase 后 6ad589e:lib/client.js 62bcb7f1…

pnpm run build:client 与整套 pnpm build 跑完,git status --porcelain 都是空的 —— 构建是确定性的,产物确实由 scripts/build-client.mjs 生成,不是手工塞进去的。

pnpm install --frozen-lockfile 一次通过,没有动 lockfile(esbuild 的 packages/snapshots 条目 main 上早就有,只补了 importers 3 行)。

3. 独立验收:把产物当 classic script 实跑

CI 那 4 项暂时跑不起来(见第 5 点),所以我自己做了同位置的对照实验 —— 用 node:vm 以 classic script 语义执行,注入 stub __ModuleLoader__:

修复后 6ad589e main 现状
classic script 解析 无错 SyntaxError: Cannot use import statement outside a module
load 调用 1 0
注册 id @perrylink/dsh-github —
factory function,执行无错 —
导出 10 个符号,apply/inject 齐全 —
顶层 import/export 0 12
__ModuleLoader__ 1 0

另外做了一次 batch 拼接验证(把产物和邻居 bundle 拼进同一个 classic script):修复后文件在前或后都无错、两个 plugin 都注册成功;对照组则 SyntaxError 且邻居也一起注册失败 —— 这就是「一个坏文件拖垮整批 combo script → Failed to load plugins」的机制本身。

顺带核对了两个 externals 确实在宿主模块表里:packages/client/web/src/platform.ts 的 PLATFORM_MODULES 同时含 react 和 @deepseek-ai/dsh-client-ui-primitives,而 packages/client/modules/src/client/system.ts:222 正是那句 loaded without registering "..." via __ModuleLoader__.load —— 契约对得上。

本地跑完 CI 全部命令,退出码 0:typecheck、typecheck:ci、test(189 passed / 3 skipped)、test:coverage、lint(12 warnings / 0 errors)、build、verify:self-contained、verify:artifacts、pack、check:readmes。pnpm pack 出来的 tgz 内 lib/client.js 与仓库一致,scripts/ 五个脚本都在包内(git 安装能跑到新 prepare)。

4. 你的仲裁我都照做了,顺带一个提醒

「external 共享运行时更稳,不带第二份单例」—— 产物里只 require(...) 了 react 和 @deepseek-ai/dsh-client-ui-primitives 两个模块表行,没有内联任何一份单例,client-bundle.mjs 对模块表之外的 @deepseek-ai/dsh-* 裸说明符是直接 fail 构建而不是内联。

#14 那条我看过了,它把修复和席位改动捆在一起、还把 manifest 的 host 线从 0.2.1-alpha.1 退回了 0.2.0-rc.2,收窄成纯席位确实更干净,你说接手我没意见。

5. 唯一需要你动一下的地方:CI 在等你点批准

push 之后三个 workflow 起来了,但状态是 action_required,0 秒、没有执行任何步骤 —— 这是 fork PR 的首次运行审批,需要维护者在 Actions 页面点一次 "Approve and run"。

上轮 10-05 那次能跑出结果,是因为你亲手批过(那批 run 的 triggering_actor 是 PerryLink)。我这边对该仓库只有 pull 权限,不会去伪造批准。

顺带说明:上一轮那个 profile 红叉不是本改动引起的,是分支里 compat.yml 太旧(还钉着 0.1.6-alpha.2)。这个文件现在与 main 完全相同了(git diff origin/main HEAD -- .github/workflows/compat.yml 为空),rebase 已经把你 10-05 那次修的断言带了进来,所以批准后应该直接绿。

6. 一个我在自己机器上发现的低危问题,不影响本 PR

Windows + core.autocrlf=true 下 pnpm build 会让 lib/client.js.map 变 dirty:map 里内嵌的 sourcesContent[0] 是 CRLF 版 src/client.ts,而仓库提交的是 LF 版(mappings 相同,仅内嵌源码行尾不同)。CI 在 ubuntu 上不暴露,lib/client.js 本身也不受影响。

根治办法是加一条 .gitattributes(例如 *.map -text)或在构建后归一化 sourcesContent。这超出本 PR 的范围,我没擅自加 —— 要的话我可以另开一个 PR。

7. 我明确没有验证的部分

  • 没在真实浏览器里验证。 vm + stub 精确复现了 classic-script 解析这一点契约,但没验证真实 web shell 的 combo 组装、模块表注入和 slots 渲染链路,也没有对着跑起来的 DSH 确认卡片真的出现在 Plugins 页。
  • 没做 dsh plugin add github:... 的端到端安装,git 安装路径下 prepare 的行为是推理(有完整源码树、tsconfig.json 在),不是实测。
  • 全部在本机 Windows + node v24.18.0 + pnpm 11.7.0 下完成;CI 是 ubuntu + node 22,未交叉验证。

@ljyzww

ljyzww commented Oct 10, 2026

Copy link
Copy Markdown

补充一个 0.2.0-rc.2 宿主上的验证数据点,以及 0.7.20 的产物核对结果,供合并决策参考。

0.7.20 仍未包含本 PR 的修复

对比 npm 上的 0.7.19 与 0.7.20 两个发布包:

项目 结果
lib/client.js 大小 12,473 字节(与 0.7.19 相同)
顶层 import 语句 2 条
__ModuleLoader__ 出现次数 0 次
lib/ 目录(77 个文件) 与 0.7.19 逐字节一致(SHA256 比对)
package.json 的 build 仍是 tsc --noEmitOnError && node scripts/fix-dts.mjs,未引入打包步骤

也就是说,从 0.7.13 到 0.7.20 的所有已发布版本,客户端产物都还是 tsc 直出的裸 ESM。

本 PR 的产物在 0.2.0-rc.2 上实测可用

  • 环境:DSH Desktop 2.0.17 / 内核 @deepseek-ai/dsh 0.2.0-rc.2 / Windows
  • 取 6ad589e 的 lib/client.js(blob 哈希与 PR 中复核的一致 62bcb7f1…,10,898 字节,__ModuleLoader__.load 出现 1 次),仅替换该单个文件,以 file: 依赖装进 desktop profile
  • 结果:启动日志无 RendererStartupFailure;插件注册的 GitHub 工具可正常调用(gh_repo 返回真实仓库数据);产物只 require react 与 @deepseek-ai/dsh-client-ui-primitives,两者都是 0.2.0-rc.2 冻结模块表里已有的行(同机上另有插件在 require 相同的名字)
  • 设置卡在 Plugins 页的 Official 组正常渲染,并成功读到凭证、显示「已配置令牌」

作为对照:同一台机器上从市场安装 0.7.19 时,直接导致 renderer 引导失败、应用进入恢复模式(RendererStartupFailure: Renderer boot failed for 1 plugin(s));换成本 PR 的产物后恢复正常。

这个数据点说明本 PR 的修复对 0.2.0-rc.2 同样有效,应该可以直接合并。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants