Skip to content

fix(client): register the browser half with the module loader - #14

Open
womeimingzi11 wants to merge 2 commits into
PerryLink:mainfrom
womeimingzi11:fix/client-module-loader
Open

womeimingzi11 wants to merge 2 commits into
PerryLink:mainfrom
womeimingzi11:fix/client-module-loader

Conversation

@womeimingzi11

@womeimingzi11 womeimingzi11 commented Oct 1, 2026 •

Copy link
Copy Markdown

Symptoms

Two defects in the browser half, both visible on a DSH 0.2.0-rc.2 desktop profile.

1. The Plugins page reported the plugin as failed to sync.

@perrylink/dsh-github: Error: client-modules: could not load "@perrylink/dsh-github":
plugins/??@perrylink/dsh-github/client.js&rev=…: loaded without registering
"@perrylink/dsh-github" via __ModuleLoader__.load

2. The configuration card appeared in the page's Official group, presented as an
official plugin rather than an installed one.

The host half was unaffected throughout: the bundle row mounts, gh_* / pr_* /
issue_* stay on the roster, and real GitHub reads answer.

Root causes

1. The bundle never registered. lib/client.js is verbatim tsc output — a
plain ESM module with top-level import / export bindings and no registration
call. The host's client module system (@deepseek-ai/dsh-client-modules) is a
lazy CJS table:

executing a plugin bundle only REGISTERS its factory
(window.__ModuleLoader__.load({id, factory}))

So the bundle executes, registers nothing, and the loader rejects the row. The
build never produced the contract: build was tsc … && node scripts/fix-dts.mjs
with no wrapping step, and verify-artifacts.mjs only ever executed the host face.

lib/client.js is identical to the committed artifact in this repository and to
the published tarball, so neither a reinstall nor a restart changes anything.

2. The card took the wrong configuration seat. It registered on
plugins.item, which the page renders through
renderGroup("official", …) — the Official group, where the host's own settings
plugins (agent-loop, shell, subagent, web-search) mount their cards. The row
configuration seat is plugins.row.config, keyed <package>#<rowId>.

Both seats hand the card the same props ({ view, form }; submit and the
snapshot hook come from the card's own inject), so this is a change of seat, not
of card. plugins.bundle.config is not the alternative: it is rendered with
{ view } only — no form — and this card reads tokenRef from the form.

Changes

  • scripts/build-client.mjs (new) rewrites the compiled bundle into the
    loader's factory: import … from 'm' → require('m'), each top-level export
    declaration loses its keyword and gains an exports.<name> = <name> line, and
    the body is wrapped. It fails loud on any shape it cannot transform and is a
    no-op on an already-wrapped bundle, so both build and prepare call it.
    Original line order is preserved and lib/client.js.map is shifted by the
    wrapper's line count, so the shipped map stays aligned.
  • The card takes plugins.row.config, keyed by this bundle's own row.
    PACKAGE_NAME / ROW_ID / ROW_CONFIG_KEY are stated once so the seat key and
    cordis.patch.yml cannot drift apart.
  • dsh.client.inject gains the two modules the bundle actually requires
    (react, @deepseek-ai/dsh-client-ui-primitives); neither was declared, so
    the module graph did not guarantee they arrive first.
  • scripts/verify-artifacts.mjs now also runs the shipped client bundle
    against a stub __ModuleLoader__, asserts it registers exactly once under the
    package name, materializes its factory, and rejects top-level ESM.
  • test/client-bundle-contract.test.ts (new) asserts the module-loader
    contract as a unit test; test/client-card-seat.test.ts (new) runs apply
    against a recording stub context and pins the seat and its key, deriving the row
    id from cordis.patch.yml rather than restating it.
  • DSH 0.2 is declared and exercised, since these fixes are what make the
    browser half work there: engines.dsh and the @deepseek-ai/dsh-* peer union
    gain || >=0.2.0-rc.1 <0.3.0-0, dshWorkshop.compatibility.dshVersions
    records 0.2.0-rc.2, the five READMEs follow, and the Compat workflow's
    profile job runs both ends of the band (0.1.7-rc.2, 0.2.0-rc.2) instead
    of a single pin.

Verification

  • Host-served wire on 0.2.0-rc.2 — the strongest check, since it runs the
    bytes the browser receives. The batch bundle the Plugins page actually requests
    (/plugins/??…,@perrylink/dsh-github/client.js,…, the URL in the reported
    error) serves and executes to seven registered modules with
    @perrylink/dsh-github among them, materializing apply and inject; the
    single-row fallback URL registers the same row, and running its apply against
    a recording context registers exactly ["plugins.row.config"] with key
    @perrylink/dsh-github#dsh-github and carries no plugins.item at all.
  • Host half on 0.2.0-rc.2 — /api/pluginInventory/list reports the row
    fiberPhase: "active" with no failure diagnostic, and a real gh_repo call
    returns repository data.
  • Unit — pnpm test: 21 files, 192 passed / 3 skipped.
  • Gates — pnpm run build, verify:artifacts, check:readmes, lint,
    typecheck, and pnpm install --frozen-lockfile all green.

Notes for review

  • lib/client.js is a generated artifact; its diff is dominated by the factory
    indentation applied to every line. scripts/build-client.mjs,
    test/client-bundle-contract.test.ts and test/client-card-seat.test.ts are
    the readable part.
  • The same acceptance test and the same message are present in
    @deepseek-ai/dsh-client-modules@0.1.7-rc.2, so the registration defect is in
    how the artifact is built rather than a 0.2-only regression. It may be worth
    confirming whether the committed bundle ever registered on the older line.
  • I only widened the declared ranges to >=0.2.0-rc.1 <0.3.0-0, matching the
    shape the ecosystem already uses; 0.3 stays out.

`lib/client.js` shipped as verbatim `tsc` ESM output: top-level `import` /
`export` bindings and no `window.__ModuleLoader__.load({ id, factory })` call.
The host's client module system (`@deepseek-ai/dsh-client-modules`) is a lazy
CJS table whose only acceptance test is that registration, so the bundle
executed, registered nothing, and the Plugins page reported

  client-modules: could not load "@perrylink/dsh-github": ... :
    loaded without registering "@perrylink/dsh-github" via __ModuleLoader__.load

The host half was unaffected and kept its tools registered throughout.

- `scripts/build-client.mjs` rewrites the compiled bundle into the loader's
  factory: `import ... from 'm'` becomes `require('m')`, each top-level `export`
  declaration loses its keyword and gains an `exports.<name>` line, and the body
  is wrapped. It fails loud on any shape it cannot transform and is a no-op on an
  already-wrapped bundle, so both `build` and `prepare` call it. Original line
  order is preserved and the shipped source map is shifted by the wrapper's line
  count, so it stays aligned.
- `dsh.client.inject` gains the two modules the bundle actually requires
  (`react`, `@deepseek-ai/dsh-client-ui-primitives`), neither of which was
  declared.
- `scripts/verify-artifacts.mjs` executed only the host face; it now also runs
  the shipped client bundle against a stub `__ModuleLoader__` and asserts
  registration, materialization and the absence of top-level ESM.
- `test/client-bundle-contract.test.ts` asserts the same contract as a unit test.

Declares and tests the DSH 0.2 host line in the same change, because this fix is
what makes the browser half work there: the `@deepseek-ai/dsh-*` peer union and
`engines.dsh` gain `|| >=0.2.0-rc.1 <0.3.0-0`, `dshWorkshop.compatibility.
dshVersions` records `0.2.0-rc.2`, the five READMEs follow, and the Compat
workflow's profile job now runs both ends of the band instead of one pin.

Verified on `0.2.0-rc.2`: the batch bundle the Plugins page requests
(`/plugins/??...,@perrylink/dsh-github/client.js,...`) serves and executes to
seven registered modules with this row among them, the single-row fallback URL
registers the same row, and `pnpm test` reports 189 passed / 3 skipped.
The browser half took the page's `plugins.item` seat. That list is what the
Plugins page renders through `renderGroup("official", …)` — the Official group,
where the host's own settings plugins (agent-loop, shell, subagent, web-search)
mount their cards — so this community bundle was presented as an official plugin
instead of an installed one, while its bundle row sat in the Installed group
with no configuration of its own.

The card now takes this bundle row's seat, `plugins.row.config`, keyed
`<package>#<rowId>` — the key the page's own `rowConfigKey` helper defines. Both
seats hand the card the same props (`{ view, form }`; `submit` and the snapshot
hook come from the card's own `inject`), so the component is unchanged.

`plugins.bundle.config` is not the alternative: that seat is rendered with
`{ view }` only — no `form` — and this card reads `tokenRef` from the form.

- `PACKAGE_NAME` / `ROW_ID` / `ROW_CONFIG_KEY` are stated once so the seat key
  and `cordis.patch.yml` cannot drift apart.
- `test/client-card-seat.test.ts` runs `apply` against a recording stub context
  and asserts the bundle registers `plugins.row.config` under the key derived
  from the patch, and never touches `plugins.item`.

Verified against the real host on `0.2.0-rc.2`: the bundle the server returns for
this row registers exactly `["plugins.row.config"]` with key
`@perrylink/dsh-github#dsh-github`, and carries no `plugins.item` at all.
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.

1 participant