Merge remote-tracking branch 'origin/master' into codex/trim-redundant-comments

# Conflicts:
#	packages/client/connection/src/index.ts
#	packages/host/webserver/tests/web-plugins.spec.ts
This commit is contained in:
Turtle
2026-07-25 13:06:40 +08:00
140 changed files with 8864 additions and 2535 deletions

View File

@@ -1,6 +1,6 @@
# @deepseek-ai/dsh-client-web
Web shell kernel: `bootWebShell(el, seams?)` mounts the whole client through the two-stage boot (web2). Stage one (module face): build the client module system (`@deepseek-ai/dsh-client-modules`) over the host-pushed entry graph (`window.__DSH_BOOT__`) and prefetch the `immediately` tier in parallel — bundle execution registers factories only. Stage two (plugin face): mount the vendored cordis Loader with the module system injected as its `internal` seam, create one loader entry per graph row plus the shell-own app-shell assembly entry (tree.import materializes each module), and gate AppRoot on the settle (loader quiesced + every entry fiber ACTIVE → full UI in one switch). Composition is entirely the host graph's: the roster and the immediately tier live in the composing app; the shell makes zero composition decisions.
Web shell kernel: `new AppWebEntry(el, seams?).run()` mounts the whole client through the two-stage boot (web2). Stage one (module face): build the client module system (`@deepseek-ai/dsh-client-modules`) over the host-pushed entry graph (`window.__DSH_BOOT__`) and prefetch the `immediately` tier in parallel — bundle execution registers factories only. Stage two (plugin face): mount the vendored cordis Loader with the module system injected as its `internal` seam, create one loader entry per graph row plus the shell-own app-shell assembly entry (tree.import materializes each module), and gate AppRoot on the settle (loader quiesced + every entry fiber ACTIVE → full UI in one switch). Composition is entirely the host graph's: the roster and the immediately tier live in the composing app; the shell makes zero composition decisions.
Shell self-sufficiency (web2 hard rule): the kernel value-imports no plugin package — the boot status store and signals are hand-rolled here (`loader-status.ts`), so the loading page works while (and especially when) plugins fail. The app-shell assembly (`@deepseek-ai/dsh-client-app-shell`, a shell-owned pseudo entry with no npm package behind it) is the only module registered through `registerStatic`; it inject-waits on slots/sessions/layout like any plugin.

View File

@@ -1,23 +1,32 @@
/**
* Web shell boot — the kernel face consumed by the apps/web entry. Everything
* here is machinery that cannot itself be an entry, and none of it
* Web shell boot kernel — the face consumed by the apps/web entry. Everything
* here is machinery that cannot itself be a loader entry, and none of it
* value-imports a plugin package (web2 shell self-sufficiency rule: the
* loading page must work while — especially when — plugins fail).
* loading page must work while — especially when — plugins fail). The one
* sanctioned exception is the modules package (design §4.7 bootstrap
* identity): the module system cannot arrive through itself, so its class
* and its client-half wrapper are shell-bundled and the kernel adopts its
* plugin entry once cordis is up.
*
* Two-stage boot (web2 §0):
* Stage one (module face): build the module system over the host graph
* (`window.__DSH_BOOT__`) and prefetch every `immediately` row in parallel
* — fetch + execute registers factories only; module side effects wait for
* materialization. Prefetch failures are non-fatal here: stage two's
* import path retries the fetch and owns the loud failure.
* Stage two (plugin face): mount the vendored cordis Loader, inject the
* module system as its internal seam (BEFORE any entry exists — the
* bare-import fallback in tree.import must never run in a browser), create
* one loader entry per graph row (tree.import materializes each module),
* let fibers activate on service availability, then loader.await() + a
* full fiber sweep (all ACTIVE, else reject listing who/what/which
* service) → flip the settled signal so AppRoot switches to the real UI in
* one pass.
* AppWebEntry.run(), module face first, then plugin face: parse
* `window.__DSH_BOOT__` into the two-view BootManifest (wire boundary, D16)
* → build the module system over the module-view rows → render the loading
* page → prefetch every `immediately` row in parallel with mounting the
* vendored cordis Loader (internal-seam injection BEFORE any entry exists —
* the bare-import fallback in tree.import must never run in a browser) →
* await the prefetch tier, THEN adopt the modules entry and create one
* loader entry per plugin-view row plus the shell-own app-shell assembly
* entry → loader.await() + a full fiber sweep (all ACTIVE, else fail
* listing who/what/which service) → flip the settled signal so AppRoot
* switches to the real UI in one pass.
*
* Entry creation waits for the whole immediately tier: materialization runs
* synchronous cross-package require edges (e.g. i18n → runtime/client) that
* fiber inject waiting cannot protect — a bundle's factory must be
* registered before any dependent entry materializes. Per-row prefetch
* failures still resolve silently (the create-side import refetches and
* owns the loud failure), so the barrier never turns one bad bundle into a
* boot-wide fail-fast.
*
* Composition lives in the host graph; the shell makes zero composition
* decisions (the app-shell assembly is itself a graph entry, the only
@@ -25,148 +34,205 @@
*/
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { createRoot } from 'react-dom/client'
import { createRoot, type Root } from 'react-dom/client'
import * as ModulesClient from '@deepseek-ai/dsh-client-modules/client'
import {
createClientModuleLoader,
type ClientModuleLoader, type ClientModuleLoaderOptions, type DshWindow, type WebBootGraph,
} from '@deepseek-ai/dsh-client-modules'
ClientModuleSystem, parseBootManifest,
type BootManifest, type ClientModuleSystemOptions, type DshWindow,
} from '@deepseek-ai/dsh-client-modules/client'
import * as AppShell from './app-shell.ts'
import { APP_SHELL_ID } from './app-shell.ts'
import { AppRoot } from './AppRoot.tsx'
import { getStaticModules } from './seed.ts'
import {
STATE_LABELS, createLoaderStatusStore, createSignal, type LoaderStatusStore,
} from './loader-status.ts'
import { STATE_LABELS, createLoaderStatusStore, createSignal } from './loader-status.ts'
import './base.css'
/** Module transport seams the shell passes through (jsdom tests replace the <script> path). */
export type BootSeams = Pick<ClientModuleLoaderOptions, 'fetchBundle' | 'executeBundle'>
export type BootSeams = Pick<ClientModuleSystemOptions, 'fetchBundle' | 'executeBundle'>
/**
* Sweep every loader entry after the tree quiesced: an entry without a fiber
* failed its import; a fiber not ACTIVE is FAILED (apply threw) or PENDING
* (a required service never arrived — cordis inject waiting has no timeout,
* so this sweep is the fail-loud compensation).
* The modules package's own graph row id. The kernel adopts that entry
* itself (its wrapper is statically registered — shell-bundled code, never
* fetched), so the plugin-row loop must skip it: the vendored Group.create
* does not deduplicate by name, and a second fiber would provide 'modules'
* twice.
*/
function assertEntriesActive(ctx: Context): void {
const failures: string[] = []
for (const entry of ctx.loader.entries()) {
const name = entry.options.name
if (entry.fiber === undefined) {
failures.push(`${name}: import failed (see console for the import error)`)
continue
}
const state = STATE_LABELS[entry.fiber.state]
if (state === 'active') continue
if (state === 'pending') {
const missing = Object.keys(entry.fiber.inject).filter((service) => ctx.get(service) === undefined)
failures.push(`${name}: pending (waiting for service${missing.length === 1 ? '' : 's'}: ${missing.join(', ') || 'unknown'})`)
} else {
failures.push(`${name}: ${state}`)
}
}
if (failures.length > 0) {
throw new Error(`web boot: ${String(failures.length)} entr${failures.length === 1 ? 'y' : 'ies'} did not activate\n${failures.join('\n')}`)
}
}
/** Stage one: prefetch the immediately tier (factory registration only; failures defer to stage two's import). */
async function prefetchImmediateTier(modules: ClientModuleLoader, graph: WebBootGraph): Promise<void> {
await Promise.all(graph.entries
.filter((row) => row.immediately === true)
.map((row) => modules.prefetch(row.id).catch(() => {
// Import (stage two) refetches and reports this loudly per entry;
// swallowing here keeps one failing prefetch from masking the others.
})))
}
/** Stage two: mount the Loader, inject the internal seam, create the graph entries, settle, sweep. */
async function runPluginBoot(
ctx: Context, modules: ClientModuleLoader, graph: WebBootGraph, status: LoaderStatusStore,
): Promise<void> {
await ctx.plugin(Loader)
const loader = ctx.loader
// Inject the module system BEFORE any entry exists: tree.import falls back
// to a bare dynamic import when internal is undefined, which in a browser
// is a guaranteed loud failure — correct as a tripwire, never as a path.
loader.internal = modules as never
// Status projection: AppRoot displays fiber truth. Every internal/status
// transition under an entry re-projects that entry's row from its ROOT
// fiber (child plugin fibers share the same entry).
ctx.on('internal/status', (fiber) => {
const entry = fiber.entry
if (entry === undefined || entry.fiber === undefined) return
status.set(entry.options.name, STATE_LABELS[entry.fiber.state])
})
// Entry creation order carries no semantics (fiber inject waiting owns
// activation order); creating concurrently lets non-prefetched bundle
// fetches parallelize. The app-shell assembly entry is appended by the
// kernel: it is shell-own code (host graph rows are all plugin bundles),
// and mounting the assembly is not a composition decision — it rides the
// same entry lifecycle so the sweep and status cover it uniformly.
const rows = [...graph.entries.map((row) => row.id), APP_SHELL_ID]
await Promise.all(rows.map(async (name) => {
status.set(name, 'loading')
const id = await loader.create({ name })
// A failed import leaves the entry fiberless (Entry._init logs and
// returns); project it as failed — no fiber means no status event.
if (loader.resolve(id).fiber === undefined) {
status.set(name, 'failed')
}
}))
await loader.await()
assertEntriesActive(ctx)
}
const MODULES_ID = '@deepseek-ai/dsh-client-modules'
/**
* Mount the web shell into a DOM element and start the two-stage boot chain.
* @param el - mount point (the app's #root).
* @param seams - optional module transport overrides (test environments).
* @returns unmount disposer.
* The web shell kernel: mounts the loading page into a DOM element and runs
* the two-stage boot over the host graph. Fields hold only what must exist
* before cordis does — the parsed manifest, the module system, and the
* loading-page UI handles; everything else lives in plugins.
*/
export function bootWebShell(el: HTMLElement, seams?: BootSeams): () => void {
const graph = (globalThis as DshWindow).__DSH_BOOT__
if (graph === undefined) throw new Error('web boot: no entry graph (window.__DSH_BOOT__ missing)')
export class AppWebEntry {
private readonly el: HTMLElement
private readonly seams: BootSeams | undefined
private readonly status = createLoaderStatusStore()
private readonly settled = createSignal(false)
private readonly error = createSignal<string | undefined>(undefined)
// Assigned by run() before any private method or settled-gated closure reads them.
private ctx!: Context
private modules!: ClientModuleSystem
private manifest!: BootManifest
private root: Root | undefined
const ctx = new Context()
const modules = createClientModuleLoader({ graph, staticModules: getStaticModules(), ...seams })
// The app-shell assembly is the only shell-own module: every other graph
// row is a plugin bundle arriving through fetch (web2 single package form).
modules.registerStatic(APP_SHELL_ID, AppShell)
// Contract C5: the module system is a boot-owned kernel service (ctx.modules).
ctx.reflect.provide('modules', modules)
/**
* Hold the mount point; all work happens in {@link run}.
* @param el - mount point (the app's #root).
* @param seams - optional module transport overrides (test environments).
*/
constructor(el: HTMLElement, seams?: BootSeams) {
this.el = el
this.seams = seams
}
const status = createLoaderStatusStore()
const settled = createSignal(false)
const error = createSignal<string | undefined>(undefined)
/**
* Run the boot chain to settlement. Boot-chain failures resolve (not
* reject): the loading page stays up and renders the failure report (the
* fail-loud surface the kernel owns). Rejects only when the boot manifest
* is missing or malformed — there is nothing to boot against.
* @returns resolves once the UI settled or the failure report rendered.
*/
async run(): Promise<void> {
this.manifest = parseBootManifest((globalThis as DshWindow).__DSH_BOOT__)
const root = createRoot(el)
root.render(
<AppRoot
settled={settled}
status={status}
error={error}
renderApp={() => {
const shell = ctx.get('appShell')
// Unreachable after a clean settle (the app-shell entry is in every graph).
if (shell === undefined) throw new Error('web boot: appShell service missing after settled')
return shell.renderApp()
}}
/>,
)
this.modules = new ClientModuleSystem({
modules: this.manifest.modules, staticModules: getStaticModules(), ...this.seams,
})
// The app-shell assembly is the only shell-own module: every other graph
// row is a plugin bundle arriving through fetch (web2 single package form).
this.modules.registerStatic(APP_SHELL_ID, AppShell)
// Adoption handoff, supply side (design §4.7): register the modules
// package's own client half under its bare package name (= graph row id
// = entry name — a suffixed key would miss the statics branch and
// trigger a real fetch), and put the instance on the kernel slot the
// wrapper's apply reads to provide ctx.modules.
this.modules.registerStatic(MODULES_ID, ModulesClient)
;(globalThis as DshWindow).__DSH_MODULES__ = this.modules
prefetchImmediateTier(modules, graph)
.then(() => runPluginBoot(ctx, modules, graph, status))
.then(
() => { settled.set(true) },
(reason: unknown) => {
// Stay on the loading page; surface the sweep report (fail loud).
console.error(reason)
error.set(reason instanceof Error ? reason.message : String(reason))
},
this.root = createRoot(this.el)
this.root.render(
<AppRoot
settled={this.settled}
status={this.status}
error={this.error}
renderApp={() => {
const shell = this.ctx.get('appShell')
// Unreachable after a clean settle (the app-shell entry is in every graph).
if (shell === undefined) throw new Error('web boot: appShell service missing after settled')
return shell.renderApp()
}}
/>,
)
return () => { root.unmount() }
// The immediately tier prefetches in parallel with Loader mounting;
// runPluginBoot awaits it before creating entries (see module comment:
// cross-package synchronous require edges need every immediately-tier
// factory registered before any materialization).
const prefetching = this.prefetchImmediateTier()
this.ctx = new Context()
try {
await this.runPluginBoot(prefetching)
this.settled.set(true)
} catch (reason) {
// Stay on the loading page; surface the sweep report (fail loud).
console.error(reason)
this.error.set(reason instanceof Error ? reason.message : String(reason))
}
}
/** Unmount the shell (loading page or settled UI). */
dispose(): void {
this.root?.unmount()
}
/** Prefetch the immediately tier (factory registration only; failures defer to the import path). */
private async prefetchImmediateTier(): Promise<void> {
await Promise.all(this.manifest.plugins
.filter((row) => row.immediately)
.map((row) => this.modules.prefetch(row.id).catch(() => {
// Import refetches and reports this loudly per entry; swallowing
// here keeps one failing prefetch from masking the others.
})))
}
/** Plugin face: mount the Loader, inject the internal seam, adopt modules, create the graph entries, settle, sweep. */
private async runPluginBoot(prefetching: Promise<void>): Promise<void> {
const ctx = this.ctx
await ctx.plugin(Loader)
const loader = ctx.loader
// Inject the module system BEFORE any entry exists: tree.import falls back
// to a bare dynamic import when internal is undefined, which in a browser
// is a guaranteed loud failure — correct as a tripwire, never as a path.
loader.internal = this.modules as never
// Status projection: AppRoot displays fiber truth. Every internal/status
// transition under an entry re-projects that entry's row from its ROOT
// fiber (child plugin fibers share the same entry).
ctx.on('internal/status', (fiber) => {
const entry = fiber.entry
if (entry === undefined || entry.fiber === undefined) return
this.status.set(entry.options.name, STATE_LABELS[entry.fiber.state])
})
// Barrier before any entry exists: entry creation materializes bundles,
// and materialization runs synchronous cross-package require edges that
// need every immediately-tier factory already registered (module
// comment). Resolves even when individual prefetches failed.
await prefetching
// Adoption handoff, plugin side: the modules entry is created first —
// its wrapper apply reads the kernel slot and provides ctx.modules (the
// provide lives on the plugin face; see MODULES_ID for why the row loop
// must then skip it).
const rows = [MODULES_ID, ...this.manifest.plugins.map((row) => row.id).filter((id) => id !== MODULES_ID), APP_SHELL_ID]
// Entry creation order carries no semantics (fiber inject waiting owns
// activation order); creating concurrently lets non-prefetched bundle
// fetches parallelize. The app-shell assembly entry is appended by the
// kernel: it is shell-own code (host graph rows are all plugin bundles),
// and mounting the assembly is not a composition decision — it rides the
// same entry lifecycle so the sweep and status cover it uniformly.
await Promise.all(rows.map(async (name) => {
this.status.set(name, 'loading')
const id = await loader.create({ name })
// A failed import leaves the entry fiberless (Entry._init logs and
// returns); project it as failed — no fiber means no status event.
if (loader.resolve(id).fiber === undefined) {
this.status.set(name, 'failed')
}
}))
await loader.await()
this.assertEntriesActive()
}
/**
* Sweep every loader entry after the tree quiesced: an entry without a
* fiber failed its import; a fiber not ACTIVE is FAILED (apply threw) or
* PENDING (a required service never arrived — cordis inject waiting has no
* timeout, so this sweep is the fail-loud compensation).
*/
private assertEntriesActive(): void {
const ctx = this.ctx
const failures: string[] = []
for (const entry of ctx.loader.entries()) {
const name = entry.options.name
if (entry.fiber === undefined) {
failures.push(`${name}: import failed (see console for the import error)`)
continue
}
const state = STATE_LABELS[entry.fiber.state]
if (state === 'active') continue
if (state === 'pending') {
const missing = Object.keys(entry.fiber.inject).filter((service) => ctx.get(service) === undefined)
failures.push(`${name}: pending (waiting for service${missing.length === 1 ? '' : 's'}: ${missing.join(', ') || 'unknown'})`)
} else {
failures.push(`${name}: ${state}`)
}
}
if (failures.length > 0) {
throw new Error(`web boot: ${String(failures.length)} entr${failures.length === 1 ? 'y' : 'ies'} did not activate\n${failures.join('\n')}`)
}
}
}

View File

@@ -1,13 +1,13 @@
/**
* Web shell library entry. The shell's product is {@link bootWebShell} —
* apps/web's vite entry calls it against #root; everything else (AppRoot
* Web shell library entry. The shell's product is {@link AppWebEntry} —
* apps/web's vite entry runs it against #root; everything else (AppRoot
* gate, app-shell assembly entry, module-table staticModules, platform constants) is
* internal to the boot chain. PLATFORM_MODULES is re-exported as the C1
* single source of truth for the tsdown client externals projection.
* @module @deepseek-ai/dsh-client-web
*/
export { bootWebShell, type BootSeams } from './boot.tsx'
export { AppWebEntry, type BootSeams } from './boot.tsx'
export { AppRoot, type AppRootProps } from './AppRoot.tsx'
export { buildRenderApp, type AssemblyDeps } from './app.tsx'
export { DocumentTitle, type DocumentTitleProps } from './DocumentTitle.tsx'