diff --git a/CHANGELOG.md b/CHANGELOG.md index 011cd63..3dd0dd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed + +- **Ship `lib/client.js` as the browser bundle the DSH web shell can install, not as `tsc` output.** `0.7.12` published the Node-side compile of `src/client.ts` — top-level `import`/`export` — at `exports["./client"].default`, but `@deepseek-ai/dsh-client-modules` installs that file with `document.createElement('script')` and concatenates several packages into one combo script per batch. The browser therefore threw `SyntaxError: Cannot use import statement outside a module`, the whole batch failed to parse, and every client entry in it stayed unactivated behind a **"Failed to load plugins"** page naming `@perrylink/dsh-github`. `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 (`scripts/client-bundle.mjs`, `scripts/build-client.mjs`) — and `esbuild` joins the devDependencies for it. The previously published `lib/client.js` was a `tsc` artifact, so this changes a shipped file; the cordis plugin face (`apply`, `inject`) and the `./client` types are unchanged. +- **Fail the build, install, and CI instead of shipping that artifact again.** `scripts/prepare.mjs` now requires *both* `typescript` and `esbuild` before it compiles anything — running `tsc` alone overwrites a good committed `lib/client.js` with plain ESM — and accepts committed artifacts only when they are a real bundle. `scripts/verify-artifacts.mjs` and the new `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 same `SyntaxError` the browser reports, and a bare specifier the module table cannot answer fails as well. + ## [0.7.15] - 2026-09-25 ### Changed @@ -32,6 +37,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Append the fourth host clause `|| >=0.1.7-0 <0.2.0` to `engines.dsh` and to all thirteen `@deepseek-ai/dsh-*` peer ranges, and raise the `@deepseek-ai/cordis` peer and dev/test pin to `^4.0.4`. Under semver's prerelease rule a range whose only prerelease comparators sit on earlier tuples cannot admit a later alpha, so the three-clause band excluded the very host line this release targets. No previously supported host line is dropped. - Move the `pnpm-workspace.yaml` `overrides` block with the pins. Its four self-referential rows exist so a transitive peer's prerelease request resolves onto ONE copy of the host type graph instead of a second one; their values now follow the devDep pin to `0.1.7-alpha.2`. The KEYS stay the ranges a transitive peer spells (`@deepseek-ai/dsh-agent@^0.1.7-alpha.1`, `@deepseek-ai/dsh-llm@^0.1.7-alpha.1`, `@deepseek-ai/dsh-llm@^0.1.2-alpha.3`, `@deepseek-ai/dsh-llm@^0.1.2-alpha.4`) — they match what asks, while the value is what it resolves to. Moving the devDeps alone would have left those requests resolving to the previous line and split this package's `UserMessage`/`ContentBlock` from the host's `Agent`, which is the failure the block was written for. + ## [0.7.12] - 2026-09-22 ### Changed diff --git a/lib/client.js b/lib/client.js index 8a2425e..62bcb7f 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,291 +1,314 @@ -/** - * dsh-github browser half — one card in the Plugins page's Official group. - * - * The card reports and edits the GitHub token. The token literal never rides - * the settings document: it is written through the credentials domain, - * addressed by the reference the plugin's cordis.yml `Config.tokenRef` names - * (default `GITHUB_TOKEN`), which is exactly where the host half resolves it — - * per operation, no restart needed. - * - * Since host 0.1.7-alpha.1 the page owns configuration: the card renders the - * `summary` one-liner and the `page` body from the `plugins.item` owner props - * (`view`, and the `form` the Plugins page supplies), instead of binding a - * `settingsScope` itself — that service, `settings.plugin.item`'s old shape, - * and the `dsh-settings-file` package behind them were all removed. The card - * therefore reads `tokenRef` from `form.state.value` and never writes the - * settings document; the credential is written through the credentials - * domain, which is a different seam and stayed. - * - * The shipped `lib/client.js` is the built bundle of this module. The browser - * module loader executes that bundle, not this file. - * @module @perrylink/dsh-github/client - */ -import { createElement, useState } from 'react'; -import { Button, IconLoadingOutlineRegular, } from '@deepseek-ai/dsh-client-ui-primitives'; -/** - * Create the minimal card store. - * @param init - initial state. - * @returns the store. - */ -export function createSnapshotStore(init) { - let state = init; - const listeners = new Set(); - return { - getSnapshot: () => state, - subscribe: (listener) => { - listeners.add(listener); - return () => { listeners.delete(listener); }; - }, - set: (next) => { - state = next; - for (const listener of [...listeners]) - listener(); - }, - }; -} -/** Namespace of the GitHub capability. Spelled here rather than imported: a client package must not depend on a Host package. */ -export const GITHUB_NS = 'dsh-github'; -/** Credential reference the provider resolves when the configuration names none. */ -export const DEFAULT_TOKEN_REF = 'GITHUB_TOKEN'; -/** English copy. */ -export const en = { - githubTitle: 'GitHub', - githubDescription: 'GitHub pull requests, issues, and CI through the agent.', - tokenLabel: 'GitHub token', - tokenHint: 'Stored in the credentials file, not here. Applied immediately; leave blank to keep the current token.', - tokenSet: 'A token is configured.', - tokenUnset: 'No token is configured; GitHub tools are unavailable until one is.', - unsaved: 'Unsaved', - readOnly: 'This deployment stores settings read-only.', - save: 'Save', - saving: 'Saving…', - discard: 'Discard', - saveFailed: 'The deployment did not accept this value; it was left for you to correct.', +window.__ModuleLoader__.load({ id: "@perrylink/dsh-github", factory: (require) => { +"use strict"; +var module = { exports: {} }; +var exports = module.exports; +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); }; -/** Simplified Chinese copy. */ -export const zh = { - githubTitle: 'GitHub', - githubDescription: '通过 agent 操作 GitHub 的 PR、issue 与 CI。', - tokenLabel: 'GitHub Token', - tokenHint: '写入凭证文件而非设置文件;保存后立即生效。留空表示保持现有令牌。', - tokenSet: '已配置令牌。', - tokenUnset: '未配置令牌;配置之前 GitHub 工具不可用。', - unsaved: '未保存', - readOnly: '本部署的设置为只读。', - save: '保存', - saving: '保存中…', - discard: '放弃修改', - saveFailed: '本部署没有接受该值,已保留供你修改。', +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; }; -/** - * Bridges the page-supplied configuration form and the credentials domain onto - * the card. The token is the one control that does not live in the - * configuration: its literal never rides a response, so the card learns only - * whether one is configured and writes it through the credentials domain, - * addressed by the reference `Config.tokenRef` names. - */ -export class GithubCardController { - form; - api; - store; - saving = false; - failed = false; - credential = { ref: '', configured: false, writable: true }; - /** - * @param form - the configuration form the Plugins page hands this entry, or - * `undefined` when the page supplied none (the card then reads the default - * reference and offers the credential control alone). - * @param api - the credentials Remote namespace. - */ - constructor(form, api) { - this.form = form; - this.api = api; - this.store = createSnapshotStore(this.projection()); - form?.subscribe(() => { - void this.readCredential(); - this.publish(); - }); - void this.readCredential(); - } - /** Whether the Host configuration document accepts writes. */ - writable() { - return this.form?.getSnapshot().writable ?? true; - } - /** The configuration form this controller reads (identity-compared by the caller). */ - formOf() { - return this.form; - } - /** Project the card's full state for its snapshot store. */ - projection() { - return { - writable: this.writable(), - configured: this.credential.configured, - credentialWritable: this.credential.writable, - saving: this.saving, - failed: this.failed, - ref: this.credential.ref, - }; - } - /** The credential reference the configuration names, or the provider default. */ - refOf() { - const declared = this.form?.getSnapshot().value?.tokenRef; - return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF; - } - /** - * Ask the credentials domain about the reference the section names. A - * response is published only while it still answers for the reference in - * force, so two reads settling out of order cannot clobber each other. - */ - async readCredential() { - const ref = this.refOf(); - if (ref !== this.credential.ref) { - this.credential = { ref, configured: false, writable: true }; - this.publish(); - } - let response; - try { - response = await this.api.describe([ref]); - } - catch { - return; - } - if (!response.ok || ref !== this.refOf()) - return; - const view = response.value[ref]; - const next = { ref, configured: view?.configured ?? false, writable: view?.writable ?? true }; - if (next.configured === this.credential.configured && next.writable === this.credential.writable) - return; - this.credential = next; - this.publish(); - } - /** - * Re-read after the Host reports a change to the reference this card - * watches — a token can be written from elsewhere, and the section does not - * change when it is. - * @param ref - the reference the Host reports as changed. - */ - refreshCredential(ref) { - if (ref !== this.credential.ref) - return; - void this.readCredential(); - } - /** - * Write the staged token, then re-read whether the Host now holds one. A - * blank value writes nothing, which keeps the stored key. - * @param value - the staged credential literal. - * @returns whether the Host reports a configured credential afterwards. - */ - async save(value) { - const text = (value ?? '').trim(); - if (text === '' || this.saving) - return false; - this.saving = true; - this.failed = false; - this.publish(); - try { - await this.api.set(this.refOf(), text); - } - catch { - // handled below by re-reading the configured state - } - await this.readCredential(); - const landed = this.credential.configured; - this.saving = false; - this.failed = !landed; - this.publish(); - return landed; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/client.ts +var client_exports = {}; +__export(client_exports, { + DEFAULT_TOKEN_REF: () => DEFAULT_TOKEN_REF, + GITHUB_NS: () => GITHUB_NS, + GithubCard: () => GithubCard, + GithubCardController: () => GithubCardController, + NS: () => NS, + apply: () => apply, + createSnapshotStore: () => createSnapshotStore, + en: () => en, + inject: () => inject, + zh: () => zh +}); +module.exports = __toCommonJS(client_exports); +var import_react = require("react"); +var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); +function createSnapshotStore(init) { + let state = init; + const listeners = /* @__PURE__ */ new Set(); + return { + getSnapshot: () => state, + subscribe: (listener) => { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, + set: (next) => { + state = next; + for (const listener of [...listeners]) listener(); } - /** Build the face the card's slot registration injects. */ - inject() { - return { - hooks: { githubCard: this.store }, - submit: (value) => this.save(value), - }; + }; +} +var GITHUB_NS = "dsh-github"; +var DEFAULT_TOKEN_REF = "GITHUB_TOKEN"; +var en = { + githubTitle: "GitHub", + githubDescription: "GitHub pull requests, issues, and CI through the agent.", + tokenLabel: "GitHub token", + tokenHint: "Stored in the credentials file, not here. Applied immediately; leave blank to keep the current token.", + tokenSet: "A token is configured.", + tokenUnset: "No token is configured; GitHub tools are unavailable until one is.", + unsaved: "Unsaved", + readOnly: "This deployment stores settings read-only.", + save: "Save", + saving: "Saving…", + discard: "Discard", + saveFailed: "The deployment did not accept this value; it was left for you to correct." +}; +var zh = { + githubTitle: "GitHub", + githubDescription: "通过 agent 操作 GitHub 的 PR、issue 与 CI。", + tokenLabel: "GitHub Token", + tokenHint: "写入凭证文件而非设置文件;保存后立即生效。留空表示保持现有令牌。", + tokenSet: "已配置令牌。", + tokenUnset: "未配置令牌;配置之前 GitHub 工具不可用。", + unsaved: "未保存", + readOnly: "本部署的设置为只读。", + save: "保存", + saving: "保存中…", + discard: "放弃修改", + saveFailed: "本部署没有接受该值,已保留供你修改。" +}; +var GithubCardController = class { + form; + api; + store; + saving = false; + failed = false; + credential = { ref: "", configured: false, writable: true }; + /** + * @param form - the configuration form the Plugins page hands this entry, or + * `undefined` when the page supplied none (the card then reads the default + * reference and offers the credential control alone). + * @param api - the credentials Remote namespace. + */ + constructor(form, api) { + this.form = form; + this.api = api; + this.store = createSnapshotStore(this.projection()); + form?.subscribe(() => { + void this.readCredential(); + this.publish(); + }); + void this.readCredential(); + } + /** Whether the Host configuration document accepts writes. */ + writable() { + return this.form?.getSnapshot().writable ?? true; + } + /** The configuration form this controller reads (identity-compared by the caller). */ + formOf() { + return this.form; + } + /** Project the card's full state for its snapshot store. */ + projection() { + return { + writable: this.writable(), + configured: this.credential.configured, + credentialWritable: this.credential.writable, + saving: this.saving, + failed: this.failed, + ref: this.credential.ref + }; + } + /** The credential reference the configuration names, or the provider default. */ + refOf() { + const declared = this.form?.getSnapshot().value?.tokenRef; + return declared !== void 0 && declared.length > 0 ? declared : DEFAULT_TOKEN_REF; + } + /** + * Ask the credentials domain about the reference the section names. A + * response is published only while it still answers for the reference in + * force, so two reads settling out of order cannot clobber each other. + */ + async readCredential() { + const ref = this.refOf(); + if (ref !== this.credential.ref) { + this.credential = { ref, configured: false, writable: true }; + this.publish(); } - publish() { - this.store.set(this.projection()); + let response; + try { + response = await this.api.describe([ref]); + } catch { + return; } -} -/** - * Render the GitHub card. `summary` renders the one-liner the Plugins page - * places under the card's title (the description plus the token state badge); - * `page` renders the token control and the save/discard row on the plugin's - * own page. The page supplies the configuration form, so the card never - * depends on a served namespace to render. - */ -export function GithubCard(props) { - const { t } = props; - const state = props.useGithubCard((snapshot) => snapshot); - const [draft, setDraft] = useState(''); - if (props.view === 'summary') { - return createElement('span', { className: 'ghc-summary' }, createElement('span', null, t('githubDescription')), createElement('span', { className: state.configured ? 'ghc-badge' : 'ghc-badgeMuted' }, state.configured ? t('tokenSet') : t('tokenUnset'))); + if (!response.ok || ref !== this.refOf()) return; + const view = response.value[ref]; + const next = { ref, configured: view?.configured ?? false, writable: view?.writable ?? true }; + if (next.configured === this.credential.configured && next.writable === this.credential.writable) return; + this.credential = next; + this.publish(); + } + /** + * Re-read after the Host reports a change to the reference this card + * watches — a token can be written from elsewhere, and the section does not + * change when it is. + * @param ref - the reference the Host reports as changed. + */ + refreshCredential(ref) { + if (ref !== this.credential.ref) return; + void this.readCredential(); + } + /** + * Write the staged token, then re-read whether the Host now holds one. A + * blank value writes nothing, which keeps the stored key. + * @param value - the staged credential literal. + * @returns whether the Host reports a configured credential afterwards. + */ + async save(value) { + const text = (value ?? "").trim(); + if (text === "" || this.saving) return false; + this.saving = true; + this.failed = false; + this.publish(); + try { + await this.api.set(this.refOf(), text); + } catch { } - const dirty = draft.trim() !== ''; - const saveLabel = state.saving ? t('saving') : t('save'); - return createElement('div', { className: 'ghc-card ghc-cardPage' }, !state.writable - ? createElement('p', { className: 'ghc-readOnly', role: 'status' }, t('readOnly')) - : null, createElement('div', { className: 'ghc-field' }, createElement('div', { className: 'ghc-head' }, createElement('label', { className: 'ghc-label', htmlFor: 'plugin-config-github-token' }, t('tokenLabel')), createElement('span', { className: 'ghc-badges' }, createElement('span', { className: state.configured ? 'ghc-badge' : 'ghc-badgeMuted' }, state.configured ? t('tokenSet') : t('tokenUnset')))), createElement('input', { - id: 'plugin-config-github-token', - className: 'ghc-input', - type: 'password', - autoComplete: 'off', + await this.readCredential(); + const landed = this.credential.configured; + this.saving = false; + this.failed = !landed; + this.publish(); + return landed; + } + /** Build the face the card's slot registration injects. */ + inject() { + return { + hooks: { githubCard: this.store }, + submit: (value) => this.save(value) + }; + } + publish() { + this.store.set(this.projection()); + } +}; +function GithubCard(props) { + const { t } = props; + const state = props.useGithubCard((snapshot) => snapshot); + const [draft, setDraft] = (0, import_react.useState)(""); + if (props.view === "summary") { + return (0, import_react.createElement)( + "span", + { className: "ghc-summary" }, + (0, import_react.createElement)("span", null, t("githubDescription")), + (0, import_react.createElement)( + "span", + { className: state.configured ? "ghc-badge" : "ghc-badgeMuted" }, + state.configured ? t("tokenSet") : t("tokenUnset") + ) + ); + } + const dirty = draft.trim() !== ""; + const saveLabel = state.saving ? t("saving") : t("save"); + return (0, import_react.createElement)( + "div", + { className: "ghc-card ghc-cardPage" }, + !state.writable ? (0, import_react.createElement)("p", { className: "ghc-readOnly", role: "status" }, t("readOnly")) : null, + (0, import_react.createElement)( + "div", + { className: "ghc-field" }, + (0, import_react.createElement)( + "div", + { className: "ghc-head" }, + (0, import_react.createElement)("label", { className: "ghc-label", htmlFor: "plugin-config-github-token" }, t("tokenLabel")), + (0, import_react.createElement)( + "span", + { className: "ghc-badges" }, + (0, import_react.createElement)( + "span", + { className: state.configured ? "ghc-badge" : "ghc-badgeMuted" }, + state.configured ? t("tokenSet") : t("tokenUnset") + ) + ) + ), + (0, import_react.createElement)("input", { + id: "plugin-config-github-token", + className: "ghc-input", + type: "password", + autoComplete: "off", value: draft, disabled: !state.credentialWritable, - onChange: (event) => setDraft(event.target.value), - }), createElement('p', { className: 'ghc-hint' }, t('tokenHint'))), createElement('div', { className: 'ghc-actions' }, state.failed ? createElement('p', { className: 'ghc-failed', role: 'status' }, t('saveFailed')) : null, createElement(Button, { - variant: 'ghost', - size: 'sm', - disabled: !dirty || state.saving, - onClick: () => setDraft(''), - }, t('discard')), createElement(Button, { - variant: 'primary', - size: 'sm', - disabled: !state.writable || !dirty || state.saving, - icon: state.saving - ? createElement('span', { className: 'ghc-spin' }, createElement(IconLoadingOutlineRegular, { size: 16 })) - : undefined, - onClick: async () => { + onChange: (event) => setDraft(event.target.value) + }), + (0, import_react.createElement)("p", { className: "ghc-hint" }, t("tokenHint")) + ), + (0, import_react.createElement)( + "div", + { className: "ghc-actions" }, + state.failed ? (0, import_react.createElement)("p", { className: "ghc-failed", role: "status" }, t("saveFailed")) : null, + (0, import_react.createElement)( + import_dsh_client_ui_primitives.Button, + { + variant: "ghost", + size: "sm", + disabled: !dirty || state.saving, + onClick: () => setDraft("") + }, + t("discard") + ), + (0, import_react.createElement)( + import_dsh_client_ui_primitives.Button, + { + variant: "primary", + size: "sm", + disabled: !state.writable || !dirty || state.saving, + icon: state.saving ? (0, import_react.createElement)("span", { className: "ghc-spin" }, (0, import_react.createElement)(import_dsh_client_ui_primitives.IconLoadingOutlineRegular, { size: 16 })) : void 0, + onClick: async () => { const landed = await props.submit(draft); - if (landed) - setDraft(''); + if (landed) setDraft(""); + } }, - }, saveLabel))); + saveLabel + ) + ) + ); } -/** Dictionary namespace owned by this plugin. */ -export const NS = 'dsh-github'; -/** - * Required services (cordis fiber inject). `settingsScope` is gone: host - * 0.1.7-alpha.1 removed it, and the Plugins page now hands the configuration - * form to the `plugins.item` entry instead. `@deepseek-ai/dsh-client-ui-plugin-manager` - * owns that slot, so it must be composed for the card to mount at all. - */ -export const inject = ['slots', 'locale', 'connection', 'remote', 'remote.credentials']; -/** - * Mount the GitHub configuration card into the Plugins page's Official group. - * - * The form arrives per render, so the controller is rebuilt whenever the page - * supplies a different one — the page's own form owner stays authoritative and - * the card keeps no second copy of the accepted values. - * @param ctx - the browser plugin context. - */ -export function apply(ctx) { - const t = ctx.locale.bind(NS); - ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-github: card dictionaries'); - let github; - const controllerFor = (form) => { - if (github === undefined || github.formOf() !== form) { - github = new GithubCardController(form, ctx.remote.credentials); - } - return github; - }; - ctx.effect(() => ctx.remote.$on('credentials/reference-updated', (ref) => github?.refreshCredential(ref)), 'dsh-github: credential invalidations'); - ctx.slots.inject('plugins.item', () => ctx.slots.register({ - name: 'plugins.item', - id: NS, - order: 100, - label: () => t('githubTitle'), - locale: NS, - inject: () => controllerFor(github?.formOf()).inject(), - }, GithubCard)); +var NS = "dsh-github"; +var inject = ["slots", "locale", "connection", "remote", "remote.credentials"]; +function apply(ctx) { + const t = ctx.locale.bind(NS); + ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-github: card dictionaries"); + let github; + const controllerFor = (form) => { + if (github === void 0 || github.formOf() !== form) { + github = new GithubCardController(form, ctx.remote.credentials); + } + return github; + }; + ctx.effect( + () => ctx.remote.$on("credentials/reference-updated", (ref) => github?.refreshCredential(ref)), + "dsh-github: credential invalidations" + ); + ctx.slots.inject("plugins.item", () => ctx.slots.register( + { + name: "plugins.item", + id: NS, + order: 100, + label: () => t("githubTitle"), + locale: NS, + inject: () => controllerFor(github?.formOf()).inject() + }, + GithubCard + )); } -//# sourceMappingURL=client.js.map \ No newline at end of file +return module.exports; +} }); +//# sourceMappingURL=client.js.map diff --git a/lib/client.js.map b/lib/client.js.map index e8bb1fc..e7a3c0b 100644 --- a/lib/client.js.map +++ b/lib/client.js.map @@ -1 +1,7 @@ -{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{ + "version": 3, + "sources": ["../src/client.ts"], + "sourcesContent": ["/**\n * dsh-github browser half — one card in the Plugins page's Official group.\n *\n * The card reports and edits the GitHub token. The token literal never rides\n * the settings document: it is written through the credentials domain,\n * addressed by the reference the plugin's cordis.yml `Config.tokenRef` names\n * (default `GITHUB_TOKEN`), which is exactly where the host half resolves it —\n * per operation, no restart needed.\n *\n * Since host 0.1.7-alpha.1 the page owns configuration: the card renders the\n * `summary` one-liner and the `page` body from the `plugins.item` owner props\n * (`view`, and the `form` the Plugins page supplies), instead of binding a\n * `settingsScope` itself — that service, `settings.plugin.item`'s old shape,\n * and the `dsh-settings-file` package behind them were all removed. The card\n * therefore reads `tokenRef` from `form.state.value` and never writes the\n * settings document; the credential is written through the credentials\n * domain, which is a different seam and stayed.\n *\n * The shipped `lib/client.js` is the built bundle of this module. The browser\n * module loader executes that bundle, not this file.\n * @module @perrylink/dsh-github/client\n */\nimport { createElement, useState, type ChangeEvent, type ReactNode } from 'react'\nimport {\n Button, IconLoadingOutlineRegular,\n} from '@deepseek-ai/dsh-client-ui-primitives'\n\n/**\n * The configuration-form contract the card consumes, declared locally: a\n * client package must not import a Host package, and the browser half of the\n * seam lives in `@deepseek-ai/dsh-client-ui-settings`, which is an optional\n * peer. Mirrors that package's `ConfigForm`/`ConfigFormSnapshot` faces — the\n * ones the Plugins page hands a `plugins.item` entry as `form`.\n */\n\n/** Client-side sync state of one configuration entry. */\nexport interface ConfigFormSnapshot {\n /** `loading` until the first accepted section, `ready` while one stands, `unavailable` otherwise. */\n status: 'loading' | 'ready' | 'unavailable'\n /** Last accepted schema-resolved section; undefined before the first acceptance. */\n value: T | undefined\n /** Composition layer the Host resolved the value over, when the owning plugin declared one. */\n base: unknown\n /** Raw user layer as stored, when one exists. */\n user: unknown\n /** Namespace revision fencing the next write; undefined before the first Host view. */\n revision: number | undefined\n /** Whether the Host document accepts writes; memory mode never does. */\n writable: boolean\n /** `host` syncs with the Host document; `memory` keeps a remote browser process-local. */\n mode: 'host' | 'memory'\n}\n\n/** Reactive owner handle over one configuration entry's accepted values. */\nexport interface ConfigForm {\n /** @returns the current sync snapshot (stable reference until the next change). */\n getSnapshot(): ConfigFormSnapshot\n /**\n * Observe snapshot replacements.\n * @param listener - invoked after each snapshot change.\n * @returns the disposer removing this listener.\n */\n subscribe(listener: () => void): () => void\n}\n\n/**\n * Minimal writable snapshot store the card's slot hooks consume. Declared and\n * implemented locally: its previous home `dsh-client-store` is not on the\n * published `0.1.1-rc.2` line, and the card only needs the bare observable\n * contract plus whole-value replacement (no drafts, no persistence) — the\n * framework synthesizes the selector hook from `getSnapshot`/`subscribe`.\n */\nexport interface SnapshotStore {\n /** @returns the current snapshot reference. */\n getSnapshot(): T\n /**\n * Subscribe to snapshot replacements.\n * @param listener - invoked after each change.\n * @returns the disposer removing this listener.\n */\n subscribe(listener: () => void): () => void\n /**\n * Replace the state wholesale.\n * @param next - next state.\n */\n set(next: T): void\n}\n\n/**\n * Create the minimal card store.\n * @param init - initial state.\n * @returns the store.\n */\nexport function createSnapshotStore(init: T): SnapshotStore {\n let state = init\n const listeners = new Set<() => void>()\n return {\n getSnapshot: () => state,\n subscribe: (listener) => {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n },\n set: (next) => {\n state = next\n for (const listener of [...listeners]) listener()\n },\n }\n}\n\n/** Namespace of the GitHub capability. Spelled here rather than imported: a client package must not depend on a Host package. */\nexport const GITHUB_NS = 'dsh-github'\n/** Credential reference the provider resolves when the configuration names none. */\nexport const DEFAULT_TOKEN_REF = 'GITHUB_TOKEN'\n\n/** Locale copy the card renders. */\nexport interface GithubCardLocale {\n githubTitle: string\n githubDescription: string\n tokenLabel: string\n tokenHint: string\n tokenSet: string\n tokenUnset: string\n unsaved: string\n readOnly: string\n save: string\n saving: string\n discard: string\n saveFailed: string\n}\n\n/** English copy. */\nexport const en: GithubCardLocale = {\n githubTitle: 'GitHub',\n githubDescription: 'GitHub pull requests, issues, and CI through the agent.',\n tokenLabel: 'GitHub token',\n tokenHint: 'Stored in the credentials file, not here. Applied immediately; leave blank to keep the current token.',\n tokenSet: 'A token is configured.',\n tokenUnset: 'No token is configured; GitHub tools are unavailable until one is.',\n unsaved: 'Unsaved',\n readOnly: 'This deployment stores settings read-only.',\n save: 'Save',\n saving: 'Saving…',\n discard: 'Discard',\n saveFailed: 'The deployment did not accept this value; it was left for you to correct.',\n}\n\n/** Simplified Chinese copy. */\nexport const zh: GithubCardLocale = {\n githubTitle: 'GitHub',\n githubDescription: '通过 agent 操作 GitHub 的 PR、issue 与 CI。',\n tokenLabel: 'GitHub Token',\n tokenHint: '写入凭证文件而非设置文件;保存后立即生效。留空表示保持现有令牌。',\n tokenSet: '已配置令牌。',\n tokenUnset: '未配置令牌;配置之前 GitHub 工具不可用。',\n unsaved: '未保存',\n readOnly: '本部署的设置为只读。',\n save: '保存',\n saving: '保存中…',\n discard: '放弃修改',\n saveFailed: '本部署没有接受该值,已保留供你修改。',\n}\n\n/** The card's full state, as projected into its snapshot store. */\nexport interface GithubCardState {\n /** Whether the Host configuration document accepts writes. */\n writable: boolean\n /** Whether the referenced credential is configured. */\n configured: boolean\n /** Whether the credentials domain accepts writes. */\n credentialWritable: boolean\n /** Whether a save is crossing the wire. */\n saving: boolean\n /** Whether the last save did not land as staged. */\n failed: boolean\n /** The credential reference the card addresses. */\n ref: string\n}\n\n/** Source and writability facts for one credential reference (never the value). */\ninterface CredentialView {\n /** Whether resolving the reference would currently return a value. */\n configured: boolean\n /** Source layer currently supplying the value; absent while unconfigured. */\n source?: string\n /** Whether the active provider can write this reference. */\n writable: boolean\n}\n\n/**\n * The credentials-domain wire face the card uses: the host `credentials` Remote\n * namespace (`remote.credentials`). The token literal crosses the wire on\n * `set` only — no read path returns it.\n */\nexport interface CredentialsApi {\n /**\n * Ask the credentials domain about references; one view per requested name,\n * keyed by that name.\n */\n describe(refs: string[]): Promise<\n | { readonly ok: true; readonly value: Record }\n | { readonly ok: false; readonly error: { readonly code: string; readonly message: string } }\n >\n /**\n * Store one non-empty value under a reference.\n */\n set(ref: string, value: string): Promise\n}\n\n/** The configuration the card reads its credential reference from (the plugin's own `Config` subset). */\ninterface GithubCardConfig {\n tokenRef?: string\n}\n\n/**\n * Bridges the page-supplied configuration form and the credentials domain onto\n * the card. The token is the one control that does not live in the\n * configuration: its literal never rides a response, so the card learns only\n * whether one is configured and writes it through the credentials domain,\n * addressed by the reference `Config.tokenRef` names.\n */\nexport class GithubCardController {\n private readonly form: ConfigForm | undefined\n private readonly api: CredentialsApi\n private readonly store: SnapshotStore\n private saving = false\n private failed = false\n private credential: { ref: string; configured: boolean; writable: boolean } = { ref: '', configured: false, writable: true }\n\n /**\n * @param form - the configuration form the Plugins page hands this entry, or\n * `undefined` when the page supplied none (the card then reads the default\n * reference and offers the credential control alone).\n * @param api - the credentials Remote namespace.\n */\n constructor(form: ConfigForm | undefined, api: CredentialsApi) {\n this.form = form\n this.api = api\n this.store = createSnapshotStore(this.projection())\n form?.subscribe(() => {\n void this.readCredential()\n this.publish()\n })\n void this.readCredential()\n }\n\n /** Whether the Host configuration document accepts writes. */\n writable(): boolean {\n return this.form?.getSnapshot().writable ?? true\n }\n\n /** The configuration form this controller reads (identity-compared by the caller). */\n formOf(): ConfigForm | undefined {\n return this.form\n }\n\n /** Project the card's full state for its snapshot store. */\n projection(): GithubCardState {\n return {\n writable: this.writable(),\n configured: this.credential.configured,\n credentialWritable: this.credential.writable,\n saving: this.saving,\n failed: this.failed,\n ref: this.credential.ref,\n }\n }\n\n /** The credential reference the configuration names, or the provider default. */\n refOf(): string {\n const declared = this.form?.getSnapshot().value?.tokenRef\n return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF\n }\n\n /**\n * Ask the credentials domain about the reference the section names. A\n * response is published only while it still answers for the reference in\n * force, so two reads settling out of order cannot clobber each other.\n */\n async readCredential(): Promise {\n const ref = this.refOf()\n if (ref !== this.credential.ref) {\n this.credential = { ref, configured: false, writable: true }\n this.publish()\n }\n let response\n try {\n response = await this.api.describe([ref])\n } catch {\n return\n }\n if (!response.ok || ref !== this.refOf()) return\n const view = response.value[ref]\n const next = { ref, configured: view?.configured ?? false, writable: view?.writable ?? true }\n if (next.configured === this.credential.configured && next.writable === this.credential.writable) return\n this.credential = next\n this.publish()\n }\n\n /**\n * Re-read after the Host reports a change to the reference this card\n * watches — a token can be written from elsewhere, and the section does not\n * change when it is.\n * @param ref - the reference the Host reports as changed.\n */\n refreshCredential(ref: string): void {\n if (ref !== this.credential.ref) return\n void this.readCredential()\n }\n\n /**\n * Write the staged token, then re-read whether the Host now holds one. A\n * blank value writes nothing, which keeps the stored key.\n * @param value - the staged credential literal.\n * @returns whether the Host reports a configured credential afterwards.\n */\n async save(value: string): Promise {\n const text = (value ?? '').trim()\n if (text === '' || this.saving) return false\n this.saving = true\n this.failed = false\n this.publish()\n try {\n await this.api.set(this.refOf(), text)\n } catch {\n // handled below by re-reading the configured state\n }\n await this.readCredential()\n const landed = this.credential.configured\n this.saving = false\n this.failed = !landed\n this.publish()\n return landed\n }\n\n /** Build the face the card's slot registration injects. */\n inject(): { hooks: { githubCard: SnapshotStore }; submit: (value: string) => Promise } {\n return {\n hooks: { githubCard: this.store },\n submit: (value) => this.save(value),\n }\n }\n\n private publish(): void {\n this.store.set(this.projection())\n }\n}\n\n/** Props the slot system injects into the card component. */\nexport interface GithubCardProps {\n t: (key: keyof GithubCardLocale) => string\n useGithubCard: (selector: (snapshot: GithubCardState) => GithubCardState) => GithubCardState\n submit: (value: string) => Promise\n /** The view the Plugins page asks for: the one-liner under the title, or the page body. */\n view: 'summary' | 'page'\n}\n\n/**\n * Render the GitHub card. `summary` renders the one-liner the Plugins page\n * places under the card's title (the description plus the token state badge);\n * `page` renders the token control and the save/discard row on the plugin's\n * own page. The page supplies the configuration form, so the card never\n * depends on a served namespace to render.\n */\nexport function GithubCard(props: GithubCardProps): ReactNode {\n const { t } = props\n const state = props.useGithubCard((snapshot) => snapshot)\n const [draft, setDraft] = useState('')\n if (props.view === 'summary') {\n return createElement(\n 'span',\n { className: 'ghc-summary' },\n createElement('span', null, t('githubDescription')),\n createElement(\n 'span',\n { className: state.configured ? 'ghc-badge' : 'ghc-badgeMuted' },\n state.configured ? t('tokenSet') : t('tokenUnset'),\n ),\n )\n }\n const dirty = draft.trim() !== ''\n const saveLabel = state.saving ? t('saving') : t('save')\n return createElement(\n 'div',\n { className: 'ghc-card ghc-cardPage' },\n !state.writable\n ? createElement('p', { className: 'ghc-readOnly', role: 'status' }, t('readOnly'))\n : null,\n createElement(\n 'div',\n { className: 'ghc-field' },\n createElement(\n 'div',\n { className: 'ghc-head' },\n createElement('label', { className: 'ghc-label', htmlFor: 'plugin-config-github-token' }, t('tokenLabel')),\n createElement(\n 'span',\n { className: 'ghc-badges' },\n createElement(\n 'span',\n { className: state.configured ? 'ghc-badge' : 'ghc-badgeMuted' },\n state.configured ? t('tokenSet') : t('tokenUnset'),\n ),\n ),\n ),\n createElement('input', {\n id: 'plugin-config-github-token',\n className: 'ghc-input',\n type: 'password',\n autoComplete: 'off',\n value: draft,\n disabled: !state.credentialWritable,\n onChange: (event: ChangeEvent) => setDraft(event.target.value),\n }),\n createElement('p', { className: 'ghc-hint' }, t('tokenHint')),\n ),\n createElement(\n 'div',\n { className: 'ghc-actions' },\n state.failed ? createElement('p', { className: 'ghc-failed', role: 'status' }, t('saveFailed')) : null,\n createElement(\n Button,\n {\n variant: 'ghost',\n size: 'sm',\n disabled: !dirty || state.saving,\n onClick: () => setDraft(''),\n },\n t('discard'),\n ),\n createElement(\n Button,\n {\n variant: 'primary',\n size: 'sm',\n disabled: !state.writable || !dirty || state.saving,\n icon: state.saving\n ? createElement('span', { className: 'ghc-spin' }, createElement(IconLoadingOutlineRegular, { size: 16 }))\n : undefined,\n onClick: async () => {\n const landed = await props.submit(draft)\n if (landed) setDraft('')\n },\n },\n saveLabel,\n ),\n ),\n )\n}\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'dsh-github'\n/**\n * Required services (cordis fiber inject). `settingsScope` is gone: host\n * 0.1.7-alpha.1 removed it, and the Plugins page now hands the configuration\n * form to the `plugins.item` entry instead. `@deepseek-ai/dsh-client-ui-plugin-manager`\n * owns that slot, so it must be composed for the card to mount at all.\n */\nexport const inject = ['slots', 'locale', 'connection', 'remote', 'remote.credentials']\n\nexport interface ClientContextLike {\n get(service: string): unknown\n effect(disposer: () => void, label: string): void\n locale: {\n bind(namespace: string): (key: keyof GithubCardLocale) => string\n register(namespace: string, dictionaries: Record): void\n }\n remote: {\n $on(event: string, listener: (ref: string) => void): unknown\n credentials: CredentialsApi\n }\n slots: {\n inject(slot: string, factory: () => unknown): unknown\n register(options: Record, component: unknown): unknown\n }\n}\n\n/** The `form` member the Plugins page hands a `plugins.item` entry. */\ninterface PluginConfigViewProps {\n readonly view: 'summary' | 'page'\n readonly form?: ConfigForm | undefined\n}\n\n/**\n * Mount the GitHub configuration card into the Plugins page's Official group.\n *\n * The form arrives per render, so the controller is rebuilt whenever the page\n * supplies a different one — the page's own form owner stays authoritative and\n * the card keeps no second copy of the accepted values.\n * @param ctx - the browser plugin context.\n */\nexport function apply(ctx: ClientContextLike): void {\n const t = ctx.locale.bind(NS)\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-github: card dictionaries')\n let github: GithubCardController | undefined\n const controllerFor = (form: PluginConfigViewProps['form']): GithubCardController => {\n if (github === undefined || github.formOf() !== form) {\n github = new GithubCardController(form, ctx.remote.credentials)\n }\n return github\n }\n ctx.effect(\n () => ctx.remote.$on('credentials/reference-updated', (ref) => github?.refreshCredential(ref)),\n 'dsh-github: credential invalidations',\n )\n ctx.slots.inject('plugins.item', () => ctx.slots.register(\n {\n name: 'plugins.item',\n id: NS,\n order: 100,\n label: () => t('githubTitle'),\n locale: NS,\n inject: () => controllerFor(github?.formOf()).inject(),\n },\n GithubCard,\n ))\n}\n"], + "mappings": "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", + "names": [] +} diff --git a/package.json b/package.json index 5c50935..f733258 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "LICENSE" ], "scripts": { - "build": "tsc --noEmitOnError && node scripts/fix-dts.mjs", + "build": "tsc --noEmitOnError && node scripts/build-client.mjs && node scripts/fix-dts.mjs", + "build:client": "node scripts/build-client.mjs", "prepare": "node scripts/prepare.mjs", "prepublishOnly": "npm run build && npm test", "test": "vitest run", @@ -170,6 +171,7 @@ "@types/node": "^22.0.0", "@types/react": "18.3.31", "@vitest/coverage-v8": "3.2.0", + "esbuild": "0.28.2", "oxlint": "0.18.1", "react": "18.3.1", "typescript": "^5.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 250ea3e..2fdecd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -577,6 +577,9 @@ importers: '@vitest/coverage-v8': specifier: 3.2.0 version: 3.2.0(vitest@3.2.7(@types/debug@4.1.13)(@types/node@22.20.1)(yaml@2.9.1)) + esbuild: + specifier: 0.28.2 + version: 0.28.2 oxlint: specifier: 0.18.1 version: 0.18.1 diff --git a/scripts/build-client.mjs b/scripts/build-client.mjs new file mode 100644 index 0000000..643c95f --- /dev/null +++ b/scripts/build-client.mjs @@ -0,0 +1,37 @@ +/** + * Emit `lib/client.js` — the browser half in the shape the DSH client module + * loader installs — from `src/client.ts`. + * + * `tsc` also writes `lib/client.js`, as ordinary ESM, for the Node-side + * project. This step replaces that file (and its sourcemap) with the + * registration bundle; `lib/client.d.ts` stays tsc's declaration emit and the + * published `./client` types stay the hand-authored `lib/types/client.d.ts`. + * The artifact is built and self-checked in memory, so a bundle the loader + * cannot install never reaches disk. + * + * Usage: node scripts/build-client.mjs [--check] + * --check build and self-check without writing (used by the test suite) + */ +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { assertClientBundle, bundleClient } from './client-bundle.mjs' + +const root = dirname(dirname(fileURLToPath(import.meta.url))) +const checkOnly = process.argv.includes('--check') +const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) + +const { code, map, externals } = await bundleClient({ root, pkg }) +const exports = assertClientBundle(code, pkg.name) + +if (!checkOnly) { + mkdirSync(join(root, 'lib'), { recursive: true }) + writeFileSync(join(root, 'lib', 'client.js'), code) + writeFileSync(join(root, 'lib', 'client.js.map'), map) +} + +const exported = Object.keys(exports).sort() +console.log( + `client bundle: ${pkg.name} ${checkOnly ? 'checked' : '→ lib/client.js'} ` + + `(${Buffer.byteLength(code)} bytes, ${externals.length} module-table externals, ${exported.length} exports: ${exported.join(', ')})`, +) diff --git a/scripts/client-bundle.mjs b/scripts/client-bundle.mjs new file mode 100644 index 0000000..b257dc4 --- /dev/null +++ b/scripts/client-bundle.mjs @@ -0,0 +1,268 @@ +/** + * The browser-half artifact contract for `@perrylink/dsh-github`. + * + * The DSH web shell does not import a client package through ESM: it installs + * the package's `exports["./client"]` target as a **classic script** and + * expects that file to hand its module factory to + * `window.__ModuleLoader__.load({ id, factory })`. A file that still carries + * top-level `import`/`export` statements is a `SyntaxError: Cannot use import + * statement outside a module` in that position, and because the shell serves + * several packages as one concatenated combo script per batch, the parse error + * aborts the whole batch: every client entry in it fails to activate and the + * UI reports "Failed to load plugins" for the entire profile. + * + * This module owns that artifact shape — the module-table externals, the + * registration wrapper, and the checks that prove a produced file has it. + * `scripts/build-client.mjs` writes the artifact; `scripts/prepare.mjs`, + * `scripts/verify-artifacts.mjs`, and `test/client-bundle.test.ts` verify one. + * @module scripts/client-bundle + */ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' + +/** + * Specifiers the DSH web shell seeds into the frozen browser module table + * (`PLATFORM_MODULES` in `@deepseek-ai/dsh-client-web/src/platform.ts`). + * + * A client bundle must leave these as bare `require(...)` calls: the loader + * injects the table entry, so a bundled copy would fork a shared runtime + * (React's hook dispatcher, the slots registry, the primitives) away from the + * single instance the rest of the shell holds. The table is the shell's, not + * this package's — keep it a literal so the artifact never depends on a + * DeepSeek Harness checkout. + */ +export const PLATFORM_MODULES = [ + 'react', + 'react/jsx-runtime', + 'react-dom', + 'react-dom/client', + '@deepseek-ai/cordis', + '@deepseek-ai/dsh-client-store', + '@deepseek-ai/dsh-client-ui-slots', + '@deepseek-ai/dsh-client-ui-primitives', + '@deepseek-ai/dsh-client-ui-dockkit', +] + +/** + * Package families whose modules carry browser runtime identity, so inlining + * one is never right: either it duplicates a singleton the shell already + * holds, or it is a client plugin whose module only exists as a table row. + * Anything else a client source imports (wire/type layers, plain libraries) + * is inlined by the bundler. + */ +const SHARED_RUNTIME_MODULE = /^@deepseek-ai\/(?:cordis|dsh-client-[a-z0-9-]+)(?:\/|$)/ + +/** + * Bare specifiers this package must leave to the loader's module table: the + * platform baseline plus whatever `dsh.client.external` adds. + * @param pkg - the parsed `package.json`. + * @returns the external specifiers, deduplicated. + * @throws {Error} when `dsh.client.external` is present but not a string array. + */ +export function clientExternals(pkg) { + const declared = pkg?.dsh?.client?.external + if (declared !== undefined && !Array.isArray(declared)) { + throw new Error(`${pkg?.name ?? 'package'}: dsh.client.external must be an array of specifiers`) + } + for (const specifier of declared ?? []) { + if (typeof specifier !== 'string' || specifier.length === 0) { + throw new Error(`${pkg?.name ?? 'package'}: dsh.client.external entries must be non-empty strings`) + } + } + return [...new Set([...PLATFORM_MODULES, ...(declared ?? [])])] +} + +/** + * The wrapper opening the loader's registration call. + * + * `"use strict"` is repeated on purpose: it has to be the factory body's first + * statement to act as a directive, and the bundler's own copy lands after the + * `module` shim below, where it is only a string literal. Strict mode must stay + * inside the factory — the shell concatenates several packages into one combo + * script, so a top-level directive here would leak into every neighbour. + * @param id - package name, the key the loader's module table answers `require(id)` with. + * @returns the banner text the bundler prepends. + */ +export function registrationBanner(id) { + return [ + `window.__ModuleLoader__.load({ id: ${JSON.stringify(id)}, factory: (require) => {`, + '"use strict";', + 'var module = { exports: {} };', + 'var exports = module.exports;', + ].join('\n') +} + +/** The wrapper closing the registration call and handing the factory's exports back. */ +export const REGISTRATION_FOOTER = 'return module.exports;\n} });' + +/** + * Resolve every bare import through the module table, and refuse to inline a + * package that carries shared browser runtime identity. + * + * This is the build-time mirror of the loader's runtime resolution order: a + * `require()` the table cannot answer is a guaranteed throw inside the + * factory, and an inlined copy of a shared module is a silent fork. Both are + * caught here instead of in the browser. + * @param externals - specifiers to leave for the loader. + * @param id - package name, used in diagnostics. + * @returns the bundler plugin. + */ +function moduleTablePlugin(externals, id) { + return { + name: 'dsh-client-module-table', + setup(build) { + // Bare specifiers only: relative and absolute paths are the package's own + // modules and stay internal. + build.onResolve({ filter: /^[^./]/ }, (args) => { + if (externals.has(args.path)) return { path: args.path, external: true } + if (SHARED_RUNTIME_MODULE.test(args.path)) { + throw new Error( + `client bundle: "${args.path}" carries shared browser runtime identity and cannot be inlined — ` + + `declare it in ${id}'s dsh.client.external so the loader answers require("${args.path}") from its module table`, + ) + } + return null + }) + }, + } +} + +/** + * Bundle the browser half into the registration artifact. + * @param options - build inputs. + * @param options.root - package root. + * @param options.pkg - the parsed `package.json`. + * @returns the script, its sourcemap, and the externals left to the loader. + */ +export async function bundleClient({ root, pkg }) { + // Loaded lazily so `prepare.mjs` can import this module on a git install that + // has no devDependencies at all. + const { build } = await import('esbuild') + const externals = new Set(clientExternals(pkg)) + const result = await build({ + absWorkingDir: root, + entryPoints: ['src/client.ts'], + outfile: 'lib/client.js', + bundle: true, + write: false, + format: 'cjs', + platform: 'browser', + target: ['es2022'], + charset: 'utf8', + legalComments: 'none', + sourcemap: true, + sourcesContent: true, + logLevel: 'silent', + // The shell fetches this file outside Vite's graph, so nothing rewrites a + // node-idiom dependency's environment probe for it. + define: { 'process.env.NODE_ENV': '"production"' }, + plugins: [moduleTablePlugin(externals, pkg.name)], + banner: { js: registrationBanner(pkg.name) }, + footer: { js: REGISTRATION_FOOTER }, + }) + const files = result.outputFiles ?? [] + const script = files.find(file => file.path.endsWith('.js')) + const map = files.find(file => file.path.endsWith('.map')) + if (script === undefined || map === undefined) { + throw new Error('client bundle: the bundler produced no script/sourcemap pair') + } + return { code: script.text, map: map.text, externals: [...externals] } +} + +/** The React surface the card touches, enough for a shape check to execute the factory. */ +const REACT_STUB = { + createElement: () => null, + Fragment: null, + useState: initial => [typeof initial === 'function' ? initial() : initial, () => {}], + useEffect: () => {}, + useRef: value => ({ current: value }), + useSyncExternalStore: (_subscribe, snapshot) => snapshot(), +} + +/** + * Stand-in `require` for a shape check: real enough for React, permissive for + * everything else. It proves the artifact's shape, never its behaviour — the + * browser is what proves behaviour. + * @param specifier - the requested module-table row. + * @returns a stub module namespace. + */ +export function stubRequire(specifier) { + if (specifier === 'react') return REACT_STUB + if (specifier === 'react/jsx-runtime') return { jsx: () => null, jsxs: () => null, Fragment: null } + return new Proxy({}, { get: (_target, key) => (key === '__esModule' ? false : () => null) }) +} + +/** + * Execute a produced bundle the way the shell does and return its exports. + * + * The bundle is compiled with `new Function`, which — like the + * `document.createElement('script')` the shell installs — always parses its + * body as a classic script. A leftover top-level `import`/`export` therefore + * fails here with the very same `SyntaxError` the browser reports, which is + * what makes this the regression check rather than a text scan. + * @param code - the bundle text. + * @param id - the package name the registration must carry. + * @param requireImpl - module-table stand-in. + * @returns the factory's exports object. + * @throws {Error} when the text is not a classic-script registration bundle. + */ +export function loadClientBundle(code, id, requireImpl = stubRequire) { + let factory + try { + factory = new Function('window', 'require', 'module', 'exports', code) + } catch (error) { + throw new Error( + `client bundle: ${id} is not a classic script the DSH loader can install — ${error.message}. ` + + 'The shell installs this file with document.createElement(\'script\') and concatenates one combo script per batch, ' + + 'so a top-level import/export takes every package in that batch down with it.', + { cause: error }, + ) + } + let registration + const window = { __ModuleLoader__: { load: (value) => { registration = value } } } + const module = { exports: {} } + factory(window, requireImpl, module, module.exports) + if (registration === undefined) { + throw new Error(`client bundle: ${id} never called window.__ModuleLoader__.load`) + } + if (registration.id !== id) { + throw new Error(`client bundle: registered as ${JSON.stringify(registration.id)}, expected ${JSON.stringify(id)}`) + } + if (typeof registration.factory !== 'function') { + throw new Error(`client bundle: ${id} registered no factory`) + } + const exports = registration.factory(requireImpl) + if (typeof exports !== 'object' || exports === null) { + throw new Error(`client bundle: ${id} factory returned ${typeof exports}, expected the module exports object`) + } + return exports +} + +/** + * Prove one produced bundle satisfies the loader contract, including the + * cordis plugin face the loader materializes the entry into. + * @param code - the bundle text. + * @param id - the package name the registration must carry. + * @param requireImpl - module-table stand-in. + * @returns the factory's exports object. + * @throws {Error} when the artifact does not satisfy the contract. + */ +export function assertClientBundle(code, id, requireImpl = stubRequire) { + const exports = loadClientBundle(code, id, requireImpl) + if (typeof exports.apply !== 'function') { + throw new Error(`client bundle: ${id} exports no apply() — the loader materializes a cordis plugin from this factory`) + } + if (!Array.isArray(exports.inject)) { + throw new Error(`client bundle: ${id} exports no inject array — the loader waits on those services before apply()`) + } + return exports +} + +/** + * Read the shipped browser half. + * @param root - package root. + * @returns the bundle text. + */ +export function readClientBundle(root) { + return readFileSync(join(root, 'lib', 'client.js'), 'utf8') +} diff --git a/scripts/prepare.mjs b/scripts/prepare.mjs index 7494bb2..5f83f00 100644 --- a/scripts/prepare.mjs +++ b/scripts/prepare.mjs @@ -3,41 +3,72 @@ // pnpm runs this after `dsh plugin add "github:owner/repo#"` once the // user allowlists the build (allowBuilds in the profile pnpm-workspace.yaml). // A git install has no devDependencies, so the script must work without -// typescript being resolvable: it falls back to the committed lib/ artifacts, -// and fails loud when neither a compiler nor build artifacts exist. -import { existsSync } from 'node:fs' +// typescript or esbuild being resolvable: it then falls back to the committed +// lib/ artifacts, and fails loud when neither a toolchain nor usable artifacts +// exist. +// +// Both tools are required before tsc runs, and that is deliberate: tsc emits +// the browser half as plain ESM, so compiling without the bundle step would +// overwrite a good committed `lib/client.js` with a file the DSH client module +// loader cannot install — a parse error that takes the whole plugin batch down +// in the browser, which is worse than a failed install. +import { existsSync, readFileSync } from 'node:fs' import { spawnSync } from 'node:child_process' +import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' +import { assertClientBundle } from './client-bundle.mjs' const root = dirname(dirname(fileURLToPath(import.meta.url))) const lib = join(root, 'lib') const tsc = join(root, 'node_modules', 'typescript', 'bin', 'tsc') +const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) -if (existsSync(tsc)) { - const result = spawnSync(process.execPath, [tsc, '-p', 'tsconfig.json', '--noEmitOnError'], { - cwd: root, - stdio: 'inherit', - }) +/** Whether a package resolves from this install. */ +const resolvable = (specifier) => { + try { + createRequire(join(root, 'package.json')).resolve(specifier) + return true + } catch { + return false + } +} + +/** Run one build step, inheriting stdio, and exit on its failure. */ +const run = (...args) => { + const result = spawnSync(process.execPath, args, { cwd: root, stdio: 'inherit' }) if (result.status !== 0) { process.exit(typeof result.status === 'number' ? result.status : 1) } +} + +/** Whether the on-disk browser half is the registration bundle the loader needs. */ +const hasClientBundle = () => { + const artifact = join(lib, 'client.js') + if (!existsSync(artifact)) return false + try { + assertClientBundle(readFileSync(artifact, 'utf8'), pkg.name) + return true + } catch (error) { + console.error(`dsh-github prepare: committed lib/client.js is not a browser bundle — ${error.message}`) + return false + } +} + +if (existsSync(tsc) && resolvable('esbuild')) { + run(tsc, '-p', 'tsconfig.json', '--noEmitOnError') // TS 5.9 does not rewrite `.ts` specifiers in declaration emit; fix them so // NodeNext declaration consumers can resolve lib. - const fix = spawnSync(process.execPath, [join(root, 'scripts', 'fix-dts.mjs')], { - cwd: root, - stdio: 'inherit', - }) - if (fix.status !== 0) { - process.exit(typeof fix.status === 'number' ? fix.status : 1) - } + run(join(root, 'scripts', 'fix-dts.mjs')) + // Replace the ESM file tsc just emitted with the loader bundle. + run(join(root, 'scripts', 'build-client.mjs')) process.exit(0) } -if (existsSync(join(lib, 'index.js'))) { - // Committed build artifacts: usable without a compiler. +if (existsSync(join(lib, 'index.js')) && hasClientBundle()) { + // Committed build artifacts: usable without a toolchain. process.exit(0) } -console.error('dsh-github prepare: no TypeScript compiler and no committed lib/ artifacts — build failed') +console.error('dsh-github prepare: no TypeScript compiler + esbuild and no usable committed lib/ artifacts — build failed') process.exit(1) diff --git a/scripts/verify-artifacts.mjs b/scripts/verify-artifacts.mjs index 93face6..68b1b6b 100644 --- a/scripts/verify-artifacts.mjs +++ b/scripts/verify-artifacts.mjs @@ -4,16 +4,25 @@ // apply is a function, no default export). Guards against TypeScript-only // syntax leaking into shipped output and against a tarball missing the // bundle patch or the Action contract. +// +// The browser half is checked separately and in its own shape: the DSH web +// shell installs `lib/client.js` as a classic script, so plain ESM there is +// the failure this file exists to catch, not something `node --check` can +// see (this package is `"type": "module"`, which makes that parse it as ESM). import { execFileSync } from 'node:child_process' -import { existsSync } from 'node:fs' +import { existsSync, readFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' +import { assertClientBundle } from './client-bundle.mjs' const root = path.dirname(path.dirname(fileURLToPath(import.meta.url))) +const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')) const required = [ 'lib/index.js', 'lib/index.d.ts', + 'lib/client.js', + 'lib/types/client.d.ts', 'action.yml', 'cordis.patch.yml', ] @@ -29,4 +38,9 @@ if (index.name !== 'dsh-github' || typeof index.apply !== 'function') { throw new Error('lib/index.js exports an unexpected plugin face') } -console.log('artifacts OK: syntax + ESM import + bundle patch and action contract present') +const clientRel = pkg.exports?.['./client']?.default +if (typeof clientRel !== 'string') throw new Error('package.json exports["./client"].default must name the browser bundle') +const client = readFileSync(path.join(root, clientRel), 'utf8') +assertClientBundle(client, pkg.name) + +console.log('artifacts OK: syntax + ESM import + browser bundle registration + bundle patch and action contract present') diff --git a/test/client-bundle.test.ts b/test/client-bundle.test.ts new file mode 100644 index 0000000..104b95d --- /dev/null +++ b/test/client-bundle.test.ts @@ -0,0 +1,71 @@ +/** + * Browser-half artifact contract. + * + * The DSH web shell installs `exports["./client"].default` as a **classic + * script** and expects it to hand its factory to + * `window.__ModuleLoader__.load({ id, factory })`. 0.7.12 shipped plain `tsc` + * output there instead — top-level `import`/`export` — so the browser threw + * `SyntaxError: Cannot use import statement outside a module`, the combo batch + * that file was concatenated into failed to parse as a whole, and every client + * entry in it (59 of them, in the reported profile) stayed unactivated behind + * a "Failed to load plugins" page. + * + * These tests pin the artifact so that regression cannot ship again: the + * shipped file must execute as a classic script and register, and a fresh + * build of `src/client.ts` must be what is committed. + * @module dsh-github/test/client-bundle + */ + +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { + assertClientBundle, bundleClient, clientExternals, loadClientBundle, +} from '../scripts/client-bundle.mjs' + +const root = fileURLToPath(new URL('..', import.meta.url)) +const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) +const artifact = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8') +const artifactMap = readFileSync(new URL('../lib/client.js.map', import.meta.url), 'utf8') + +/** The shape `tsc` alone emits for `src/client.ts` — the 0.7.12 failure, verbatim in structure. */ +const TSC_OUTPUT = [ + "import { createElement, useState } from 'react';", + "import { Button, IconLoadingOutline16, } from '@deepseek-ai/dsh-client-ui-primitives';", + "export const NS = 'dsh-github';", + 'export const inject = [\'slots\', \'locale\'];', + 'export function apply(ctx) { return ctx; }', + '', +].join('\n') + +describe('lib/client.js browser bundle', () => { + it('executes as a classic script and registers the package id', () => { + const exports = assertClientBundle(artifact, pkg.name) + expect(typeof exports.apply).toBe('function') + expect(Array.isArray(exports.inject)).toBe(true) + expect(exports.inject).toContain('slots') + }) + + it('keeps every bare import on the loader module table', () => { + const requested = [...new Set( + [...artifact.matchAll(/\brequire\((['"])([^'"]+)\1\)/g)].map(match => match[2] as string), + )] + // A bundle with no `require` at all would pass vacuously and prove nothing. + expect(requested.length).toBeGreaterThan(0) + const allowed = new Set(clientExternals(pkg)) + for (const specifier of requested) { + expect(allowed.has(specifier), `"${specifier}" is not a module-table row — the loader cannot answer it`).toBe(true) + } + }) + + it('ships exactly what a fresh build of src/client.ts produces', async () => { + const fresh = await bundleClient({ root, pkg }) + expect(artifact).toBe(fresh.code) + expect(artifactMap).toBe(fresh.map) + }) + + it('rejects the plain tsc output that broke the plugin batch', () => { + expect(() => loadClientBundle(TSC_OUTPUT, pkg.name)).toThrow(/not a classic script/) + expect(() => assertClientBundle(TSC_OUTPUT, pkg.name)).toThrow(/not a classic script/) + }) +})