mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor(gui): one plugin-package shape — dshClient manifests, clientBundle preset, purity gate over all nine
Every client plugin package carries dshClient ({platform, inject,
immediately?}) and emits lib/client.js through the shared clientBundle
preset; exports["./client"] points at the bundle. The infrastructure
tier (connection, runtime, ui-theme, i18n, hmr) declares immediately: true
in its manifest — absent means lazy. The bundle purity gate covers all
nine packages: platform modules stay external, INLINE_SAFE wire layers
inline, any other cross-plugin value import is a build error. Migrations
that rule forced: scopeOf became a SessionsService method and
transportError moved into dsh-host-apiproxy's wire layer; the store
engine stays in runtime under a documented temporary exemption
(TODO(webload/store-rehome)).
This commit is contained in:
@@ -14,7 +14,10 @@ export type {
|
||||
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
|
||||
ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt,
|
||||
} from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
export { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
// transportError moved down to the apiproxy api layer (it belongs beside
|
||||
// RpcResult, its subject); re-exported here so connection consumers keep one
|
||||
// contract entry point.
|
||||
export { RpcId, transportError } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
export { AbstractApiClient } from '@deepseek-ai/dsh-host-apiproxy/client'
|
||||
export type { IApiClient } from '@deepseek-ai/dsh-host-apiproxy/client'
|
||||
export type { SessionId, SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
@@ -31,16 +34,3 @@ import type { RpcResponse, RpcResult } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
export function resultOf<T>(response: RpcResponse<T>): RpcResult<T> {
|
||||
return response.result
|
||||
}
|
||||
|
||||
/**
|
||||
* Fold a transport exception into the RpcResult error branch (unified error
|
||||
* surface; 'internal' as the catch-all code).
|
||||
* @param error - the thrown value from the carrier.
|
||||
* @returns the error branch of an RpcResult.
|
||||
*/
|
||||
export function transportError<T>(error: unknown): RpcResult<T> {
|
||||
return {
|
||||
ok: false,
|
||||
error: { code: 'internal', message: error instanceof Error ? error.message : String(error), details: {} },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,6 @@
|
||||
"platform": "web",
|
||||
"immediately": true
|
||||
},
|
||||
"scripts": {
|
||||
"bundle": "tsdown",
|
||||
"watch": "tsdown --watch"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^"
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
// List data never enters zustand; React connects via subscribe/getListSnapshot.
|
||||
|
||||
import type { IApiClient, HostFrame, MuxFrame, RpcError, RpcRequest, RpcResult, SessionId, SessionSummary } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { transportError } from '@deepseek-ai/dsh-client-connection/client'
|
||||
// Value import from the inline-safe wire layer (not the connection plugin):
|
||||
// plugin-to-plugin value imports are a bundle purity error.
|
||||
import { transportError } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import type { SessionListEntry, TitledSessionSummary } from './lineage.ts'
|
||||
import { flattenLineage } from './lineage.ts'
|
||||
import { Notifier } from './notifier.ts'
|
||||
|
||||
@@ -168,6 +168,18 @@ export class SessionsService {
|
||||
return this.resolve(id)?.ctx
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the session scope tag off a context. Service-method seam: fetch
|
||||
* bundles must reach scope resolution through ctx.sessions — a cross-bundle
|
||||
* value import of the standalone helper would inline a second module
|
||||
* instance whose private tag Symbol never matches.
|
||||
* @param ctx - any client context.
|
||||
* @returns the session id, or undefined on root contexts.
|
||||
*/
|
||||
scopeOf(ctx: Context): SessionId | undefined {
|
||||
return scopeOf(ctx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the stable session binding (scope-addressed assembly feed). Pure
|
||||
* resolution — no staging, no window side effects.
|
||||
|
||||
@@ -9,7 +9,9 @@ import type {
|
||||
HistoryEntry, IApiClient, MuxFrame, RpcError, RpcId, RpcResult,
|
||||
SessionId, ToolEventView,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { transportError } from '@deepseek-ai/dsh-client-connection/client'
|
||||
// Value import from the inline-safe wire layer (not the connection plugin):
|
||||
// plugin-to-plugin value imports are a bundle purity error.
|
||||
import { transportError } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import type { ObservableSnapshot } from '../contract/store.ts'
|
||||
import type {
|
||||
ConversationNode, ConversationSnapshot, OpenState, PromptError, RunningToolCall,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Shared tsdown preset for UI plugin client bundles. Emits a closure-factory
|
||||
* artifact: the bundle calls window.DSHClientProxy.loadPlugin({id, factory})
|
||||
* artifact: the bundle calls window.__ModuleLoader__.load({id, factory})
|
||||
* and resolves externals through the injected require (loader module table —
|
||||
* cordis DI entities, no globals, no import map). CSS Modules are compiled by
|
||||
* lightningcss inside the bundle: importing `x.module.css` yields the
|
||||
@@ -11,6 +11,7 @@ import { readFile } from 'node:fs/promises'
|
||||
import { basename, dirname, resolve as resolvePath } from 'node:path'
|
||||
import type { UserConfig } from 'tsdown'
|
||||
import { transform } from 'lightningcss'
|
||||
import { PLATFORM_MODULES } from './web/src/platform.ts'
|
||||
|
||||
/**
|
||||
* Virtual-id wrapper keeping module CSS away from tsdown's own css pipeline
|
||||
@@ -28,22 +29,20 @@ const CSS_VIRTUAL_SUFFIX = '.mjs'
|
||||
*/
|
||||
export const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|session|llm|tools|brand)(\/|$)/
|
||||
|
||||
/** Externals resolved from the loader module table (keep in sync with the shell's seeding list). */
|
||||
export const CLIENT_EXTERNALS = [
|
||||
'react',
|
||||
'react-dom',
|
||||
'react/jsx-runtime',
|
||||
'cordis',
|
||||
'@deepseek-ai/dsh-client-ui-slots',
|
||||
'@deepseek-ai/dsh-client-web-react',
|
||||
'@deepseek-ai/dsh-client-ui-primitives',
|
||||
'@deepseek-ai/dsh-client-connection/client',
|
||||
'@deepseek-ai/dsh-client-runtime/client',
|
||||
'@deepseek-ai/dsh-client-ui-layout/client',
|
||||
'@deepseek-ai/dsh-client-ui-conversation/client',
|
||||
'@deepseek-ai/dsh-client-ui-theme/client',
|
||||
'@deepseek-ai/dsh-client-i18n/client',
|
||||
]
|
||||
/**
|
||||
* Documented TEMPORARY exemption, not a platform module (hence not in
|
||||
* platform.ts): the snapshot-store engine (createSnapshotStore/defineStore/
|
||||
* shallowEqual) lives in runtime pending its promotion-time rehoming, and
|
||||
* five importers (i18n, ui-layout, ui-conversation ×3) ride this single
|
||||
* exemption. At runtime the lazy CJS table answers the require natively:
|
||||
* runtime is an immediately-tier row, its factory is registered before any
|
||||
* dependent bundle materializes. TODO(webload/store-rehome): remove with the
|
||||
* store-engine relocation follow-up.
|
||||
*/
|
||||
const RUNTIME_STORE_EXEMPTION = '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
/** Externals resolved from the loader module table: the platform seed entries plus the documented runtime exemption. */
|
||||
export const CLIENT_EXTERNALS: readonly string[] = [...PLATFORM_MODULES, RUNTIME_STORE_EXEMPTION]
|
||||
|
||||
/**
|
||||
* Build the tsdown config for one UI plugin package: the node-half lib build
|
||||
@@ -51,8 +50,8 @@ export const CLIENT_EXTERNALS = [
|
||||
* the root workspace shape, so the lib half must be restated here — dropping
|
||||
* it leaves the package without lib/index.js and the host Loader cannot
|
||||
* import its node half.
|
||||
* @param id - plugin id (package name), stamped into the loadPlugin handoff
|
||||
* and onto the injected style tags.
|
||||
* @param id - plugin id (package name), stamped into the __ModuleLoader__.load
|
||||
* handoff and onto the injected style tags.
|
||||
* @param libEntry - node-half entries, spelled at the call site so the
|
||||
* package-invariants gate can see `lib/types/invariant.js` in each package's
|
||||
* own tsdown.config.ts (a preset-side glob hides it from the mechanical check).
|
||||
@@ -79,7 +78,7 @@ export function clientBundle(id: string, libEntry: readonly string[]): UserConfi
|
||||
// Types ship from lib/types (tsc); dts here would wrap the banner/footer into .d.cts and break parsing.
|
||||
dts: false,
|
||||
clean: false,
|
||||
external: CLIENT_EXTERNALS,
|
||||
external: [...CLIENT_EXTERNALS],
|
||||
// Browser bundles inline node-idiom deps (zustand/immer read
|
||||
// process.env.NODE_ENV; zustand's esm build also probes
|
||||
// import.meta.env.MODE, which a CJS output cannot carry — rolldown flags
|
||||
@@ -102,24 +101,20 @@ export function clientBundle(id: string, libEntry: readonly string[]): UserConfi
|
||||
// opinion for table entries (external above wins), bundle everything else.
|
||||
noExternal: (id: string) => (CLIENT_EXTERNALS.includes(id) ? undefined : true),
|
||||
plugins: [{
|
||||
// Bundle purity gate: a bare-name import of a module-table package would
|
||||
// slip past CLIENT_EXTERNALS (which lists the /client form) and INLINE a
|
||||
// second copy of that package — duplicate runtime identity (a second
|
||||
// scope Symbol was tonight's white-screen root cause). Resolve-time is
|
||||
// the earliest, most precise interception: rewrite bare table names to
|
||||
// their /client form (the loader registers both specifiers), and reject
|
||||
// any other @deepseek-ai/* leak that is not an inline-safe wire layer.
|
||||
// Bundle purity gate (build-time mirror of the module-edge rules):
|
||||
// platform seed entries stay external, inline-safe wire layers inline,
|
||||
// and every other @deepseek-ai value import is a build error — a
|
||||
// cross-plugin value import either inlines a duplicate runtime instance
|
||||
// or requires a specifier the frozen module table cannot answer.
|
||||
// Cross-plugin collaboration goes through cordis services instead.
|
||||
name: 'dsh-client-bundle-purity',
|
||||
resolveId(source: string) {
|
||||
if (!source.startsWith('@deepseek-ai/')) return null
|
||||
if (CLIENT_EXTERNALS.includes(source)) return null // external wins
|
||||
if (CLIENT_EXTERNALS.includes(`${source}/client`)) {
|
||||
return { id: `${source}/client`, external: true }
|
||||
}
|
||||
if (CLIENT_EXTERNALS.includes(source)) return null // platform module: external wins
|
||||
if (INLINE_SAFE.test(source)) return null // wire/type layer: inline is the point
|
||||
throw new Error(
|
||||
`client bundle purity: "${source}" is not in CLIENT_EXTERNALS and not an inline-safe wire layer — `
|
||||
+ 'import the /client form, add it to the module table, or it inlines a duplicate runtime instance',
|
||||
`client bundle purity: "${source}" is not a platform module (CLIENT_EXTERNALS) and not an inline-safe wire layer — `
|
||||
+ 'cross-plugin value imports are forbidden; collaborate through cordis services (type-only imports are erased and never reach this gate)',
|
||||
)
|
||||
},
|
||||
}, {
|
||||
@@ -158,7 +153,7 @@ export function clientBundle(id: string, libEntry: readonly string[]): UserConfi
|
||||
}],
|
||||
outputOptions: {
|
||||
entryFileNames: 'client.js',
|
||||
banner: `window.DSHClientProxy.loadPlugin({ id: ${JSON.stringify(id)}, factory: (require) => {`,
|
||||
banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(id)}, factory: (require) => {`,
|
||||
footer: `return module.exports; } });`,
|
||||
intro: 'var module = { exports: {} }; var exports = module.exports;',
|
||||
},
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
},
|
||||
"dshClient": {
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-i18n",
|
||||
"@deepseek-ai/dsh-client-runtime",
|
||||
"@deepseek-ai/dsh-client-ui-layout"
|
||||
],
|
||||
"platform": "web"
|
||||
@@ -34,21 +36,27 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-i18n": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-client-i18n": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-layout": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"clsx": "^2.0.0",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
|
||||
@@ -15,14 +15,10 @@
|
||||
*/
|
||||
import { Service } from 'cordis'
|
||||
import type { Context } from 'cordis'
|
||||
// Value import MUST use the /client subpath: only that specifier is in the
|
||||
// bundle externals (CLIENT_EXTERNALS), so it resolves to the shared runtime
|
||||
// module at load time. A bare-specifier value import gets INLINED as a second
|
||||
// module instance whose private scope-tag Symbol never matches the one
|
||||
// SessionsService tags contexts with — scopeOf then always returns undefined
|
||||
// in the browser while unit tests (single-instance path resolution) stay green.
|
||||
import { scopeOf } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { Session, SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
// Type-only imports: a plugin-to-plugin value import is a bundle purity
|
||||
// error, so scope resolution goes through the sessions service (scopeOf
|
||||
// method) instead of the standalone helper.
|
||||
import type { Session, SessionId, SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
/** Scope-addressed conversation service (root singleton, provided as `conversation`). */
|
||||
export class ConversationService extends Service {
|
||||
@@ -83,11 +79,17 @@ export class ConversationService extends Service {
|
||||
|
||||
/** Resolve the caller scope's Session or throw on root contexts. */
|
||||
private scopedSession(op: string): Session {
|
||||
const id = scopeOf(this.ctx)
|
||||
const id = this.scopeId(op)
|
||||
return this.requireSessions().manager.get(id)
|
||||
}
|
||||
|
||||
/** Read the caller's session scope tag via the sessions service; root contexts fail loud. */
|
||||
private scopeId(op: string): SessionId {
|
||||
const id = this.requireSessions().scopeOf(this.ctx)
|
||||
if (id === undefined) {
|
||||
throw new Error(`conversation.${op} requires a session scope — address one via ctx.sessions.scope(id).conversation`)
|
||||
}
|
||||
return this.requireSessions().manager.get(id)
|
||||
return id
|
||||
}
|
||||
|
||||
private requireSessions(): SessionsService {
|
||||
|
||||
@@ -76,6 +76,7 @@ async function bench() {
|
||||
manager: { get: () => sessionFake },
|
||||
scope: (id: SessionId) => mint(id),
|
||||
cell: () => undefined,
|
||||
scopeOf,
|
||||
create: vi.fn(() => Promise.resolve(ROOT)),
|
||||
open: vi.fn(),
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ async function bench(opts?: { sessions?: boolean }) {
|
||||
create: createMock,
|
||||
open: openMock,
|
||||
scope: (id: SessionId) => (id === sid('new-1') ? mint(id) : scopes.get(id)),
|
||||
scopeOf,
|
||||
} as unknown as SessionsService
|
||||
if (opts?.sessions !== false) ctx.provide('sessions', sessionsFake)
|
||||
// Class-plugin mount — the same form apply.ts uses in production.
|
||||
|
||||
@@ -33,19 +33,20 @@
|
||||
"watch": "tsdown --watch"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
},
|
||||
"dshClient": {
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-runtime",
|
||||
"@deepseek-ai/dsh-client-ui-layout"
|
||||
],
|
||||
"platform": "web"
|
||||
@@ -34,21 +35,25 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-layout": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"clsx": "^2.0.0",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
|
||||
@@ -28,10 +28,6 @@
|
||||
"platform": "web",
|
||||
"immediately": true
|
||||
},
|
||||
"scripts": {
|
||||
"bundle": "tsdown",
|
||||
"watch": "tsdown --watch"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
|
||||
@@ -33,19 +33,19 @@
|
||||
"watch": "tsdown --watch"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* Real tsdown artifact shape: lib/client.js hands off through
|
||||
* window.DSHClientProxy.loadPlugin, resolves externals through the injected
|
||||
* window.__ModuleLoader__.load, resolves externals through the injected
|
||||
* require, returns the export surface (apply + inject), and a mounted apply
|
||||
* registers both view tabs into a real SlotsService ring. Skips when dist/ is
|
||||
* not built (`pnpm --filter @deepseek-ai/dsh-client-ui-trajectory bundle`).
|
||||
@@ -15,7 +15,7 @@ import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
const PLUGIN_ID = '@deepseek-ai/dsh-client-ui-trajectory'
|
||||
|
||||
interface Handoff { id: string; factory: (require: (spec: string) => unknown) => Record<string, unknown> }
|
||||
type Win = { DSHClientProxy?: { loadPlugin(h: Handoff): void } }
|
||||
type Win = { __ModuleLoader__?: { load(h: Handoff): void } }
|
||||
|
||||
function readBundle(): string | undefined {
|
||||
try {
|
||||
@@ -28,7 +28,7 @@ function readBundle(): string | undefined {
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
delete (window as Win).DSHClientProxy
|
||||
delete (window as Win).__ModuleLoader__
|
||||
for (const el of document.querySelectorAll('style')) el.remove()
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('tsdown client artifact', () => {
|
||||
|
||||
async function loadArtifact() {
|
||||
let handoff: Handoff | undefined
|
||||
;(window as Win).DSHClientProxy = { loadPlugin: (h) => { handoff = h } }
|
||||
;(window as Win).__ModuleLoader__ = { load: (h) => { handoff = h } }
|
||||
// Same execution form the loader uses (inline script eval, window scope) —
|
||||
// the implied-eval ban targets accidental string execution, not this
|
||||
// deliberate bundle-execution fixture.
|
||||
|
||||
@@ -39,7 +39,7 @@ export type {
|
||||
} from './rpc.ts'
|
||||
|
||||
// ---- Errors and ids ----
|
||||
export { RpcId } from './rpc.ts'
|
||||
export { RpcId, transportError } from './rpc.ts'
|
||||
export type { RpcError, RpcErrorCode, RpcErrorDetailsMap, RpcResult } from './rpc.ts'
|
||||
|
||||
// ---- Method registry and derived generics ----
|
||||
|
||||
@@ -50,6 +50,20 @@ export type RpcError = {
|
||||
/** Business success/failure result: the result slot of a unary response; methods never throw business errors. */
|
||||
export type RpcResult<T> = { ok: true; value: T } | { ok: false; error: RpcError }
|
||||
|
||||
/**
|
||||
* Fold a transport exception into the RpcResult error branch (unified error
|
||||
* surface; 'internal' as the catch-all code). Lives with RpcResult so every
|
||||
* carrier consumer folds the same way.
|
||||
* @param error - the thrown value from the carrier.
|
||||
* @returns the error branch of an RpcResult.
|
||||
*/
|
||||
export function transportError<T>(error: unknown): RpcResult<T> {
|
||||
return {
|
||||
ok: false,
|
||||
error: { code: 'internal', message: error instanceof Error ? error.message : String(error), details: {} },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signature-layer narrow form, request side (domain-interface view, shared by
|
||||
* both directions): rpcId is explicit in the signature, never mixed into the
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Pins the client-bundle purity gate (tsdown preset resolveId classifier):
|
||||
* a bare-name import of a module-table package must rewrite to its /client
|
||||
* external form (inlining it duplicates runtime identity — the P0
|
||||
/* leak that is not an
|
||||
* inline-safe wire layer must fail the build loudly.
|
||||
* Pins the client-bundle purity gate (tsdown preset resolveId classifier),
|
||||
* the build-time mirror of the module-edge rules: platform module-table
|
||||
* entries stay external, inline-safe wire layers inline, and every other
|
||||
* @deepseek-ai value import — including a bare plugin-package name and a
|
||||
* cross-plugin /client subpath — must fail the build loudly (cross-plugin
|
||||
* collaboration goes through cordis services, never module imports).
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { CLIENT_EXTERNALS, clientBundle } from '../packages/client/tsdown.client.ts'
|
||||
@@ -23,22 +24,16 @@ function purityResolveId(): ResolveId {
|
||||
describe('client bundle purity gate', () => {
|
||||
const resolveId = purityResolveId()
|
||||
|
||||
it('leaves table entries and non-scoped specifiers alone', () => {
|
||||
it('leaves platform table entries and non-scoped specifiers alone', () => {
|
||||
expect(resolveId('@deepseek-ai/dsh-client-ui-slots')).toBeNull()
|
||||
expect(resolveId('@deepseek-ai/dsh-client-runtime/client')).toBeNull()
|
||||
expect(resolveId('@deepseek-ai/dsh-client-web-react')).toBeNull()
|
||||
expect(resolveId('@deepseek-ai/dsh-client-ui-primitives')).toBeNull()
|
||||
expect(resolveId('react')).toBeNull()
|
||||
expect(resolveId('zod')).toBeNull()
|
||||
})
|
||||
|
||||
it('rewrites a bare table-package name to its external /client form (duplicate-instance prevention)', () => {
|
||||
expect(resolveId('@deepseek-ai/dsh-client-connection')).toEqual({
|
||||
id: '@deepseek-ai/dsh-client-connection/client',
|
||||
external: true,
|
||||
})
|
||||
expect(resolveId('@deepseek-ai/dsh-client-ui-layout')).toEqual({
|
||||
id: '@deepseek-ai/dsh-client-ui-layout/client',
|
||||
external: true,
|
||||
})
|
||||
it('rejects retired table entries (web-react/store left the 8-entry seed)', () => {
|
||||
expect(() => resolveId('@deepseek-ai/dsh-client-web-react/store')).toThrow(/purity/)
|
||||
})
|
||||
|
||||
it('lets inline-safe wire layers inline', () => {
|
||||
@@ -52,9 +47,16 @@ describe('client bundle purity gate', () => {
|
||||
expect(() => resolveId('@deepseek-ai/dsh-client-web')).toThrow(/purity/)
|
||||
})
|
||||
|
||||
it('every /client external has no bare-name twin in the table (the rewrite assumption)', () => {
|
||||
for (const entry of CLIENT_EXTERNALS) {
|
||||
if (entry.endsWith('/client')) expect(CLIENT_EXTERNALS).not.toContain(entry.slice(0, -'/client'.length))
|
||||
}
|
||||
it('throws on cross-plugin value imports — bare plugin names and /client subpaths alike (the rewrite arm is gone)', () => {
|
||||
expect(() => resolveId('@deepseek-ai/dsh-client-connection')).toThrow(/purity/)
|
||||
expect(() => resolveId('@deepseek-ai/dsh-client-runtime')).toThrow(/purity/)
|
||||
expect(() => resolveId('@deepseek-ai/dsh-client-ui-layout/client')).toThrow(/purity/)
|
||||
})
|
||||
|
||||
it('carries exactly one documented temporary exemption: runtime/client (store engine pending rehoming)', () => {
|
||||
expect(resolveId('@deepseek-ai/dsh-client-runtime/client')).toBeNull()
|
||||
const dshClientChannels = CLIENT_EXTERNALS.filter(
|
||||
entry => entry.startsWith('@deepseek-ai/') && entry.endsWith('/client'))
|
||||
expect(dshClientChannels).toEqual(['@deepseek-ai/dsh-client-runtime/client'])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user