diff --git a/apps/web/package.json b/apps/web/package.json index a6b5a9b43f..3c8f90b6a0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -24,7 +24,6 @@ "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-client-web-react": "workspace:^", - "@deepseek-ai/dsh-host-webserver": "workspace:^", "@types/node": "^22.0.0", "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index 16ae6e9ed9..feb85db15a 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -3,8 +3,8 @@ * loader holding, module-table seeding, AppRoot gate, plugin assembly — lives * in @deepseek-ai/dsh-client-web; this file only finds the mount point. */ -import { bootWebShell } from '@deepseek-ai/dsh-client-web' +import { AppWebEntry } from '@deepseek-ai/dsh-client-web' const el = document.getElementById('root') if (el === null) throw new Error('web app: missing #root') -bootWebShell(el) +void new AppWebEntry(el).run() diff --git a/apps/web/tests/session-title.snapshot.ts b/apps/web/tests/session-title.snapshot.ts index 78023d5455..673e92f9ce 100644 --- a/apps/web/tests/session-title.snapshot.ts +++ b/apps/web/tests/session-title.snapshot.ts @@ -3,8 +3,8 @@ import { readFileSync } from 'node:fs' import { join } from 'node:path' import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' -import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules' -import { bootWebShell } from '@deepseek-ai/dsh-client-web' +import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client' +import { AppWebEntry } from '@deepseek-ai/dsh-client-web' const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [ { id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true }, @@ -81,13 +81,15 @@ it('projects initial and revised durable titles through the built eight-plugin f const root = document.querySelector('#root') if (root === null) throw new Error('snapshot root missing') act(() => { - unmount = bootWebShell(root, { + const entry = new AppWebEntry(root, { fetchBundle: (url) => { const code = bundles.get(url) return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code) }, executeBundle: (code) => { (0, eval)(code) }, }) + void entry.run() + unmount = () => { entry.dispose() } }) const projectLabel = await screen.findByText('fixture', {}, { timeout: 10_000 }) diff --git a/apps/web/tests/smoke-fixture.e2e.ts b/apps/web/tests/smoke-fixture.e2e.ts deleted file mode 100644 index 0726d14c8b..0000000000 --- a/apps/web/tests/smoke-fixture.e2e.ts +++ /dev/null @@ -1,291 +0,0 @@ -// Keyless boot-chain smoke over the REAL carrier: startWebServer + entry -// graph (__DSH_BOOT__ web2 shape) injection + built shell dist in a real -// chromium. First describe: graph injection + the fail-loud half. Second -// describe: the settled success pass — all nine REAL tsdown bundles load -// through the module system + vendored Loader chain in ?fixture mode (the -// infrastructure four ride the immediately prefetch tier, the UI rows fetch -// on demand), the three-column frame appears in one flip, and the resident -// question completes through the real UI stack. The full model round lands -// in smoke-real under the W5 real-host standard. -import { existsSync } from 'node:fs' -import { fileURLToPath } from 'node:url' -import type { Browser, Page } from 'playwright' -import { chromium } from 'playwright' -import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' -import { startWebServer } from '@deepseek-ai/dsh-host-webserver' -import type { WebBootEntry, WebBootGraph } from '@deepseek-ai/dsh-host-webserver' -import { DIST_INDEX, probeFreePort, requireDist, saveFailureShot } from './support.ts' - -const bundlePath = (dir: string): string => - fileURLToPath(new URL(`../../../packages/client/${dir}/lib/client.js`, import.meta.url)) - -const LAYOUT_ID = '@deepseek-ai/dsh-client-ui-layout' -const SIDEBAR_ID = '@deepseek-ai/dsh-client-ui-sidebar' - -/** id ↔ bundle table for the success pass (the complete Web UI assembly). */ -const REAL_PLUGINS: { id: string; dir: string; inject?: string[]; immediately?: boolean }[] = [ - { id: '@deepseek-ai/dsh-client-connection', dir: 'connection', immediately: true }, - { id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true }, - { id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', immediately: true }, - { id: '@deepseek-ai/dsh-client-i18n', dir: 'i18n', immediately: true }, - { id: LAYOUT_ID, dir: 'ui-layout', inject: ['@deepseek-ai/dsh-client-runtime'] }, - { id: SIDEBAR_ID, dir: 'ui-sidebar', inject: [LAYOUT_ID] }, - { id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', inject: [LAYOUT_ID] }, - { id: '@deepseek-ai/dsh-client-ui-question', dir: 'ui-question', inject: ['@deepseek-ai/dsh-client-ui-conversation'] }, - { id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', inject: ['@deepseek-ai/dsh-client-ui-conversation'] }, -] - -const BUNDLE_PATHS = new Map(REAL_PLUGINS.map(p => [p.id, bundlePath(p.dir)])) - -const row = (id: string, extra?: Partial): WebBootEntry => - ({ id, url: `/plugins/${id}/client.js?rev=e2e`, rev: 'e2e', ...extra }) - -const graphRows: WebBootEntry[] = REAL_PLUGINS.map(p => row(p.id, { - ...(p.inject !== undefined ? { inject: p.inject } : {}), - ...(p.immediately === true ? { immediately: true } : {}), -})) - -/** Graph for the fail-loud half: the immediately tier, one live UI row, one missing row. */ -const FAIL_GRAPH: WebBootGraph = { - rev: 'e2e-fail', - entries: [...graphRows.filter(r => r.immediately === true), row(LAYOUT_ID), row('@probe/absent')], -} - -/** Graph for the success pass: the complete assembly. */ -const OK_GRAPH: WebBootGraph = { rev: 'e2e-ok', entries: graphRows } - -/** Registry stub over a fixed graph (the real HostWebPluginRegistry is webserver-side production code). */ -function fixedRegistry(graph: WebBootGraph, byId: ReadonlyMap) { - return { - graph: () => graph, - clientPath: (id: string) => byId.get(id), - onRebuilt: () => () => undefined, - } -} - -describe('web boot chain (keyless, real carrier)', () => { - let server: Awaited> - let browser: Browser - let page: Page - const pageErrors: string[] = [] - - beforeAll(async () => { - requireDist() - const port = await probeFreePort() - const apiHandler = { fetch: () => Promise.resolve(new Response('boot smoke must not call /api', { status: 500 })) } - server = await startWebServer({ - host: '127.0.0.1', - port, - distIndex: DIST_INDEX, - apiHandler, - webPlugins: fixedRegistry(FAIL_GRAPH, BUNDLE_PATHS), - }, (err) => { pageErrors.push(`server: ${String(err)}`) }) - browser = await chromium.launch() - page = await browser.newPage() - page.on('pageerror', e => pageErrors.push(String(e))) - await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'load' }) - }) - - afterAll(async () => { - await browser?.close() - await server?.close() - }) - - it('GET / injects the entry graph verbatim', async () => { - onTestFailed(() => saveFailureShot(page, 'smoke-boot-manifest')) - const boot = await page.evaluate(() => (window as { __DSH_BOOT__?: unknown }).__DSH_BOOT__) - expect(boot).toEqual(FAIL_GRAPH) - }) - - it('serves a real bundle through the plugins endpoint', async () => { - const res = await page.request.get(`${new URL(page.url()).origin}/plugins/${LAYOUT_ID}/client.js`) - expect(res.status()).toBe(200) - expect(await res.text()).toContain('window.__ModuleLoader__.load') - }) - - it('boots to the loading page and fail-louds the absent entry', async () => { - onTestFailed(() => saveFailureShot(page, 'smoke-boot-fail-loud')) - await page.waitForSelector('text=HARNESS', { timeout: 10_000 }) - await page.waitForSelector('text=Failed to load plugins', { timeout: 10_000 }) - await page.waitForSelector('text=@probe/absent', { timeout: 2000 }) - // The real UI must not have flipped in: the gate opens only on settled. - expect(await page.locator('[class*="frame"]').count()).toBe(0) - }) - - it('applies the token sheets before any plugin CSS', async () => { - const family = await page.evaluate(() => getComputedStyle(document.body).getPropertyValue('--dsw-font-family')) - expect(family.trim().length).toBeGreaterThan(0) - }) -}) - -describe('web boot chain success pass (keyless, nine real bundles, ?fixture)', () => { - let server: Awaited> - let browser: Browser - let page: Page - const pageErrors: string[] = [] - - beforeAll(async () => { - requireDist() - const missing = REAL_PLUGINS.filter(p => !existsSync(bundlePath(p.dir))) - if (missing.length > 0) throw new Error(`client bundles not built (pnpm --filter bundle): ${missing.map(m => m.dir).join(', ')}`) - const port = await probeFreePort() - // ?fixture never opens HTTP streams; /api is a tripwire like the first describe. - const apiHandler = { fetch: () => Promise.resolve(new Response('fixture mode must not call /api', { status: 500 })) } - server = await startWebServer({ - host: '127.0.0.1', - port, - distIndex: DIST_INDEX, - apiHandler, - webPlugins: fixedRegistry(OK_GRAPH, BUNDLE_PATHS), - }, (err) => { pageErrors.push(`server: ${String(err)}`) }) - browser = await chromium.launch() - page = await browser.newPage() - page.on('pageerror', e => pageErrors.push(String(e))) - await page.goto(`http://127.0.0.1:${port}/?fixture`, { waitUntil: 'load' }) - }) - - afterAll(async () => { - await browser?.close() - await server?.close() - }) - - it('settles and flips to the three-column frame in one pass', async () => { - onTestFailed(() => saveFailureShot(page, 'smoke-boot-settled')) - await page.waitForSelector('[class*="frame"]', { timeout: 15_000 }) - // Loading page is gone; the grid carries the three tracks. - expect(await page.locator('text=Failed to load plugins').count()).toBe(0) - const template = await page.locator('[class*="frame"]').evaluate(el => getComputedStyle(el).gridTemplateColumns) - expect(template.split(' ').length).toBe(3) - }) - - it('every plugin CSS landed with its ownership tag', async () => { - const owners = await page.evaluate(() => - [...document.querySelectorAll('style[data-plugin]')].map(s => (s as HTMLElement).dataset['plugin'])) - expect(owners).toContain(LAYOUT_ID) - expect(owners).toContain(SIDEBAR_ID) - }) - - it('collapsed sidebar animates to a 56px rail with the four controls', async () => { - onTestFailed(() => saveFailureShot(page, 'smoke-boot-collapsed-rail')) - const frame = page.locator('[class*="frame"]') - const firstTrack = async (): Promise => (await frame.evaluate( - el => getComputedStyle(el).gridTemplateColumns)).split(' ')[0]! - // The tracks transition on the deepsuite curve; assert the animated - // settle rather than an instant jump. - const settledTrack = async (px: string): Promise => { - await expect.poll(firstTrack, { timeout: 2000 }).toBe(px) - } - // The brand wordmark is decorative svg (aria-hidden) — presence tracks the wide chrome. - const brand = () => page.locator('[class*="brand"]').count() - await page.getByRole('button', { name: 'Collapse sidebar' }).click() - // Mid-collapse the wide chrome is still mounted, fading — not swapped out. - expect(await brand()).toBe(1) - await settledTrack('56px') - await expect.poll(brand, { timeout: 2000 }).toBe(0) - for (const name of ['Open sidebar', 'New session', 'New workspace', 'Search sessions', 'Settings']) { - await expect(page.getByRole('button', { name }).isVisible(), name).resolves.toBe(true) - } - await page.getByRole('button', { name: 'Open sidebar' }).click() - await settledTrack('280px') - await expect(page.getByRole('button', { name: 'Collapse sidebar' }).isVisible()).resolves.toBe(true) - // Rail search: collapse again, the search control expands and lands in the box. - await page.getByRole('button', { name: 'Collapse sidebar' }).click() - await settledTrack('56px') - await page.getByRole('button', { name: 'Search sessions' }).click() - await settledTrack('280px') - // Focus is deferred past the slide (EXPAND_SLIDE_MS) — poll for it. - await expect.poll(() => page.evaluate(() => - (document.activeElement as HTMLInputElement | null)?.placeholder ?? ''), { timeout: 2000 }).toContain('Search') - }) - - it('renders file tool rows and expands fixture reasoning from either click target', async () => { - onTestFailed(() => saveFailureShot(page, 'smoke-think-disclosure')) - await page.locator('[role="treeitem"]').first().click() - await page.locator('[role="treeitem"][aria-selected]').first().click() - - const thinkRoot = page.locator('[data-variant="think"]').first() - const think = thinkRoot.getByRole('button') - await think.waitFor({ state: 'visible', timeout: 10_000 }) - expect(await think.getAttribute('aria-expanded')).toBe('false') - - await thinkRoot.getByText(/^思考过程 .*reasoning 内容。$/).click() - expect(await think.getAttribute('aria-expanded')).toBe('true') - expect(await thinkRoot.locator(':scope > div').count()).toBe(2) - - await think.getByText('Think', { exact: true }).click() - expect(await think.getAttribute('aria-expanded')).toBe('false') - - const editRoot = page.locator('[data-variant="edit"]').first() - await editRoot.waitFor({ state: 'visible', timeout: 10_000 }) - expect(await editRoot.getByText('Edit', { exact: true }).count()).toBe(1) - expect(await editRoot.getByText('notes/demo.txt', { exact: true }).count()).toBe(1) - - const writeRoot = page.locator('[data-variant="write"]').first() - await writeRoot.waitFor({ state: 'visible', timeout: 10_000 }) - expect(await writeRoot.getByText('Write', { exact: true }).count()).toBe(1) - expect(await writeRoot.getByText('notes/new-demo.txt', { exact: true }).count()).toBe(1) - }) - - it('keeps Markdown semantic while a fixture reply streams and finalizes', async () => { - onTestFailed(() => saveFailureShot(page, 'smoke-markdown-stream')) - await page.getByRole('button', { name: 'New session', exact: true }).click() - const input = page.locator('textarea[placeholder]') - await input.waitFor({ timeout: 15_000 }) - await input.fill('render markdown') - await page.getByRole('button', { name: '发送' }).click() - - const streaming = page.locator('[data-streaming="true"]') - await streaming.getByRole('heading', { name: 'Markdown fixture' }).waitFor({ timeout: 15_000 }) - await streaming.waitFor({ state: 'detached', timeout: 15_000 }) - - const finalHeading = page.getByRole('heading', { name: 'Markdown fixture' }) - expect(await finalHeading.evaluate(element => element.tagName)).toBe('H1') - expect(await page.locator('pre code').filter({ hasText: 'const markdown = true' }).count()).toBe(1) - const external = page.getByRole('link', { name: 'DeepSeek' }) - expect(await external.getAttribute('target')).toBe('_blank') - expect(await external.getAttribute('rel')).toBe('noopener noreferrer') - }) - - it('renders and completes the resident question through the composer slot', async () => { - onTestFailed(() => saveFailureShot(page, 'smoke-question-composer')) - const sessionTree = page.getByRole('tree', { name: 'Sessions' }) - const projectRow = sessionTree.getByRole('treeitem').filter({ hasText: '3 sessions' }) - if (await projectRow.getAttribute('aria-expanded') === 'false') await projectRow.click() - await sessionTree.getByText('Fixture 历史会话', { exact: true }).click() - const composer = page.locator('[data-question-key]') - await composer.waitFor({ timeout: 15_000 }) - expect({ - question: await composer.getByRole('heading').innerText(), - progress: await composer.getByText('1 / 3', { exact: true }).innerText(), - options: await composer.getByRole('radio').allTextContents(), - custom: await composer.getByRole('button', { name: '其他,请填写自定义答案' }).innerText(), - }).toMatchInlineSnapshot(` - { - "custom": "其他,请填写自定义答案", - "options": [ - "1工程落地型推荐更看重能直接做 runtime、tool executor、sandbox、trace 和线上问题排查。", - "2研究潜力型更看重 Agent 理解、训练评测思路和长期成长空间。", - "3均衡型同时要求工程能力和 Agent 认知,但可能筛选门槛更高。", - ], - "progress": "1 / 3", - "question": "你现在更想招哪类 Agent/Harness 候选人?", - } - `) - - await composer.getByRole('radio', { name: '工程落地型' }).click() - await composer.getByText('2 / 3', { exact: true }).waitFor() - await composer.getByRole('button', { name: '跳过本题', exact: true }).click() - await composer.getByRole('checkbox', { name: '系统设计' }).click() - await composer.getByRole('checkbox', { name: 'Agent 产品判断' }).click() - await composer.getByRole('checkbox', { name: 'Agent 产品判断' }).press('Enter') - - await composer.waitFor({ state: 'detached' }) - const restoredInput = page.locator('textarea[placeholder]') - await restoredInput.waitFor() - expect(await restoredInput.getAttribute('placeholder')).toBe('回复生成中,可停止后再输入') - }) - - it('stayed clean: no page errors across the whole load chain', () => { - expect(pageErrors).toEqual([]) - }) -}) diff --git a/apps/web/tests/support.ts b/apps/web/tests/support.ts index f4fbbb265f..ce0a6db799 100644 --- a/apps/web/tests/support.ts +++ b/apps/web/tests/support.ts @@ -16,10 +16,7 @@ export function requireDist(): void { } } -/** - * OS-assigned free port, released before use. startWebServer echoes - * options.port instead of the bound one, so passing 0 directly is unusable. - */ +/** OS-assigned free port, released before use (the spawned `dsh web` needs a concrete --port). */ export function probeFreePort(): Promise { return new Promise((resolvePort, reject) => { const probe = createServer() diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 998996304e..d0af0d4641 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -21,9 +21,6 @@ { "path": "../../packages/client/web" }, - { - "path": "../../packages/host/webserver" - }, { "path": "../../packages/client/modules" } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 5a805cbeb3..7043de4911 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ { find: /^@deepseek-ai\/dsh-client-web-react$/, replacement: src('../../packages/client/web-react/src/index.ts') }, { find: /^@deepseek-ai\/dsh-client-ui-slots$/, replacement: src('../../packages/client/ui-slots/src/index.ts') }, { find: /^@deepseek-ai\/dsh-client-ui-primitives$/, replacement: src('../../packages/client/ui-primitives/src/index.ts') }, - { find: /^@deepseek-ai\/dsh-client-modules$/, replacement: src('../../packages/client/modules/src/index.ts') }, + { find: /^@deepseek-ai\/dsh-client-modules\/client$/, replacement: src('../../packages/client/modules/src/client/index.ts') }, ], }, define: { diff --git a/packages/client/web/README.md b/packages/client/web/README.md index 4b26238cf6..6cea165608 100644 --- a/packages/client/web/README.md +++ b/packages/client/web/README.md @@ -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. diff --git a/packages/client/web/src/boot.tsx b/packages/client/web/src/boot.tsx index 6c04f1619b..bd755a5fc1 100644 --- a/packages/client/web/src/boot.tsx +++ b/packages/client/web/src/boot.tsx @@ -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