mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
* feat(web): boot past failed optional client plugins The Web client boot audit now follows Host startup strictness. An inactive entry on the global required list (module system, Session transport, renderer, application frame, Session view, and sidebar) still rejects startup and the report marks the required entries. Other inactive entries produce one console warning and the application mounts without them. The boot page no longer draws a failed entry before the audit decides. * fix(web): audit boot entries whose creation rejects and name activation errors ClientEntries.start() now logs a row whose Loader creation rejects, such as a module that exports no plugin, and leaves it inactive for the boot audit, as the Host Loader group does. The audit awaits only failed fibers to report their recorded activation error. Notes, READMEs, and the Web client subsystem page describe the browser boot audit; tests cover invalid exports, carrier presentation of optional failures, and required names absent from the boot graph. * test(web): fail test carriers and e2e on inactive client entries The product boot only warns about inactive entries outside its required list, but tests must still surface every plugin that fails. TestClient and createClientTest now require every roster entry to activate unless a case passes allowInactiveEntries. The watchConsole tripwire, the real-host smoke, and the assembled jsdom boot treat a web boot warning as a failure with no opt-out. Docs: the Web README names Session control, notes that an entry without a fiber is reported as an import failure like the Host audit's "failed to import", and the startup strictness note lists the browser tests.
128 lines
5.7 KiB
TypeScript
128 lines
5.7 KiB
TypeScript
/**
|
|
* Web boot kernel. It owns only the module system, Cordis loader, and a
|
|
* framework-free boot page; plugin composition and the renderer handoff are
|
|
* `bootClient` and `mountClient`. The dynamic UI renderer receives the mount
|
|
* point after the activation audit accepts the client tree.
|
|
* @module @deepseek-ai/dsh-client-web/src/boot
|
|
*/
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import type {
|
|
BootManifest, ClientModuleCreateOptions, ClientModuleSystem, DshWindow,
|
|
} from '@deepseek-ai/dsh-client-modules/client'
|
|
import { bootClient } from './boot-client.ts'
|
|
import { BootPage } from './boot-page.ts'
|
|
import { mountClient } from './mount.ts'
|
|
import { getStaticModules } from './seed.ts'
|
|
import { installWindowDragRecall } from './window-drag/recall.ts'
|
|
import './base.css'
|
|
|
|
/** Module transport hook replaced by jsdom tests. */
|
|
export type BootSeams = Pick<ClientModuleCreateOptions, 'loadBundle' | 'resolveAsset'>
|
|
|
|
/** Browser boot entry consumed by `apps/web`. */
|
|
export class AppWebEntry {
|
|
private readonly container: HTMLElement
|
|
private readonly seams: BootSeams | undefined
|
|
private readonly page: BootPage
|
|
private ctx: Context | undefined
|
|
private stopDragRecall: (() => void) | undefined
|
|
private modules!: ClientModuleSystem
|
|
private manifest!: BootManifest
|
|
|
|
/**
|
|
* Draw the boot page; {@link run} starts the loader.
|
|
* @param container - Application mount point.
|
|
* @param seams - Optional module transport replacement.
|
|
*/
|
|
constructor(container: HTMLElement, seams?: BootSeams) {
|
|
this.container = container
|
|
this.seams = seams
|
|
this.page = new BootPage(container)
|
|
}
|
|
|
|
/**
|
|
* Load and activate every client entry, audit activation, then hand the
|
|
* mount point to the UI renderer. A required-entry failure remains visible
|
|
* on the boot page; optional-entry failures are reported to the console and
|
|
* the application mounts without them.
|
|
* @param onFailure - Optional carrier-owned fatal presentation; keeps the boot page visible.
|
|
* @returns Resolves after application mount or failure reporting.
|
|
*/
|
|
async run(onFailure?: (reason: unknown) => void): Promise<void> {
|
|
try {
|
|
// Boot-readiness gate: whichever bootstrap applies the injection table
|
|
// settles this deferred once every row has taken effect — the served
|
|
// index resolves it in the rendered tail, so the await returns on the
|
|
// next microtask; an asynchronous bootstrap resolves it after its last
|
|
// row, or rejects it into the failure rendering below. An absent global
|
|
// means no bootstrap owns the document and there is nothing to wait for.
|
|
await (globalThis as { __DSH_BOOT_READY__?: { promise: Promise<void> } }).__DSH_BOOT_READY__?.promise
|
|
const win = globalThis as DshWindow
|
|
const moduleLoader = win.__ModuleLoader__
|
|
if (moduleLoader === undefined) {
|
|
throw new Error('web boot: window.__ModuleLoader__ bootstrap facade is missing')
|
|
}
|
|
// A pre-injected transport (the worker preview page) owns bundle and asset
|
|
// bytes; its loadBundle and resolveAsset are the defaults and explicit seams still win. The global
|
|
// is `ClientTransportHooks`, owned by @deepseek-ai/dsh-client-connection;
|
|
// this structural slice reads two optional members without adding a
|
|
// package edge.
|
|
const transport = (globalThis as {
|
|
__DSH_TRANSPORT__?: Pick<ClientModuleCreateOptions, 'loadBundle' | 'resolveAsset'>
|
|
}).__DSH_TRANSPORT__
|
|
this.modules = moduleLoader.create({
|
|
boot: win.__DSH_BOOT__,
|
|
staticModules: getStaticModules(),
|
|
...transport?.loadBundle === undefined ? {} : { loadBundle: transport.loadBundle },
|
|
...transport?.resolveAsset === undefined ? {} : { resolveAsset: transport.resolveAsset },
|
|
...this.seams,
|
|
})
|
|
this.manifest = this.modules.manifest
|
|
|
|
const prefetching = this.prefetchImmediateTier()
|
|
const ctx = new Context()
|
|
this.ctx = ctx
|
|
this.page.setTotal(this.manifest.plugins.length)
|
|
await prefetching
|
|
await bootClient({
|
|
ctx,
|
|
modules: this.modules,
|
|
manifest: this.manifest,
|
|
// A failed entry may be optional; only the audit decides whether the boot page reports a failure.
|
|
onEntryState: (name, state) => {
|
|
if (state !== 'failed') this.page.setState(name, state)
|
|
},
|
|
})
|
|
// The shell owns the one watcher that keeps Electron's window drag rects in
|
|
// step with the rows that own them (electron#32341), so no chrome row has to
|
|
// know that trap. It installs before the first mount, so the surface the
|
|
// renderer draws is the one the first frame measures.
|
|
this.stopDragRecall = installWindowDragRecall({ document: this.container.ownerDocument })
|
|
await mountClient(ctx, this.container)
|
|
} catch (reason) {
|
|
console.error(reason)
|
|
if (onFailure !== undefined) onFailure(reason)
|
|
else this.page.fail(reason instanceof Error ? reason.message : String(reason))
|
|
}
|
|
}
|
|
|
|
/** Dispose the client plugin tree and whichever page owns the mount point. */
|
|
async dispose(): Promise<void> {
|
|
this.stopDragRecall?.()
|
|
this.stopDragRecall = undefined
|
|
const ctx = this.ctx
|
|
this.ctx = undefined
|
|
if (ctx !== undefined) await ctx.fiber.dispose()
|
|
this.page.dispose()
|
|
}
|
|
|
|
/** Prefetch stage-one bundles and their dynamic requests before concurrent plugin imports. */
|
|
private async prefetchImmediateTier(): Promise<void> {
|
|
await Promise.all(this.manifest.plugins
|
|
.filter(row => row.immediately)
|
|
.map(row => this.modules.prefetch(row.id).catch((_prefetchError: unknown) => {
|
|
// Prefetch only starts transport early; the Loader import retries and reports this bundle failure.
|
|
})))
|
|
}
|
|
}
|