client-loader-bundle.e2e.ts 3.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. /**
  2. * Real-bundle smoke: the actual tsdown client bundle of ui-layout runs
  3. * through the loader chain (execute → handoff → factory(require) → apply →
  4. * export re-registration). Skips when the bundle is not built (lib/client.js is a
  5. * build product; `pnpm --filter @deepseek-ai/dsh-client-ui-layout build`).
  6. */
  7. import { readFileSync } from 'node:fs'
  8. import { createRequire } from 'node:module'
  9. import { Context } from 'cordis'
  10. import { afterEach, describe, expect, it } from 'vitest'
  11. import * as uiSlots from '@deepseek-ai/dsh-client-ui-slots'
  12. import * as webReact from '@deepseek-ai/dsh-client-web-react'
  13. import { createClientLoader } from '../src/client/loader/index.ts'
  14. import type { ClientPluginHandoff } from '../src/client/loader/index.ts'
  15. import { SessionsService } from '../src/client/sessions/service.ts'
  16. import { SlotsService } from '../src/client/slots.ts'
  17. import { FakeApiClient } from './fake-api.ts'
  18. const LAYOUT_ID = '@deepseek-ai/dsh-client-ui-layout'
  19. type Win = { DSHClientProxy?: { loadPlugin(h: ClientPluginHandoff): void }; window?: unknown }
  20. afterEach(() => {
  21. delete (globalThis as Win).DSHClientProxy
  22. delete (globalThis as Win).window
  23. })
  24. function readLayoutBundle(): string | undefined {
  25. try {
  26. const require = createRequire(import.meta.url)
  27. return readFileSync(require.resolve(`${LAYOUT_ID}/client`), 'utf8')
  28. } catch {
  29. return undefined
  30. }
  31. }
  32. describe('real tsdown bundle through the loader', () => {
  33. const code = readLayoutBundle()
  34. it.skipIf(code === undefined)('loads ui-layout lib/client.js: handoff, DI require, apply, export surface', async () => {
  35. // The bundle banner addresses window.DSHClientProxy; node has no window —
  36. // alias it to globalThis so the loader-installed proxy is reachable.
  37. ;(globalThis as Win).window = globalThis
  38. const ctx = new Context()
  39. // The layout apply consumes the slots + sessions services; the real chain
  40. // loads the runtime bundle first — stand both up directly here.
  41. ctx.plugin(SlotsService)
  42. await ctx.fiber.await()
  43. new SessionsService(ctx, new FakeApiClient())
  44. const loader = createClientLoader({
  45. ctx,
  46. // The real bundle externals resolved from the seeded table. React is a
  47. // type-only import in the layout bundle today, but jsx-runtime is real.
  48. modules: {
  49. 'react': await import('react'),
  50. 'react/jsx-runtime': await import('react/jsx-runtime'),
  51. '@deepseek-ai/dsh-client-ui-slots': uiSlots,
  52. '@deepseek-ai/dsh-client-web-react': webReact,
  53. },
  54. boot: { plugins: [{ id: LAYOUT_ID, url: `/plugins/${LAYOUT_ID}/client.js`, inject: [] }] },
  55. fetchBundle: () => Promise.resolve(code as string),
  56. // node has no DOM: evaluate the bundle body directly (same synchronous
  57. // handoff contract as the <script> path).
  58. executeBundle: (bundleCode) => {
  59. // Node has no <script>: Function-evaluating the built bundle IS the
  60. // system under test (same synchronous handoff as the browser path).
  61. // eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call
  62. new Function(bundleCode)()
  63. },
  64. })
  65. loader.start()
  66. await loader.settled()
  67. expect(loader.status.getSnapshot()[LAYOUT_ID]).toBe('active')
  68. const surface = loader.requireModule(LAYOUT_ID) as Record<string, unknown>
  69. expect(typeof surface.apply).toBe('function')
  70. })
  71. })