@noego/wood 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/README.md +7 -0
  2. package/dist/browser-test-setup.cjs +29 -0
  3. package/dist/browser-test-setup.cjs.map +1 -0
  4. package/dist/browser-test-setup.d.cts +8 -0
  5. package/dist/browser-test-setup.d.ts +8 -0
  6. package/dist/browser-test-setup.js +9 -0
  7. package/dist/browser-test-setup.js.map +1 -0
  8. package/dist/client/global_polyfill.d.cts +1 -0
  9. package/dist/client/global_polyfill.d.ts +1 -0
  10. package/dist/client/index.cjs +7 -0
  11. package/dist/client/index.cjs.map +1 -1
  12. package/dist/client/index.d.cts +7 -4
  13. package/dist/client/index.d.ts +7 -4
  14. package/dist/client/index.js +6 -1
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/codegen/renderer_app_generator.cjs +97 -24
  17. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  18. package/dist/codegen/renderer_app_generator.js +97 -24
  19. package/dist/codegen/renderer_app_generator.js.map +1 -1
  20. package/dist/codegen/type_generator.cjs +9 -0
  21. package/dist/codegen/type_generator.cjs.map +1 -1
  22. package/dist/codegen/type_generator.js +9 -0
  23. package/dist/codegen/type_generator.js.map +1 -1
  24. package/dist/config/config_parser.cjs +10 -0
  25. package/dist/config/config_parser.cjs.map +1 -1
  26. package/dist/config/config_parser.js +10 -0
  27. package/dist/config/config_parser.js.map +1 -1
  28. package/dist/fetch.cjs +38 -0
  29. package/dist/fetch.cjs.map +1 -0
  30. package/dist/fetch.d.cts +7 -0
  31. package/dist/fetch.d.ts +7 -0
  32. package/dist/fetch.js +12 -0
  33. package/dist/fetch.js.map +1 -0
  34. package/dist/frontend/frontend_page.cjs +3 -3
  35. package/dist/frontend/frontend_page.cjs.map +1 -1
  36. package/dist/frontend/frontend_page.d.cts +1 -1
  37. package/dist/frontend/frontend_page.d.ts +1 -1
  38. package/dist/frontend/frontend_page.js +3 -3
  39. package/dist/frontend/frontend_page.js.map +1 -1
  40. package/dist/frontend/frontend_surface.cjs +31 -17
  41. package/dist/frontend/frontend_surface.cjs.map +1 -1
  42. package/dist/frontend/frontend_surface.d.cts +12 -2
  43. package/dist/frontend/frontend_surface.d.ts +12 -2
  44. package/dist/frontend/frontend_surface.js +37 -17
  45. package/dist/frontend/frontend_surface.js.map +1 -1
  46. package/dist/frontend/index.d.cts +2 -0
  47. package/dist/frontend/index.d.ts +2 -0
  48. package/dist/index.cjs +5 -0
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.d.cts +25 -0
  51. package/dist/index.d.ts +25 -0
  52. package/dist/index.js +3 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/loader/index.cjs +19 -2
  55. package/dist/loader/index.cjs.map +1 -1
  56. package/dist/loader/index.d.cts +1 -0
  57. package/dist/loader/index.d.ts +1 -0
  58. package/dist/loader/index.js +19 -1
  59. package/dist/loader/index.js.map +1 -1
  60. package/dist/loader/loader_orchestration.cjs +82 -0
  61. package/dist/loader/loader_orchestration.cjs.map +1 -0
  62. package/dist/loader/loader_orchestration.d.cts +66 -0
  63. package/dist/loader/loader_orchestration.d.ts +66 -0
  64. package/dist/loader/loader_orchestration.js +51 -0
  65. package/dist/loader/loader_orchestration.js.map +1 -0
  66. package/dist/loader/loader_runner.cjs +46 -41
  67. package/dist/loader/loader_runner.cjs.map +1 -1
  68. package/dist/loader/loader_runner.d.cts +12 -6
  69. package/dist/loader/loader_runner.d.ts +12 -6
  70. package/dist/loader/loader_runner.js +50 -41
  71. package/dist/loader/loader_runner.js.map +1 -1
  72. package/dist/native_browser.types-BY2daUOS.d.ts +189 -0
  73. package/dist/native_browser.types-vWXtE4rM.d.cts +189 -0
  74. package/dist/navigation/index.cjs +15 -2
  75. package/dist/navigation/index.cjs.map +1 -1
  76. package/dist/navigation/index.d.cts +2 -0
  77. package/dist/navigation/index.d.ts +2 -0
  78. package/dist/navigation/index.js +8 -1
  79. package/dist/navigation/index.js.map +1 -1
  80. package/dist/runtime/get_container.cjs +2 -0
  81. package/dist/runtime/get_container.cjs.map +1 -1
  82. package/dist/runtime/get_container.js +2 -0
  83. package/dist/runtime/get_container.js.map +1 -1
  84. package/dist/runtime/index.cjs +5 -0
  85. package/dist/runtime/index.cjs.map +1 -1
  86. package/dist/runtime/index.d.cts +2 -0
  87. package/dist/runtime/index.d.ts +2 -0
  88. package/dist/runtime/index.js +3 -0
  89. package/dist/runtime/index.js.map +1 -1
  90. package/dist/runtime/runtime_capabilities.cjs +35 -0
  91. package/dist/runtime/runtime_capabilities.cjs.map +1 -0
  92. package/dist/runtime/runtime_capabilities.d.cts +6 -0
  93. package/dist/runtime/runtime_capabilities.d.ts +6 -0
  94. package/dist/runtime/runtime_capabilities.js +11 -0
  95. package/dist/runtime/runtime_capabilities.js.map +1 -0
  96. package/dist/runtime/runtime_module.cjs +2 -0
  97. package/dist/runtime/runtime_module.cjs.map +1 -1
  98. package/dist/runtime/runtime_module.js +2 -0
  99. package/dist/runtime/runtime_module.js.map +1 -1
  100. package/dist/test-wood.cjs +150 -0
  101. package/dist/test-wood.cjs.map +1 -0
  102. package/dist/test-wood.d.cts +62 -0
  103. package/dist/test-wood.d.ts +62 -0
  104. package/dist/test-wood.js +115 -0
  105. package/dist/test-wood.js.map +1 -0
  106. package/dist/testing/browser.cjs +92 -163
  107. package/dist/testing/browser.cjs.map +1 -1
  108. package/dist/testing/browser.d.cts +14 -13
  109. package/dist/testing/browser.d.ts +14 -13
  110. package/dist/testing/browser.js +88 -162
  111. package/dist/testing/browser.js.map +1 -1
  112. package/dist/testing/create_controller_harness.d.cts +2 -1
  113. package/dist/testing/create_controller_harness.d.ts +2 -1
  114. package/dist/testing/create_router_bridge.cjs +19 -15
  115. package/dist/testing/create_router_bridge.cjs.map +1 -1
  116. package/dist/testing/create_router_bridge.d.cts +8 -1
  117. package/dist/testing/create_router_bridge.d.ts +8 -1
  118. package/dist/testing/create_router_bridge.js +18 -15
  119. package/dist/testing/create_router_bridge.js.map +1 -1
  120. package/dist/testing/index.cjs +12 -4
  121. package/dist/testing/index.cjs.map +1 -1
  122. package/dist/testing/index.d.cts +11 -5
  123. package/dist/testing/index.d.ts +11 -5
  124. package/dist/testing/index.js +9 -3
  125. package/dist/testing/index.js.map +1 -1
  126. package/dist/testing/native_browser/browser_pool.cjs +31 -0
  127. package/dist/testing/native_browser/browser_pool.cjs.map +1 -0
  128. package/dist/testing/native_browser/browser_pool.d.cts +1 -0
  129. package/dist/testing/native_browser/browser_pool.d.ts +1 -0
  130. package/dist/testing/native_browser/browser_pool.js +6 -0
  131. package/dist/testing/native_browser/browser_pool.js.map +1 -0
  132. package/dist/testing/native_browser/happy_dom_tab.cjs +107 -0
  133. package/dist/testing/native_browser/happy_dom_tab.cjs.map +1 -0
  134. package/dist/testing/native_browser/happy_dom_tab.d.cts +35 -0
  135. package/dist/testing/native_browser/happy_dom_tab.d.ts +35 -0
  136. package/dist/testing/native_browser/happy_dom_tab.js +73 -0
  137. package/dist/testing/native_browser/happy_dom_tab.js.map +1 -0
  138. package/dist/testing/native_browser/happy_dom_transfer.cjs +29 -0
  139. package/dist/testing/native_browser/happy_dom_transfer.cjs.map +1 -0
  140. package/dist/testing/native_browser/happy_dom_transfer.d.cts +1 -0
  141. package/dist/testing/native_browser/happy_dom_transfer.d.ts +1 -0
  142. package/dist/testing/native_browser/happy_dom_transfer.js +5 -0
  143. package/dist/testing/native_browser/happy_dom_transfer.js.map +1 -0
  144. package/dist/testing/native_browser/host_transport.cjs +89 -0
  145. package/dist/testing/native_browser/host_transport.cjs.map +1 -0
  146. package/dist/testing/native_browser/host_transport.d.cts +44 -0
  147. package/dist/testing/native_browser/host_transport.d.ts +44 -0
  148. package/dist/testing/native_browser/host_transport.js +65 -0
  149. package/dist/testing/native_browser/host_transport.js.map +1 -0
  150. package/dist/testing/native_browser/native_browser.types.cjs +17 -0
  151. package/dist/testing/native_browser/native_browser.types.cjs.map +1 -0
  152. package/dist/testing/native_browser/native_browser.types.d.cts +31 -0
  153. package/dist/testing/native_browser/native_browser.types.d.ts +31 -0
  154. package/dist/testing/native_browser/native_browser.types.js +1 -0
  155. package/dist/testing/native_browser/native_browser.types.js.map +1 -0
  156. package/dist/testing/native_browser/native_browser_environment.cjs +143 -0
  157. package/dist/testing/native_browser/native_browser_environment.cjs.map +1 -0
  158. package/dist/testing/native_browser/native_browser_environment.d.cts +36 -0
  159. package/dist/testing/native_browser/native_browser_environment.d.ts +36 -0
  160. package/dist/testing/native_browser/native_browser_environment.js +109 -0
  161. package/dist/testing/native_browser/native_browser_environment.js.map +1 -0
  162. package/dist/testing/native_browser/native_browser_lifecycle.cjs +31 -0
  163. package/dist/testing/native_browser/native_browser_lifecycle.cjs.map +1 -0
  164. package/dist/testing/native_browser/native_browser_lifecycle.d.cts +1 -0
  165. package/dist/testing/native_browser/native_browser_lifecycle.d.ts +1 -0
  166. package/dist/testing/native_browser/native_browser_lifecycle.js +6 -0
  167. package/dist/testing/native_browser/native_browser_lifecycle.js.map +1 -0
  168. package/dist/testing/native_browser/playwright_tab.cjs +124 -0
  169. package/dist/testing/native_browser/playwright_tab.cjs.map +1 -0
  170. package/dist/testing/native_browser/playwright_tab.d.cts +35 -0
  171. package/dist/testing/native_browser/playwright_tab.d.ts +35 -0
  172. package/dist/testing/native_browser/playwright_tab.js +100 -0
  173. package/dist/testing/native_browser/playwright_tab.js.map +1 -0
  174. package/dist/testing/native_browser/renderer_compilation.cjs +149 -0
  175. package/dist/testing/native_browser/renderer_compilation.cjs.map +1 -0
  176. package/dist/testing/native_browser/renderer_compilation.d.cts +35 -0
  177. package/dist/testing/native_browser/renderer_compilation.d.ts +35 -0
  178. package/dist/testing/native_browser/renderer_compilation.js +115 -0
  179. package/dist/testing/native_browser/renderer_compilation.js.map +1 -0
  180. package/dist/testing/native_browser/renderer_entry.cjs +148 -0
  181. package/dist/testing/native_browser/renderer_entry.cjs.map +1 -0
  182. package/dist/testing/native_browser/renderer_entry.d.cts +13 -0
  183. package/dist/testing/native_browser/renderer_entry.d.ts +13 -0
  184. package/dist/testing/native_browser/renderer_entry.js +114 -0
  185. package/dist/testing/native_browser/renderer_entry.js.map +1 -0
  186. package/dist/testing/registration_only_root.cjs +61 -5
  187. package/dist/testing/registration_only_root.cjs.map +1 -1
  188. package/dist/testing/registration_only_root.d.cts +25 -4
  189. package/dist/testing/registration_only_root.d.ts +25 -4
  190. package/dist/testing/registration_only_root.js +60 -5
  191. package/dist/testing/registration_only_root.js.map +1 -1
  192. package/dist/testing/runner_importer.cjs +75 -0
  193. package/dist/testing/runner_importer.cjs.map +1 -0
  194. package/dist/testing/runner_importer.d.cts +8 -0
  195. package/dist/testing/runner_importer.d.ts +8 -0
  196. package/dist/testing/runner_importer.js +41 -0
  197. package/dist/testing/runner_importer.js.map +1 -0
  198. package/dist/testing/test_wood_config.cjs +407 -273
  199. package/dist/testing/test_wood_config.cjs.map +1 -1
  200. package/dist/testing/test_wood_config.d.cts +18 -123
  201. package/dist/testing/test_wood_config.d.ts +18 -123
  202. package/dist/testing/test_wood_config.js +416 -271
  203. package/dist/testing/test_wood_config.js.map +1 -1
  204. package/dist/testing/wood_config_source.cjs +12 -0
  205. package/dist/testing/wood_config_source.cjs.map +1 -1
  206. package/dist/testing/wood_config_source.d.cts +10 -0
  207. package/dist/testing/wood_config_source.d.ts +10 -0
  208. package/dist/testing/wood_config_source.js +12 -0
  209. package/dist/testing/wood_config_source.js.map +1 -1
  210. package/dist/testing/wood_env_renderer.cjs +31 -4
  211. package/dist/testing/wood_env_renderer.cjs.map +1 -1
  212. package/dist/testing/wood_env_renderer.d.cts +14 -1
  213. package/dist/testing/wood_env_renderer.d.ts +14 -1
  214. package/dist/testing/wood_env_renderer.js +31 -4
  215. package/dist/testing/wood_env_renderer.js.map +1 -1
  216. package/dist/testing/wood_lazy_runtime.cjs +109 -0
  217. package/dist/testing/wood_lazy_runtime.cjs.map +1 -0
  218. package/dist/testing/wood_lazy_runtime.d.cts +36 -0
  219. package/dist/testing/wood_lazy_runtime.d.ts +36 -0
  220. package/dist/testing/wood_lazy_runtime.js +84 -0
  221. package/dist/testing/wood_lazy_runtime.js.map +1 -0
  222. package/dist/testing/wood_test_selection.cjs +34 -55
  223. package/dist/testing/wood_test_selection.cjs.map +1 -1
  224. package/dist/testing/wood_test_selection.d.cts +18 -24
  225. package/dist/testing/wood_test_selection.d.ts +18 -24
  226. package/dist/testing/wood_test_selection.js +30 -54
  227. package/dist/testing/wood_test_selection.js.map +1 -1
  228. package/dist/testing-api.cjs +17 -0
  229. package/dist/testing-api.cjs.map +1 -0
  230. package/dist/testing-api.d.cts +18 -0
  231. package/dist/testing-api.d.ts +18 -0
  232. package/dist/testing-api.js +1 -0
  233. package/dist/testing-api.js.map +1 -0
  234. package/dist/types/config.cjs.map +1 -1
  235. package/dist/types/config.d.cts +4 -0
  236. package/dist/types/config.d.ts +4 -0
  237. package/docs/native-browser-testing.md +164 -0
  238. package/package.json +31 -5
  239. package/src/components/NavigationShell.browser.test.ts +10 -10
  240. package/src/components/NavigationShell.cleanup.browser.test.ts +57 -0
  241. package/src/components/NavigationShell.scopes.browser.test.ts +5 -5
  242. package/src/components/NavigationShell.svelte +50 -24
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/testing/wood_env_renderer.ts"],"sourcesContent":["/**\n * The renderer tier of a `testWood(configPath)` environment: a fresh renderer\n * root (mirroring the production process split), an environment-local\n * Navigation, the generated bridge client over an in-process transport into\n * the main router, and the frontend application surface over the selected\n * page catalog. With a DOM mode, the page's real component is mounted by the\n * app's host module and driven through a selector-based surface.\n */\nimport type { IContainer } from '@noego/ioc';\nimport { ExecutionContext, LoadAs } from '@noego/ioc';\nimport type { TestIocBuilder } from '@noego/testing';\nimport type { WoodRouter } from '../router/wood_router.cjs';\nimport { InProcessRouterTransport } from '../transport/in_process_router_transport.js';\nimport { buildTransportBridge } from '../transport/build_transport_bridge.js';\nimport { RouterBridgeEvents } from './create_router_bridge.js';\nimport type { OperationDef, ViewDef } from '../types/index.cjs';\nimport { Navigation } from '../navigation/index.js';\nimport type { PageCatalog } from '../frontend/page_catalog.js';\nimport { WorkRegistry } from '../frontend/work_registry.js';\nimport { TrackedTransport } from '../frontend/tracked_transport.js';\nimport {\n createFrontendSurface,\n type FrontendApplicationSurface,\n type PageScopeContext,\n} from '../frontend/frontend_surface.js';\nimport type { FrontendPage } from '../frontend/frontend_page.js';\nimport { TestWoodError, WOOD_FRONTEND_BRIDGE } from './test_wood.cjs';\nimport { importDefaultExport, type ModuleImporter, type WoodConfigSource } from './wood_config_source.cjs';\nimport type { WoodDomMode } from './wood_test_selection.cjs';\nimport { leaseDomAnimation, type WoodAnimationLease } from './wood_dom_animation.cjs';\nimport {\n buildRecordedEnvironment,\n cleanupAll,\n RegistrationOnlyRoot,\n TestWoodCleanupError,\n} from './registration_only_root.cjs';\n\n/** What the app's `main.testing.compose` module may hand back for the renderer side. */\nexport interface WoodRendererHooks {\n /** Backend push emitter feeding renderer subscriptions (default: transport-owned). */\n events?: RouterBridgeEvents;\n /** Register app-owned renderer composition on the fresh renderer root. */\n configureRendererRoot?(root: IContainer): void | Promise<void>;\n /** Register per-scope renderer builtins on a new page/layout scope. */\n configurePageScope?(scope: IContainer, view: ViewDef, context: PageScopeContext): void | Promise<void>;\n}\n\n/** A page's real component mounted under a DOM driver (`renderer.testing.host`). */\nexport interface WoodMountedPage {\n readonly target: Element;\n flush?(): void | Promise<void>;\n unmount(): void;\n}\n\nexport type WoodPageHost = (identity: string, page: FrontendPage, target: Element) => WoodMountedPage;\n\nexport interface RendererTierOptions {\n source: WoodConfigSource;\n router: WoodRouter;\n catalog: PageCatalog;\n operations: Map<string, OperationDef>;\n importer: ModuleImporter;\n hooks: WoodRendererHooks | null;\n dom: WoodDomMode | null;\n ioc: TestIocBuilder;\n registrations: RegistrationOnlyRoot;\n controllerClasses: ReadonlyMap<string, ControllerClass>;\n}\n\nexport interface WoodDomDriver {\n readonly target: Element;\n click(selector: string): Promise<void>;\n type(selector: string, text: string): Promise<void>;\n submit(selector: string): Promise<void>;\n text(selector: string): Promise<string>;\n exists(selector: string): Promise<boolean>;\n}\n\nexport interface RendererTier {\n readonly frontend: FrontendApplicationSurface;\n readonly events: RouterBridgeEvents;\n readonly rendererRoot: IContainer;\n open(page: string, params?: Record<string, string>, query?: Record<string, string>): Promise<FrontendPage>;\n navigate(page: string, params?: Record<string, string>, query?: Record<string, string>): Promise<FrontendPage>;\n current(): FrontendPage | null;\n settle(): Promise<void>;\n pendingWork(): ReturnType<WorkRegistry['pending']>;\n verify(): Promise<void>;\n dom: WoodDomDriver | null;\n dispose(): Promise<void>;\n}\ntype ControllerClass = new (...args: never[]) => unknown;\n\nfunction controllerLoader(\n source: WoodConfigSource,\n importer: ModuleImporter,\n preloaded: ReadonlyMap<string, ControllerClass>,\n) {\n const cache = new Map<string, Promise<ControllerClass>>();\n for (const [identity, controller] of preloaded) cache.set(identity, Promise.resolve(controller));\n return (identity: string): Promise<ControllerClass> => {\n let pending = cache.get(identity);\n if (!pending) {\n pending = importDefaultExport<ControllerClass>(\n importer,\n source.rendererControllerFile(identity),\n `renderer controller \"${identity}\"`,\n );\n cache.set(identity, pending);\n }\n return pending;\n };\n}\n\nasync function loadPageHost(\n source: WoodConfigSource,\n importer: ModuleImporter,\n dom: WoodDomMode,\n): Promise<WoodPageHost> {\n if (dom === 'chromium') {\n throw new TestWoodError(\"select({ dom: 'chromium' }) is not available yet; use 'happy-dom'\");\n }\n if (typeof document === 'undefined') {\n throw new TestWoodError(\n \"select({ dom: 'happy-dom' }) requires a DOM environment (// @vitest-environment happy-dom)\",\n );\n }\n const hostPath = source.hookModule(source.config.renderer.testing?.host);\n if (!hostPath) {\n throw new TestWoodError(\n 'select({ dom }) requires renderer.testing.host in wood.config.yml (a module whose default export mounts a page)',\n );\n }\n return importDefaultExport<WoodPageHost>(importer, hostPath, 'renderer.testing.host');\n}\n\n/**\n * Keeps the mounted component in step with the surface's current page. While\n * a page is mounted it holds a Web Animations lease on the target's realm so\n * Svelte transitions work under happy-dom (see `wood_dom_animation.ts`); the\n * lease is released on unmount, on a failed host mount, and on dispose.\n */\nexport class MountKeeper {\n private mounted: WoodMountedPage | null = null;\n private mountedFor: FrontendPage | null = null;\n private animation: WoodAnimationLease | null = null;\n\n constructor(private readonly host: WoodPageHost) {}\n\n get target(): Element {\n if (!this.mounted) throw new TestWoodError('no page is mounted; open(page) first');\n return this.mounted.target;\n }\n\n /** The Web Animations lease held for the current mount (null when nothing is mounted). */\n get animationLease(): WoodAnimationLease | null {\n return this.animation;\n }\n\n async flush(): Promise<void> {\n await this.mounted?.flush?.();\n }\n\n sync(current: FrontendPage | null): void {\n if (current === this.mountedFor) return;\n this.unmount();\n if (!current) return;\n const target = document.createElement('div');\n document.body.appendChild(target);\n const lease = leaseDomAnimation(target);\n try {\n this.mounted = this.host(current.identity, current, target);\n } catch (error) {\n lease.release();\n target.remove();\n throw error;\n }\n this.animation = lease;\n this.mountedFor = current;\n }\n\n unmount(): void {\n const mounted = this.mounted;\n const lease = this.animation;\n this.mounted = null;\n this.mountedFor = null;\n this.animation = null;\n try {\n mounted?.unmount();\n } finally {\n mounted?.target.remove();\n lease?.release();\n }\n }\n}\n\nfunction domDriver(keeper: MountKeeper): WoodDomDriver {\n const find = (selector: string): Element => {\n const found = keeper.target.querySelector(selector);\n if (!found) throw new TestWoodError(`no element matches \"${selector}\"`);\n return found;\n };\n return {\n get target() {\n return keeper.target;\n },\n async click(selector) {\n (find(selector) as HTMLElement).click();\n await keeper.flush();\n },\n async type(selector, text) {\n const input = find(selector) as HTMLInputElement;\n input.focus();\n input.value = text;\n input.dispatchEvent(new Event('input', { bubbles: true }));\n await keeper.flush();\n },\n async submit(selector) {\n find(selector).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));\n await keeper.flush();\n },\n async text(selector) {\n return keeper.target.querySelector(selector)?.textContent ?? '';\n },\n async exists(selector) {\n return keeper.target.querySelector(selector) !== null;\n },\n };\n}\n\nexport async function buildRendererTier(options: RendererTierOptions): Promise<RendererTier> {\n const {\n source,\n router,\n catalog,\n operations,\n importer,\n hooks,\n dom,\n ioc,\n registrations,\n controllerClasses,\n } = options;\n const keeper = dom ? new MountKeeper(await loadPageHost(source, importer, dom)) : null;\n\n const registry = new WorkRegistry();\n const defaultNavigation = new Navigation();\n const innerTransport = new InProcessRouterTransport(router, hooks?.events ?? new RouterBridgeEvents());\n let currentOwner: () => string = () => 'application';\n const transport = new TrackedTransport(innerTransport, registry, () => currentOwner());\n const { bridge } = buildTransportBridge(operations, transport);\n\n registrations.root.registerFunction(Navigation, () => defaultNavigation, { loadAs: LoadAs.Singleton });\n registrations.root.registerFunction(WOOD_FRONTEND_BRIDGE, () => bridge, { loadAs: LoadAs.Singleton });\n\n const rendererEnv = await buildRecordedEnvironment(ioc, registrations, 'renderer');\n const rendererRoot = rendererEnv.root;\n let navigation: Navigation;\n const loadController = controllerLoader(source, importer, controllerClasses);\n let frontend: FrontendApplicationSurface;\n try {\n navigation = await ExecutionContext.run(rendererRoot as never, () => rendererRoot.get(Navigation)) as Navigation;\n frontend = createFrontendSurface({\n catalog,\n rendererRoot: rendererRoot as never,\n registry,\n navigation,\n resolveController: (identity) => loadController(identity),\n ...(hooks?.configurePageScope\n ? { configurePageScope: (scope, view, context) => hooks.configurePageScope!(scope as IContainer, view, context) }\n : {}),\n });\n } catch (error) {\n try {\n await cleanupAll('renderer build rollback failed', [\n ['renderer mount', () => keeper?.unmount()],\n ['renderer root', () => ExecutionContext.run(rendererRoot as never, () => rendererEnv.dispose()) as Promise<void>],\n ]);\n } catch (cleanupError) {\n const failures = cleanupError instanceof TestWoodCleanupError\n ? cleanupError.failures\n : [{ owner: 'renderer rollback', error: cleanupError }];\n throw new TestWoodCleanupError('renderer build failed and rollback failed', failures, error);\n }\n throw error;\n }\n\n currentOwner = () => {\n const current = frontend.current();\n return current ? `page ${current.identity}` : 'application';\n };\n\n const syncMount = async (): Promise<void> => {\n if (!keeper) return;\n keeper.sync(frontend.current());\n await keeper.flush();\n };\n const settle = async (): Promise<void> => {\n await frontend.settle();\n if (!keeper) return;\n await syncMount();\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\n await keeper.flush();\n };\n\n let closing: Promise<void> | null = null;\n const dispose = (): Promise<void> => closing ??= cleanupAll('renderer cleanup failed', [\n ['renderer mount', () => keeper?.unmount()],\n ['renderer work drain (before frontend)', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.settle()) as Promise<void>],\n ['renderer frontend', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.dispose()) as Promise<void>],\n ['renderer work drain (after frontend)', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.settle()) as Promise<void>],\n ['renderer root', () =>\n ExecutionContext.run(rendererRoot as never, () => rendererEnv.dispose()) as Promise<void>],\n ]);\n\n return {\n frontend,\n events: innerTransport.events,\n rendererRoot,\n async open(page, params, query) {\n const opened = await frontend.open({\n page,\n ...(params ? { params } : {}),\n ...(query ? { query } : {}),\n });\n await syncMount();\n return opened;\n },\n async navigate(page, params, query) {\n if (!frontend.current()) throw new TestWoodError('navigate() needs an open page; call open(page) first');\n await frontend.act(() => navigation.go(page, params, query));\n await syncMount();\n const current = frontend.current();\n if (!current) throw new TestWoodError(`navigate(\"${page}\") left no page open`);\n return current;\n },\n current: () => frontend.current(),\n settle,\n pendingWork: () => registry.pending(),\n verify: () => rendererEnv.verify(),\n dom: keeper ? domDriver(keeper) : null,\n dispose,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/testing/wood_env_renderer.ts"],"sourcesContent":["/**\n * The renderer tier of a `testWood(configPath)` environment: a fresh renderer\n * root (mirroring the production process split), an environment-local\n * Navigation, the generated bridge client over an in-process transport into\n * the main router, and the frontend application surface over the selected\n * page catalog. With a DOM mode, the page's real component is mounted by the\n * app's host module and driven through a selector-based surface.\n */\nimport type { IContainer } from '@noego/ioc';\nimport { ExecutionContext, LoadAs } from '@noego/ioc';\nimport { ENV_REGISTRY, type TestExecutableOverlays, type TestIocBuilder } from '@noego/testing';\nimport type { WoodRouter } from '../router/wood_router.cjs';\nimport { InProcessRouterTransport } from '../transport/in_process_router_transport.js';\nimport { buildTransportBridge } from '../transport/build_transport_bridge.js';\nimport { RouterBridgeEvents } from './create_router_bridge.js';\nimport type { OperationDef, ViewDef } from '../types/index.cjs';\nimport type { DataLoader, ViewLoaderSet } from '../loader/loader_orchestration.js';\nimport { Navigation } from '../navigation/index.js';\nimport type { PageCatalog } from '../frontend/page_catalog.js';\nimport { WorkRegistry } from '../frontend/work_registry.js';\nimport { TrackedTransport } from '../frontend/tracked_transport.js';\nimport {\n createFrontendSurface,\n type FrontendApplicationSurface,\n type PageScopeContext,\n} from '../frontend/frontend_surface.js';\nimport type { FrontendPage } from '../frontend/frontend_page.js';\nimport { TestWoodError, WOOD_FRONTEND_BRIDGE } from './test_wood.cjs';\nimport { importDefaultExport, type ModuleImporter, type WoodConfigSource } from './wood_config_source.cjs';\nimport type { WoodDomMode } from './wood_test_selection.cjs';\nimport { leaseDomAnimation, type WoodAnimationLease } from './wood_dom_animation.cjs';\nimport {\n buildRecordedEnvironment,\n cleanupAll,\n RegistrationOnlyRoot,\n TestWoodCleanupError,\n} from './registration_only_root.cjs';\n\n/** What the app's `main.testing.compose` module may hand back for the renderer side. */\nexport interface WoodRendererHooks {\n /** Backend push emitter feeding renderer subscriptions (default: transport-owned). */\n events?: RouterBridgeEvents;\n /** Register app-owned renderer composition on the fresh renderer root. */\n configureRendererRoot?(root: IContainer): void | Promise<void>;\n /** Register per-scope renderer builtins on a new page/layout scope. */\n configurePageScope?(scope: IContainer, view: ViewDef, context: PageScopeContext): void | Promise<void>;\n}\n\n/** A page's real component mounted under a DOM driver (`renderer.testing.host`). */\nexport interface WoodMountedPage {\n readonly target: Element;\n flush?(): void | Promise<void>;\n unmount(): void;\n}\n\nexport type WoodPageHost = (identity: string, page: FrontendPage, target: Element) => WoodMountedPage;\n\nexport interface RendererTierOptions {\n source: WoodConfigSource;\n router: WoodRouter;\n catalog: PageCatalog;\n operations: Map<string, OperationDef>;\n importer: ModuleImporter;\n hooks: WoodRendererHooks | null;\n dom: WoodDomMode | null;\n ioc: TestIocBuilder;\n /** Tokens whose `.value`/callable `.function` replacements stay caller-owned. */\n borrowedTokens?: readonly unknown[];\n registrations: RegistrationOnlyRoot;\n controllerClasses: ReadonlyMap<string, ControllerClass>;\n /**\n * `.load` seams over the page/layout DATA loaders. The native loader\n * identity is the config-relative companion source path\n * (`ui/pages/chat.load.ts`), export `default`.\n */\n loaders?: TestExecutableOverlays;\n /** The imported companion loaders by identity (discovered at build, none has run). */\n loaderModules?: ReadonlyMap<string, DataLoader>;\n}\n\nexport interface WoodDomDriver {\n readonly target: Element;\n click(selector: string): Promise<void>;\n type(selector: string, text: string): Promise<void>;\n submit(selector: string): Promise<void>;\n text(selector: string): Promise<string>;\n exists(selector: string): Promise<boolean>;\n}\n\nexport interface RendererTier {\n readonly frontend: FrontendApplicationSurface;\n readonly events: RouterBridgeEvents;\n readonly rendererRoot: IContainer;\n open(page: string, params?: Record<string, string>, query?: Record<string, string>): Promise<FrontendPage>;\n navigate(page: string, params?: Record<string, string>, query?: Record<string, string>): Promise<FrontendPage>;\n current(): FrontendPage | null;\n settle(): Promise<void>;\n pendingWork(): ReturnType<WorkRegistry['pending']>;\n verify(): Promise<void>;\n dom: WoodDomDriver | null;\n dispose(): Promise<void>;\n /** The renderer owner's method-seam registry (`test.inspect` on renderer tokens). */\n readonly [ENV_REGISTRY]: unknown;\n}\ntype ControllerClass = new (...args: never[]) => unknown;\n\nfunction controllerLoader(\n source: WoodConfigSource,\n importer: ModuleImporter,\n preloaded: ReadonlyMap<string, ControllerClass>,\n) {\n const cache = new Map<string, Promise<ControllerClass>>();\n for (const [identity, controller] of preloaded) cache.set(identity, Promise.resolve(controller));\n return (identity: string): Promise<ControllerClass> => {\n let pending = cache.get(identity);\n if (!pending) {\n pending = importDefaultExport<ControllerClass>(\n importer,\n source.rendererControllerFile(identity),\n `renderer controller \"${identity}\"`,\n );\n cache.set(identity, pending);\n }\n return pending;\n };\n}\n\nasync function loadPageHost(\n source: WoodConfigSource,\n importer: ModuleImporter,\n dom: WoodDomMode,\n): Promise<WoodPageHost> {\n if (dom === 'chromium') {\n throw new TestWoodError(\"select({ dom: 'chromium' }) is not available yet; use 'happy-dom'\");\n }\n if (typeof document === 'undefined') {\n throw new TestWoodError(\n \"select({ dom: 'happy-dom' }) requires a DOM environment (// @vitest-environment happy-dom)\",\n );\n }\n const hostPath = source.hookModule(source.config.renderer.testing?.host);\n if (!hostPath) {\n throw new TestWoodError(\n 'select({ dom }) requires renderer.testing.host in wood.config.yml (a module whose default export mounts a page)',\n );\n }\n return importDefaultExport<WoodPageHost>(importer, hostPath, 'renderer.testing.host');\n}\n\n/**\n * Keeps the mounted component in step with the surface's current page. While\n * a page is mounted it holds a Web Animations lease on the target's realm so\n * Svelte transitions work under happy-dom (see `wood_dom_animation.ts`); the\n * lease is released on unmount, on a failed host mount, and on dispose.\n */\nexport class MountKeeper {\n private mounted: WoodMountedPage | null = null;\n private mountedFor: FrontendPage | null = null;\n private animation: WoodAnimationLease | null = null;\n\n constructor(private readonly host: WoodPageHost) {}\n\n get target(): Element {\n if (!this.mounted) throw new TestWoodError('no page is mounted; open(page) first');\n return this.mounted.target;\n }\n\n /** The Web Animations lease held for the current mount (null when nothing is mounted). */\n get animationLease(): WoodAnimationLease | null {\n return this.animation;\n }\n\n async flush(): Promise<void> {\n await this.mounted?.flush?.();\n }\n\n sync(current: FrontendPage | null): void {\n if (current === this.mountedFor) return;\n this.unmount();\n if (!current) return;\n const target = document.createElement('div');\n document.body.appendChild(target);\n const lease = leaseDomAnimation(target);\n try {\n this.mounted = this.host(current.identity, current, target);\n } catch (error) {\n lease.release();\n target.remove();\n throw error;\n }\n this.animation = lease;\n this.mountedFor = current;\n }\n\n unmount(): void {\n const mounted = this.mounted;\n const lease = this.animation;\n this.mounted = null;\n this.mountedFor = null;\n this.animation = null;\n try {\n mounted?.unmount();\n } finally {\n mounted?.target.remove();\n lease?.release();\n }\n }\n}\n\nfunction domDriver(keeper: MountKeeper): WoodDomDriver {\n const find = (selector: string): Element => {\n const found = keeper.target.querySelector(selector);\n if (!found) throw new TestWoodError(`no element matches \"${selector}\"`);\n return found;\n };\n return {\n get target() {\n return keeper.target;\n },\n async click(selector) {\n (find(selector) as HTMLElement).click();\n await keeper.flush();\n },\n async type(selector, text) {\n const input = find(selector) as HTMLInputElement;\n input.focus();\n input.value = text;\n input.dispatchEvent(new Event('input', { bubbles: true }));\n await keeper.flush();\n },\n async submit(selector) {\n find(selector).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));\n await keeper.flush();\n },\n async text(selector) {\n return keeper.target.querySelector(selector)?.textContent ?? '';\n },\n async exists(selector) {\n return keeper.target.querySelector(selector) !== null;\n },\n };\n}\n\nexport async function buildRendererTier(options: RendererTierOptions): Promise<RendererTier> {\n const {\n source,\n router,\n catalog,\n operations,\n importer,\n hooks,\n dom,\n ioc,\n borrowedTokens,\n registrations,\n controllerClasses,\n loaders,\n loaderModules,\n } = options;\n const keeper = dom ? new MountKeeper(await loadPageHost(source, importer, dom)) : null;\n\n const registry = new WorkRegistry();\n const defaultNavigation = new Navigation();\n const innerTransport = new InProcessRouterTransport(router, hooks?.events ?? new RouterBridgeEvents());\n let currentOwner: () => string = () => 'application';\n const transport = new TrackedTransport(innerTransport, registry, () => currentOwner());\n const { bridge } = buildTransportBridge(operations, transport);\n\n registrations.root.registerFunction(Navigation, () => defaultNavigation, { loadAs: LoadAs.Singleton });\n registrations.root.registerFunction(WOOD_FRONTEND_BRIDGE, () => bridge, { loadAs: LoadAs.Singleton });\n\n const rendererEnv = await buildRecordedEnvironment(ioc, registrations, 'renderer', borrowedTokens);\n const rendererRoot = rendererEnv.root;\n let navigation: Navigation;\n const loadController = controllerLoader(source, importer, controllerClasses);\n const resolveController = (identity: string): Promise<ControllerClass> => loadController(identity);\n // `.load` seams are installed over the imported companion loaders NOW —\n // before any page resolves — one slot per loader identity. Nothing runs\n // here; a page open/navigation executes them through the shared\n // orchestration exactly as the production renderer does.\n const installedLoaders = new Map<string, DataLoader>();\n for (const [identity, original] of loaderModules ?? []) {\n installedLoaders.set(\n identity,\n loaders ? (loaders.replace('load', { path: identity, export: 'default' }, original) as DataLoader) : original,\n );\n }\n const loaderFor = (viewPath: string): DataLoader | null => {\n const reference = source.rendererLoaderReference(viewPath);\n if (!reference) return null;\n const installed = installedLoaders.get(reference.identity);\n if (!installed) {\n throw new TestWoodError(`data loader ${reference.identity} was not imported at build; rebuild the environment`);\n }\n return installed;\n };\n const resolveLoaders = (view: ViewDef): ViewLoaderSet => ({\n layouts: view.layouts.map((layoutPath) => loaderFor(layoutPath)),\n view: loaderFor(view.viewPath),\n });\n let frontend: FrontendApplicationSurface;\n try {\n navigation = await ExecutionContext.run(rendererRoot as never, () => rendererRoot.get(Navigation)) as Navigation;\n frontend = createFrontendSurface({\n catalog,\n rendererRoot: rendererRoot as never,\n registry,\n navigation,\n resolveController,\n resolveLoaders,\n ...(hooks?.configurePageScope\n ? { configurePageScope: (scope, view, context) => hooks.configurePageScope!(scope as IContainer, view, context) }\n : {}),\n });\n } catch (error) {\n try {\n await cleanupAll('renderer build rollback failed', [\n ['renderer mount', () => keeper?.unmount()],\n ['renderer root', () => ExecutionContext.run(rendererRoot as never, () => rendererEnv.dispose()) as Promise<void>],\n ]);\n } catch (cleanupError) {\n const failures = cleanupError instanceof TestWoodCleanupError\n ? cleanupError.failures\n : [{ owner: 'renderer rollback', error: cleanupError }];\n throw new TestWoodCleanupError('renderer build failed and rollback failed', failures, error);\n }\n throw error;\n }\n\n currentOwner = () => {\n const current = frontend.current();\n return current ? `page ${current.identity}` : 'application';\n };\n\n const syncMount = async (): Promise<void> => {\n if (!keeper) return;\n keeper.sync(frontend.current());\n await keeper.flush();\n };\n const settle = async (): Promise<void> => {\n await frontend.settle();\n if (!keeper) return;\n await syncMount();\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\n await keeper.flush();\n };\n\n let closing: Promise<void> | null = null;\n const dispose = (): Promise<void> => closing ??= cleanupAll('renderer cleanup failed', [\n ['renderer mount', () => keeper?.unmount()],\n ['renderer work drain (before frontend)', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.settle()) as Promise<void>],\n ['renderer frontend', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.dispose()) as Promise<void>],\n ['renderer work drain (after frontend)', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.settle()) as Promise<void>],\n ['renderer root', () =>\n ExecutionContext.run(rendererRoot as never, () => rendererEnv.dispose()) as Promise<void>],\n ]);\n\n return {\n frontend,\n events: innerTransport.events,\n rendererRoot,\n async open(page, params, query) {\n const opened = await frontend.open({\n page,\n ...(params ? { params } : {}),\n ...(query ? { query } : {}),\n });\n await syncMount();\n return opened;\n },\n async navigate(page, params, query) {\n if (!frontend.current()) throw new TestWoodError('navigate() needs an open page; call open(page) first');\n await frontend.act(() => navigation.go(page, params, query));\n await syncMount();\n const current = frontend.current();\n if (!current) throw new TestWoodError(`navigate(\"${page}\") left no page open`);\n return current;\n },\n current: () => frontend.current(),\n settle,\n pendingWork: () => registry.pending(),\n verify: () => rendererEnv.verify(),\n dom: keeper ? domDriver(keeper) : null,\n dispose,\n [ENV_REGISTRY]: (rendererEnv as unknown as Record<symbol, unknown>)[ENV_REGISTRY],\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,9 +1,10 @@
1
1
  import { IContainer } from '@noego/ioc';
2
- import { TestIocBuilder } from '@noego/testing';
2
+ import { ENV_REGISTRY, TestIocBuilder, TestExecutableOverlays } from '@noego/testing';
3
3
  import { WoodRouter } from '../router/wood_router.cjs';
4
4
  import { RouterBridgeEvents } from './create_router_bridge.cjs';
5
5
  import { OperationDef } from '../types/operations.cjs';
6
6
  import { ViewDef } from '../types/views.cjs';
7
+ import { DataLoader } from '../loader/loader_orchestration.cjs';
7
8
  import { PageCatalog } from '../frontend/page_catalog.cjs';
8
9
  import { WorkRegistry } from '../frontend/work_registry.cjs';
9
10
  import { PageScopeContext, FrontendApplicationSurface } from '../frontend/frontend_surface.cjs';
@@ -57,8 +58,18 @@ interface RendererTierOptions {
57
58
  hooks: WoodRendererHooks | null;
58
59
  dom: WoodDomMode | null;
59
60
  ioc: TestIocBuilder;
61
+ /** Tokens whose `.value`/callable `.function` replacements stay caller-owned. */
62
+ borrowedTokens?: readonly unknown[];
60
63
  registrations: RegistrationOnlyRoot;
61
64
  controllerClasses: ReadonlyMap<string, ControllerClass>;
65
+ /**
66
+ * `.load` seams over the page/layout DATA loaders. The native loader
67
+ * identity is the config-relative companion source path
68
+ * (`ui/pages/chat.load.ts`), export `default`.
69
+ */
70
+ loaders?: TestExecutableOverlays;
71
+ /** The imported companion loaders by identity (discovered at build, none has run). */
72
+ loaderModules?: ReadonlyMap<string, DataLoader>;
62
73
  }
63
74
  interface WoodDomDriver {
64
75
  readonly target: Element;
@@ -80,6 +91,8 @@ interface RendererTier {
80
91
  verify(): Promise<void>;
81
92
  dom: WoodDomDriver | null;
82
93
  dispose(): Promise<void>;
94
+ /** The renderer owner's method-seam registry (`test.inspect` on renderer tokens). */
95
+ readonly [ENV_REGISTRY]: unknown;
83
96
  }
84
97
  type ControllerClass = new (...args: never[]) => unknown;
85
98
  /**
@@ -1,9 +1,10 @@
1
1
  import { IContainer } from '@noego/ioc';
2
- import { TestIocBuilder } from '@noego/testing';
2
+ import { ENV_REGISTRY, TestIocBuilder, TestExecutableOverlays } from '@noego/testing';
3
3
  import { WoodRouter } from '../router/wood_router.js';
4
4
  import { RouterBridgeEvents } from './create_router_bridge.js';
5
5
  import { OperationDef } from '../types/operations.js';
6
6
  import { ViewDef } from '../types/views.js';
7
+ import { DataLoader } from '../loader/loader_orchestration.js';
7
8
  import { PageCatalog } from '../frontend/page_catalog.js';
8
9
  import { WorkRegistry } from '../frontend/work_registry.js';
9
10
  import { PageScopeContext, FrontendApplicationSurface } from '../frontend/frontend_surface.js';
@@ -57,8 +58,18 @@ interface RendererTierOptions {
57
58
  hooks: WoodRendererHooks | null;
58
59
  dom: WoodDomMode | null;
59
60
  ioc: TestIocBuilder;
61
+ /** Tokens whose `.value`/callable `.function` replacements stay caller-owned. */
62
+ borrowedTokens?: readonly unknown[];
60
63
  registrations: RegistrationOnlyRoot;
61
64
  controllerClasses: ReadonlyMap<string, ControllerClass>;
65
+ /**
66
+ * `.load` seams over the page/layout DATA loaders. The native loader
67
+ * identity is the config-relative companion source path
68
+ * (`ui/pages/chat.load.ts`), export `default`.
69
+ */
70
+ loaders?: TestExecutableOverlays;
71
+ /** The imported companion loaders by identity (discovered at build, none has run). */
72
+ loaderModules?: ReadonlyMap<string, DataLoader>;
62
73
  }
63
74
  interface WoodDomDriver {
64
75
  readonly target: Element;
@@ -80,6 +91,8 @@ interface RendererTier {
80
91
  verify(): Promise<void>;
81
92
  dom: WoodDomDriver | null;
82
93
  dispose(): Promise<void>;
94
+ /** The renderer owner's method-seam registry (`test.inspect` on renderer tokens). */
95
+ readonly [ENV_REGISTRY]: unknown;
83
96
  }
84
97
  type ControllerClass = new (...args: never[]) => unknown;
85
98
  /**
@@ -1,4 +1,5 @@
1
1
  import { ExecutionContext, LoadAs } from "@noego/ioc";
2
+ import { ENV_REGISTRY } from "@noego/testing";
2
3
  import { InProcessRouterTransport } from "../transport/in_process_router_transport.js";
3
4
  import { buildTransportBridge } from "../transport/build_transport_bridge.js";
4
5
  import { RouterBridgeEvents } from "./create_router_bridge.js";
@@ -141,8 +142,11 @@ async function buildRendererTier(options) {
141
142
  hooks,
142
143
  dom,
143
144
  ioc,
145
+ borrowedTokens,
144
146
  registrations,
145
- controllerClasses
147
+ controllerClasses,
148
+ loaders,
149
+ loaderModules
146
150
  } = options;
147
151
  const keeper = dom ? new MountKeeper(await loadPageHost(source, importer, dom)) : null;
148
152
  const registry = new WorkRegistry();
@@ -153,10 +157,31 @@ async function buildRendererTier(options) {
153
157
  const { bridge } = buildTransportBridge(operations, transport);
154
158
  registrations.root.registerFunction(Navigation, () => defaultNavigation, { loadAs: LoadAs.Singleton });
155
159
  registrations.root.registerFunction(WOOD_FRONTEND_BRIDGE, () => bridge, { loadAs: LoadAs.Singleton });
156
- const rendererEnv = await buildRecordedEnvironment(ioc, registrations, "renderer");
160
+ const rendererEnv = await buildRecordedEnvironment(ioc, registrations, "renderer", borrowedTokens);
157
161
  const rendererRoot = rendererEnv.root;
158
162
  let navigation;
159
163
  const loadController = controllerLoader(source, importer, controllerClasses);
164
+ const resolveController = (identity) => loadController(identity);
165
+ const installedLoaders = /* @__PURE__ */ new Map();
166
+ for (const [identity, original] of loaderModules ?? []) {
167
+ installedLoaders.set(
168
+ identity,
169
+ loaders ? loaders.replace("load", { path: identity, export: "default" }, original) : original
170
+ );
171
+ }
172
+ const loaderFor = (viewPath) => {
173
+ const reference = source.rendererLoaderReference(viewPath);
174
+ if (!reference) return null;
175
+ const installed = installedLoaders.get(reference.identity);
176
+ if (!installed) {
177
+ throw new TestWoodError(`data loader ${reference.identity} was not imported at build; rebuild the environment`);
178
+ }
179
+ return installed;
180
+ };
181
+ const resolveLoaders = (view) => ({
182
+ layouts: view.layouts.map((layoutPath) => loaderFor(layoutPath)),
183
+ view: loaderFor(view.viewPath)
184
+ });
160
185
  let frontend;
161
186
  try {
162
187
  navigation = await ExecutionContext.run(rendererRoot, () => rendererRoot.get(Navigation));
@@ -165,7 +190,8 @@ async function buildRendererTier(options) {
165
190
  rendererRoot,
166
191
  registry,
167
192
  navigation,
168
- resolveController: (identity) => loadController(identity),
193
+ resolveController,
194
+ resolveLoaders,
169
195
  ...hooks?.configurePageScope ? { configurePageScope: (scope, view, context) => hooks.configurePageScope(scope, view, context) } : {}
170
196
  });
171
197
  } catch (error) {
@@ -230,7 +256,8 @@ async function buildRendererTier(options) {
230
256
  pendingWork: () => registry.pending(),
231
257
  verify: () => rendererEnv.verify(),
232
258
  dom: keeper ? domDriver(keeper) : null,
233
- dispose
259
+ dispose,
260
+ [ENV_REGISTRY]: rendererEnv[ENV_REGISTRY]
234
261
  };
235
262
  }
236
263
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/testing/wood_env_renderer.ts"],"sourcesContent":["/**\n * The renderer tier of a `testWood(configPath)` environment: a fresh renderer\n * root (mirroring the production process split), an environment-local\n * Navigation, the generated bridge client over an in-process transport into\n * the main router, and the frontend application surface over the selected\n * page catalog. With a DOM mode, the page's real component is mounted by the\n * app's host module and driven through a selector-based surface.\n */\nimport type { IContainer } from '@noego/ioc';\nimport { ExecutionContext, LoadAs } from '@noego/ioc';\nimport type { TestIocBuilder } from '@noego/testing';\nimport type { WoodRouter } from '../router/wood_router.js';\nimport { InProcessRouterTransport } from '../transport/in_process_router_transport.js';\nimport { buildTransportBridge } from '../transport/build_transport_bridge.js';\nimport { RouterBridgeEvents } from './create_router_bridge.js';\nimport type { OperationDef, ViewDef } from '../types/index.js';\nimport { Navigation } from '../navigation/index.js';\nimport type { PageCatalog } from '../frontend/page_catalog.js';\nimport { WorkRegistry } from '../frontend/work_registry.js';\nimport { TrackedTransport } from '../frontend/tracked_transport.js';\nimport {\n createFrontendSurface,\n type FrontendApplicationSurface,\n type PageScopeContext,\n} from '../frontend/frontend_surface.js';\nimport type { FrontendPage } from '../frontend/frontend_page.js';\nimport { TestWoodError, WOOD_FRONTEND_BRIDGE } from './test_wood.js';\nimport { importDefaultExport, type ModuleImporter, type WoodConfigSource } from './wood_config_source.js';\nimport type { WoodDomMode } from './wood_test_selection.js';\nimport { leaseDomAnimation, type WoodAnimationLease } from './wood_dom_animation.js';\nimport {\n buildRecordedEnvironment,\n cleanupAll,\n RegistrationOnlyRoot,\n TestWoodCleanupError,\n} from './registration_only_root.js';\n\n/** What the app's `main.testing.compose` module may hand back for the renderer side. */\nexport interface WoodRendererHooks {\n /** Backend push emitter feeding renderer subscriptions (default: transport-owned). */\n events?: RouterBridgeEvents;\n /** Register app-owned renderer composition on the fresh renderer root. */\n configureRendererRoot?(root: IContainer): void | Promise<void>;\n /** Register per-scope renderer builtins on a new page/layout scope. */\n configurePageScope?(scope: IContainer, view: ViewDef, context: PageScopeContext): void | Promise<void>;\n}\n\n/** A page's real component mounted under a DOM driver (`renderer.testing.host`). */\nexport interface WoodMountedPage {\n readonly target: Element;\n flush?(): void | Promise<void>;\n unmount(): void;\n}\n\nexport type WoodPageHost = (identity: string, page: FrontendPage, target: Element) => WoodMountedPage;\n\nexport interface RendererTierOptions {\n source: WoodConfigSource;\n router: WoodRouter;\n catalog: PageCatalog;\n operations: Map<string, OperationDef>;\n importer: ModuleImporter;\n hooks: WoodRendererHooks | null;\n dom: WoodDomMode | null;\n ioc: TestIocBuilder;\n registrations: RegistrationOnlyRoot;\n controllerClasses: ReadonlyMap<string, ControllerClass>;\n}\n\nexport interface WoodDomDriver {\n readonly target: Element;\n click(selector: string): Promise<void>;\n type(selector: string, text: string): Promise<void>;\n submit(selector: string): Promise<void>;\n text(selector: string): Promise<string>;\n exists(selector: string): Promise<boolean>;\n}\n\nexport interface RendererTier {\n readonly frontend: FrontendApplicationSurface;\n readonly events: RouterBridgeEvents;\n readonly rendererRoot: IContainer;\n open(page: string, params?: Record<string, string>, query?: Record<string, string>): Promise<FrontendPage>;\n navigate(page: string, params?: Record<string, string>, query?: Record<string, string>): Promise<FrontendPage>;\n current(): FrontendPage | null;\n settle(): Promise<void>;\n pendingWork(): ReturnType<WorkRegistry['pending']>;\n verify(): Promise<void>;\n dom: WoodDomDriver | null;\n dispose(): Promise<void>;\n}\ntype ControllerClass = new (...args: never[]) => unknown;\n\nfunction controllerLoader(\n source: WoodConfigSource,\n importer: ModuleImporter,\n preloaded: ReadonlyMap<string, ControllerClass>,\n) {\n const cache = new Map<string, Promise<ControllerClass>>();\n for (const [identity, controller] of preloaded) cache.set(identity, Promise.resolve(controller));\n return (identity: string): Promise<ControllerClass> => {\n let pending = cache.get(identity);\n if (!pending) {\n pending = importDefaultExport<ControllerClass>(\n importer,\n source.rendererControllerFile(identity),\n `renderer controller \"${identity}\"`,\n );\n cache.set(identity, pending);\n }\n return pending;\n };\n}\n\nasync function loadPageHost(\n source: WoodConfigSource,\n importer: ModuleImporter,\n dom: WoodDomMode,\n): Promise<WoodPageHost> {\n if (dom === 'chromium') {\n throw new TestWoodError(\"select({ dom: 'chromium' }) is not available yet; use 'happy-dom'\");\n }\n if (typeof document === 'undefined') {\n throw new TestWoodError(\n \"select({ dom: 'happy-dom' }) requires a DOM environment (// @vitest-environment happy-dom)\",\n );\n }\n const hostPath = source.hookModule(source.config.renderer.testing?.host);\n if (!hostPath) {\n throw new TestWoodError(\n 'select({ dom }) requires renderer.testing.host in wood.config.yml (a module whose default export mounts a page)',\n );\n }\n return importDefaultExport<WoodPageHost>(importer, hostPath, 'renderer.testing.host');\n}\n\n/**\n * Keeps the mounted component in step with the surface's current page. While\n * a page is mounted it holds a Web Animations lease on the target's realm so\n * Svelte transitions work under happy-dom (see `wood_dom_animation.ts`); the\n * lease is released on unmount, on a failed host mount, and on dispose.\n */\nexport class MountKeeper {\n private mounted: WoodMountedPage | null = null;\n private mountedFor: FrontendPage | null = null;\n private animation: WoodAnimationLease | null = null;\n\n constructor(private readonly host: WoodPageHost) {}\n\n get target(): Element {\n if (!this.mounted) throw new TestWoodError('no page is mounted; open(page) first');\n return this.mounted.target;\n }\n\n /** The Web Animations lease held for the current mount (null when nothing is mounted). */\n get animationLease(): WoodAnimationLease | null {\n return this.animation;\n }\n\n async flush(): Promise<void> {\n await this.mounted?.flush?.();\n }\n\n sync(current: FrontendPage | null): void {\n if (current === this.mountedFor) return;\n this.unmount();\n if (!current) return;\n const target = document.createElement('div');\n document.body.appendChild(target);\n const lease = leaseDomAnimation(target);\n try {\n this.mounted = this.host(current.identity, current, target);\n } catch (error) {\n lease.release();\n target.remove();\n throw error;\n }\n this.animation = lease;\n this.mountedFor = current;\n }\n\n unmount(): void {\n const mounted = this.mounted;\n const lease = this.animation;\n this.mounted = null;\n this.mountedFor = null;\n this.animation = null;\n try {\n mounted?.unmount();\n } finally {\n mounted?.target.remove();\n lease?.release();\n }\n }\n}\n\nfunction domDriver(keeper: MountKeeper): WoodDomDriver {\n const find = (selector: string): Element => {\n const found = keeper.target.querySelector(selector);\n if (!found) throw new TestWoodError(`no element matches \"${selector}\"`);\n return found;\n };\n return {\n get target() {\n return keeper.target;\n },\n async click(selector) {\n (find(selector) as HTMLElement).click();\n await keeper.flush();\n },\n async type(selector, text) {\n const input = find(selector) as HTMLInputElement;\n input.focus();\n input.value = text;\n input.dispatchEvent(new Event('input', { bubbles: true }));\n await keeper.flush();\n },\n async submit(selector) {\n find(selector).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));\n await keeper.flush();\n },\n async text(selector) {\n return keeper.target.querySelector(selector)?.textContent ?? '';\n },\n async exists(selector) {\n return keeper.target.querySelector(selector) !== null;\n },\n };\n}\n\nexport async function buildRendererTier(options: RendererTierOptions): Promise<RendererTier> {\n const {\n source,\n router,\n catalog,\n operations,\n importer,\n hooks,\n dom,\n ioc,\n registrations,\n controllerClasses,\n } = options;\n const keeper = dom ? new MountKeeper(await loadPageHost(source, importer, dom)) : null;\n\n const registry = new WorkRegistry();\n const defaultNavigation = new Navigation();\n const innerTransport = new InProcessRouterTransport(router, hooks?.events ?? new RouterBridgeEvents());\n let currentOwner: () => string = () => 'application';\n const transport = new TrackedTransport(innerTransport, registry, () => currentOwner());\n const { bridge } = buildTransportBridge(operations, transport);\n\n registrations.root.registerFunction(Navigation, () => defaultNavigation, { loadAs: LoadAs.Singleton });\n registrations.root.registerFunction(WOOD_FRONTEND_BRIDGE, () => bridge, { loadAs: LoadAs.Singleton });\n\n const rendererEnv = await buildRecordedEnvironment(ioc, registrations, 'renderer');\n const rendererRoot = rendererEnv.root;\n let navigation: Navigation;\n const loadController = controllerLoader(source, importer, controllerClasses);\n let frontend: FrontendApplicationSurface;\n try {\n navigation = await ExecutionContext.run(rendererRoot as never, () => rendererRoot.get(Navigation)) as Navigation;\n frontend = createFrontendSurface({\n catalog,\n rendererRoot: rendererRoot as never,\n registry,\n navigation,\n resolveController: (identity) => loadController(identity),\n ...(hooks?.configurePageScope\n ? { configurePageScope: (scope, view, context) => hooks.configurePageScope!(scope as IContainer, view, context) }\n : {}),\n });\n } catch (error) {\n try {\n await cleanupAll('renderer build rollback failed', [\n ['renderer mount', () => keeper?.unmount()],\n ['renderer root', () => ExecutionContext.run(rendererRoot as never, () => rendererEnv.dispose()) as Promise<void>],\n ]);\n } catch (cleanupError) {\n const failures = cleanupError instanceof TestWoodCleanupError\n ? cleanupError.failures\n : [{ owner: 'renderer rollback', error: cleanupError }];\n throw new TestWoodCleanupError('renderer build failed and rollback failed', failures, error);\n }\n throw error;\n }\n\n currentOwner = () => {\n const current = frontend.current();\n return current ? `page ${current.identity}` : 'application';\n };\n\n const syncMount = async (): Promise<void> => {\n if (!keeper) return;\n keeper.sync(frontend.current());\n await keeper.flush();\n };\n const settle = async (): Promise<void> => {\n await frontend.settle();\n if (!keeper) return;\n await syncMount();\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\n await keeper.flush();\n };\n\n let closing: Promise<void> | null = null;\n const dispose = (): Promise<void> => closing ??= cleanupAll('renderer cleanup failed', [\n ['renderer mount', () => keeper?.unmount()],\n ['renderer work drain (before frontend)', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.settle()) as Promise<void>],\n ['renderer frontend', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.dispose()) as Promise<void>],\n ['renderer work drain (after frontend)', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.settle()) as Promise<void>],\n ['renderer root', () =>\n ExecutionContext.run(rendererRoot as never, () => rendererEnv.dispose()) as Promise<void>],\n ]);\n\n return {\n frontend,\n events: innerTransport.events,\n rendererRoot,\n async open(page, params, query) {\n const opened = await frontend.open({\n page,\n ...(params ? { params } : {}),\n ...(query ? { query } : {}),\n });\n await syncMount();\n return opened;\n },\n async navigate(page, params, query) {\n if (!frontend.current()) throw new TestWoodError('navigate() needs an open page; call open(page) first');\n await frontend.act(() => navigation.go(page, params, query));\n await syncMount();\n const current = frontend.current();\n if (!current) throw new TestWoodError(`navigate(\"${page}\") left no page open`);\n return current;\n },\n current: () => frontend.current(),\n settle,\n pendingWork: () => registry.pending(),\n verify: () => rendererEnv.verify(),\n dom: keeper ? domDriver(keeper) : null,\n dispose,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/testing/wood_env_renderer.ts"],"sourcesContent":["/**\n * The renderer tier of a `testWood(configPath)` environment: a fresh renderer\n * root (mirroring the production process split), an environment-local\n * Navigation, the generated bridge client over an in-process transport into\n * the main router, and the frontend application surface over the selected\n * page catalog. With a DOM mode, the page's real component is mounted by the\n * app's host module and driven through a selector-based surface.\n */\nimport type { IContainer } from '@noego/ioc';\nimport { ExecutionContext, LoadAs } from '@noego/ioc';\nimport { ENV_REGISTRY, type TestExecutableOverlays, type TestIocBuilder } from '@noego/testing';\nimport type { WoodRouter } from '../router/wood_router.js';\nimport { InProcessRouterTransport } from '../transport/in_process_router_transport.js';\nimport { buildTransportBridge } from '../transport/build_transport_bridge.js';\nimport { RouterBridgeEvents } from './create_router_bridge.js';\nimport type { OperationDef, ViewDef } from '../types/index.js';\nimport type { DataLoader, ViewLoaderSet } from '../loader/loader_orchestration.js';\nimport { Navigation } from '../navigation/index.js';\nimport type { PageCatalog } from '../frontend/page_catalog.js';\nimport { WorkRegistry } from '../frontend/work_registry.js';\nimport { TrackedTransport } from '../frontend/tracked_transport.js';\nimport {\n createFrontendSurface,\n type FrontendApplicationSurface,\n type PageScopeContext,\n} from '../frontend/frontend_surface.js';\nimport type { FrontendPage } from '../frontend/frontend_page.js';\nimport { TestWoodError, WOOD_FRONTEND_BRIDGE } from './test_wood.js';\nimport { importDefaultExport, type ModuleImporter, type WoodConfigSource } from './wood_config_source.js';\nimport type { WoodDomMode } from './wood_test_selection.js';\nimport { leaseDomAnimation, type WoodAnimationLease } from './wood_dom_animation.js';\nimport {\n buildRecordedEnvironment,\n cleanupAll,\n RegistrationOnlyRoot,\n TestWoodCleanupError,\n} from './registration_only_root.js';\n\n/** What the app's `main.testing.compose` module may hand back for the renderer side. */\nexport interface WoodRendererHooks {\n /** Backend push emitter feeding renderer subscriptions (default: transport-owned). */\n events?: RouterBridgeEvents;\n /** Register app-owned renderer composition on the fresh renderer root. */\n configureRendererRoot?(root: IContainer): void | Promise<void>;\n /** Register per-scope renderer builtins on a new page/layout scope. */\n configurePageScope?(scope: IContainer, view: ViewDef, context: PageScopeContext): void | Promise<void>;\n}\n\n/** A page's real component mounted under a DOM driver (`renderer.testing.host`). */\nexport interface WoodMountedPage {\n readonly target: Element;\n flush?(): void | Promise<void>;\n unmount(): void;\n}\n\nexport type WoodPageHost = (identity: string, page: FrontendPage, target: Element) => WoodMountedPage;\n\nexport interface RendererTierOptions {\n source: WoodConfigSource;\n router: WoodRouter;\n catalog: PageCatalog;\n operations: Map<string, OperationDef>;\n importer: ModuleImporter;\n hooks: WoodRendererHooks | null;\n dom: WoodDomMode | null;\n ioc: TestIocBuilder;\n /** Tokens whose `.value`/callable `.function` replacements stay caller-owned. */\n borrowedTokens?: readonly unknown[];\n registrations: RegistrationOnlyRoot;\n controllerClasses: ReadonlyMap<string, ControllerClass>;\n /**\n * `.load` seams over the page/layout DATA loaders. The native loader\n * identity is the config-relative companion source path\n * (`ui/pages/chat.load.ts`), export `default`.\n */\n loaders?: TestExecutableOverlays;\n /** The imported companion loaders by identity (discovered at build, none has run). */\n loaderModules?: ReadonlyMap<string, DataLoader>;\n}\n\nexport interface WoodDomDriver {\n readonly target: Element;\n click(selector: string): Promise<void>;\n type(selector: string, text: string): Promise<void>;\n submit(selector: string): Promise<void>;\n text(selector: string): Promise<string>;\n exists(selector: string): Promise<boolean>;\n}\n\nexport interface RendererTier {\n readonly frontend: FrontendApplicationSurface;\n readonly events: RouterBridgeEvents;\n readonly rendererRoot: IContainer;\n open(page: string, params?: Record<string, string>, query?: Record<string, string>): Promise<FrontendPage>;\n navigate(page: string, params?: Record<string, string>, query?: Record<string, string>): Promise<FrontendPage>;\n current(): FrontendPage | null;\n settle(): Promise<void>;\n pendingWork(): ReturnType<WorkRegistry['pending']>;\n verify(): Promise<void>;\n dom: WoodDomDriver | null;\n dispose(): Promise<void>;\n /** The renderer owner's method-seam registry (`test.inspect` on renderer tokens). */\n readonly [ENV_REGISTRY]: unknown;\n}\ntype ControllerClass = new (...args: never[]) => unknown;\n\nfunction controllerLoader(\n source: WoodConfigSource,\n importer: ModuleImporter,\n preloaded: ReadonlyMap<string, ControllerClass>,\n) {\n const cache = new Map<string, Promise<ControllerClass>>();\n for (const [identity, controller] of preloaded) cache.set(identity, Promise.resolve(controller));\n return (identity: string): Promise<ControllerClass> => {\n let pending = cache.get(identity);\n if (!pending) {\n pending = importDefaultExport<ControllerClass>(\n importer,\n source.rendererControllerFile(identity),\n `renderer controller \"${identity}\"`,\n );\n cache.set(identity, pending);\n }\n return pending;\n };\n}\n\nasync function loadPageHost(\n source: WoodConfigSource,\n importer: ModuleImporter,\n dom: WoodDomMode,\n): Promise<WoodPageHost> {\n if (dom === 'chromium') {\n throw new TestWoodError(\"select({ dom: 'chromium' }) is not available yet; use 'happy-dom'\");\n }\n if (typeof document === 'undefined') {\n throw new TestWoodError(\n \"select({ dom: 'happy-dom' }) requires a DOM environment (// @vitest-environment happy-dom)\",\n );\n }\n const hostPath = source.hookModule(source.config.renderer.testing?.host);\n if (!hostPath) {\n throw new TestWoodError(\n 'select({ dom }) requires renderer.testing.host in wood.config.yml (a module whose default export mounts a page)',\n );\n }\n return importDefaultExport<WoodPageHost>(importer, hostPath, 'renderer.testing.host');\n}\n\n/**\n * Keeps the mounted component in step with the surface's current page. While\n * a page is mounted it holds a Web Animations lease on the target's realm so\n * Svelte transitions work under happy-dom (see `wood_dom_animation.ts`); the\n * lease is released on unmount, on a failed host mount, and on dispose.\n */\nexport class MountKeeper {\n private mounted: WoodMountedPage | null = null;\n private mountedFor: FrontendPage | null = null;\n private animation: WoodAnimationLease | null = null;\n\n constructor(private readonly host: WoodPageHost) {}\n\n get target(): Element {\n if (!this.mounted) throw new TestWoodError('no page is mounted; open(page) first');\n return this.mounted.target;\n }\n\n /** The Web Animations lease held for the current mount (null when nothing is mounted). */\n get animationLease(): WoodAnimationLease | null {\n return this.animation;\n }\n\n async flush(): Promise<void> {\n await this.mounted?.flush?.();\n }\n\n sync(current: FrontendPage | null): void {\n if (current === this.mountedFor) return;\n this.unmount();\n if (!current) return;\n const target = document.createElement('div');\n document.body.appendChild(target);\n const lease = leaseDomAnimation(target);\n try {\n this.mounted = this.host(current.identity, current, target);\n } catch (error) {\n lease.release();\n target.remove();\n throw error;\n }\n this.animation = lease;\n this.mountedFor = current;\n }\n\n unmount(): void {\n const mounted = this.mounted;\n const lease = this.animation;\n this.mounted = null;\n this.mountedFor = null;\n this.animation = null;\n try {\n mounted?.unmount();\n } finally {\n mounted?.target.remove();\n lease?.release();\n }\n }\n}\n\nfunction domDriver(keeper: MountKeeper): WoodDomDriver {\n const find = (selector: string): Element => {\n const found = keeper.target.querySelector(selector);\n if (!found) throw new TestWoodError(`no element matches \"${selector}\"`);\n return found;\n };\n return {\n get target() {\n return keeper.target;\n },\n async click(selector) {\n (find(selector) as HTMLElement).click();\n await keeper.flush();\n },\n async type(selector, text) {\n const input = find(selector) as HTMLInputElement;\n input.focus();\n input.value = text;\n input.dispatchEvent(new Event('input', { bubbles: true }));\n await keeper.flush();\n },\n async submit(selector) {\n find(selector).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));\n await keeper.flush();\n },\n async text(selector) {\n return keeper.target.querySelector(selector)?.textContent ?? '';\n },\n async exists(selector) {\n return keeper.target.querySelector(selector) !== null;\n },\n };\n}\n\nexport async function buildRendererTier(options: RendererTierOptions): Promise<RendererTier> {\n const {\n source,\n router,\n catalog,\n operations,\n importer,\n hooks,\n dom,\n ioc,\n borrowedTokens,\n registrations,\n controllerClasses,\n loaders,\n loaderModules,\n } = options;\n const keeper = dom ? new MountKeeper(await loadPageHost(source, importer, dom)) : null;\n\n const registry = new WorkRegistry();\n const defaultNavigation = new Navigation();\n const innerTransport = new InProcessRouterTransport(router, hooks?.events ?? new RouterBridgeEvents());\n let currentOwner: () => string = () => 'application';\n const transport = new TrackedTransport(innerTransport, registry, () => currentOwner());\n const { bridge } = buildTransportBridge(operations, transport);\n\n registrations.root.registerFunction(Navigation, () => defaultNavigation, { loadAs: LoadAs.Singleton });\n registrations.root.registerFunction(WOOD_FRONTEND_BRIDGE, () => bridge, { loadAs: LoadAs.Singleton });\n\n const rendererEnv = await buildRecordedEnvironment(ioc, registrations, 'renderer', borrowedTokens);\n const rendererRoot = rendererEnv.root;\n let navigation: Navigation;\n const loadController = controllerLoader(source, importer, controllerClasses);\n const resolveController = (identity: string): Promise<ControllerClass> => loadController(identity);\n // `.load` seams are installed over the imported companion loaders NOW —\n // before any page resolves — one slot per loader identity. Nothing runs\n // here; a page open/navigation executes them through the shared\n // orchestration exactly as the production renderer does.\n const installedLoaders = new Map<string, DataLoader>();\n for (const [identity, original] of loaderModules ?? []) {\n installedLoaders.set(\n identity,\n loaders ? (loaders.replace('load', { path: identity, export: 'default' }, original) as DataLoader) : original,\n );\n }\n const loaderFor = (viewPath: string): DataLoader | null => {\n const reference = source.rendererLoaderReference(viewPath);\n if (!reference) return null;\n const installed = installedLoaders.get(reference.identity);\n if (!installed) {\n throw new TestWoodError(`data loader ${reference.identity} was not imported at build; rebuild the environment`);\n }\n return installed;\n };\n const resolveLoaders = (view: ViewDef): ViewLoaderSet => ({\n layouts: view.layouts.map((layoutPath) => loaderFor(layoutPath)),\n view: loaderFor(view.viewPath),\n });\n let frontend: FrontendApplicationSurface;\n try {\n navigation = await ExecutionContext.run(rendererRoot as never, () => rendererRoot.get(Navigation)) as Navigation;\n frontend = createFrontendSurface({\n catalog,\n rendererRoot: rendererRoot as never,\n registry,\n navigation,\n resolveController,\n resolveLoaders,\n ...(hooks?.configurePageScope\n ? { configurePageScope: (scope, view, context) => hooks.configurePageScope!(scope as IContainer, view, context) }\n : {}),\n });\n } catch (error) {\n try {\n await cleanupAll('renderer build rollback failed', [\n ['renderer mount', () => keeper?.unmount()],\n ['renderer root', () => ExecutionContext.run(rendererRoot as never, () => rendererEnv.dispose()) as Promise<void>],\n ]);\n } catch (cleanupError) {\n const failures = cleanupError instanceof TestWoodCleanupError\n ? cleanupError.failures\n : [{ owner: 'renderer rollback', error: cleanupError }];\n throw new TestWoodCleanupError('renderer build failed and rollback failed', failures, error);\n }\n throw error;\n }\n\n currentOwner = () => {\n const current = frontend.current();\n return current ? `page ${current.identity}` : 'application';\n };\n\n const syncMount = async (): Promise<void> => {\n if (!keeper) return;\n keeper.sync(frontend.current());\n await keeper.flush();\n };\n const settle = async (): Promise<void> => {\n await frontend.settle();\n if (!keeper) return;\n await syncMount();\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\n await keeper.flush();\n };\n\n let closing: Promise<void> | null = null;\n const dispose = (): Promise<void> => closing ??= cleanupAll('renderer cleanup failed', [\n ['renderer mount', () => keeper?.unmount()],\n ['renderer work drain (before frontend)', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.settle()) as Promise<void>],\n ['renderer frontend', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.dispose()) as Promise<void>],\n ['renderer work drain (after frontend)', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.settle()) as Promise<void>],\n ['renderer root', () =>\n ExecutionContext.run(rendererRoot as never, () => rendererEnv.dispose()) as Promise<void>],\n ]);\n\n return {\n frontend,\n events: innerTransport.events,\n rendererRoot,\n async open(page, params, query) {\n const opened = await frontend.open({\n page,\n ...(params ? { params } : {}),\n ...(query ? { query } : {}),\n });\n await syncMount();\n return opened;\n },\n async navigate(page, params, query) {\n if (!frontend.current()) throw new TestWoodError('navigate() needs an open page; call open(page) first');\n await frontend.act(() => navigation.go(page, params, query));\n await syncMount();\n const current = frontend.current();\n if (!current) throw new TestWoodError(`navigate(\"${page}\") left no page open`);\n return current;\n },\n current: () => frontend.current(),\n settle,\n pendingWork: () => registry.pending(),\n verify: () => rendererEnv.verify(),\n dom: keeper ? domDriver(keeper) : null,\n dispose,\n [ENV_REGISTRY]: (rendererEnv as unknown as Record<symbol, unknown>)[ENV_REGISTRY],\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var wood_lazy_runtime_exports = {};
20
+ __export(wood_lazy_runtime_exports, {
21
+ LazyRuntimeStage: () => LazyRuntimeStage,
22
+ lazyMember: () => lazyMember
23
+ });
24
+ module.exports = __toCommonJS(wood_lazy_runtime_exports);
25
+ var import_test_wood = require("./test_wood.cjs");
26
+ class LazyRuntimeStage {
27
+ constructor(init, rollback) {
28
+ this.init = init;
29
+ this.rollback = rollback;
30
+ this.pending = null;
31
+ /** The bare `init()` attempt, settled (never rejecting) — excludes rollback. */
32
+ this.attempt = null;
33
+ this.value = null;
34
+ }
35
+ /** The initialized value, or null while this stage has not been used yet. */
36
+ get current() {
37
+ return this.value;
38
+ }
39
+ /** True once initialization has been requested (whether or not it finished). */
40
+ get requested() {
41
+ return this.pending !== null;
42
+ }
43
+ /** Initialize exactly once; concurrent callers share the same promise. */
44
+ acquire() {
45
+ if (!this.pending) {
46
+ const attempt = this.init();
47
+ this.attempt = attempt.then(() => void 0, () => void 0);
48
+ this.pending = attempt.then(
49
+ (value) => {
50
+ this.value = value;
51
+ return value;
52
+ },
53
+ (error) => this.rollback(error)
54
+ );
55
+ }
56
+ return this.pending;
57
+ }
58
+ /**
59
+ * Resolves once an in-flight initialization attempt has finished (either
60
+ * way); immediately when none was requested. Deliberately excludes the
61
+ * rollback so a disposer invoked *by* the rollback never waits on itself.
62
+ */
63
+ settled() {
64
+ return this.attempt ?? Promise.resolve();
65
+ }
66
+ /** A derived view over one member of the initialized value. */
67
+ map(project) {
68
+ const stage = this;
69
+ return {
70
+ get current() {
71
+ return stage.value === null ? null : project(stage.value);
72
+ },
73
+ acquire: async () => project(await stage.acquire())
74
+ };
75
+ }
76
+ }
77
+ function lazyMember(label, value, asyncMethods, hint) {
78
+ const isAsync = (property) => typeof property === "string" && asyncMethods.includes(property);
79
+ return new Proxy({}, {
80
+ get(_target, property) {
81
+ const live = value.current;
82
+ if (live) {
83
+ const member = Reflect.get(live, property, live);
84
+ return typeof member === "function" ? member.bind(live) : member;
85
+ }
86
+ if (isAsync(property)) {
87
+ return async (...args) => {
88
+ const initialized = await value.acquire();
89
+ const method = Reflect.get(initialized, property, initialized);
90
+ return method.apply(initialized, args);
91
+ };
92
+ }
93
+ if (typeof property === "symbol" || property === "then" || property === "constructor") return void 0;
94
+ throw new import_test_wood.TestWoodError(
95
+ `${label}.${property} is not available before the runtime is initialized; ${hint}`
96
+ );
97
+ },
98
+ has(_target, property) {
99
+ const live = value.current;
100
+ return live ? property in live : isAsync(property);
101
+ }
102
+ });
103
+ }
104
+ // Annotate the CommonJS export names for ESM import in node:
105
+ 0 && (module.exports = {
106
+ LazyRuntimeStage,
107
+ lazyMember
108
+ });
109
+ //# sourceMappingURL=wood_lazy_runtime.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/testing/wood_lazy_runtime.ts"],"sourcesContent":["/**\n * Lazy, once-only runtime stages for `testWood(configPath)` environments.\n *\n * `build()` records registrations and installs overlays; the managed router,\n * `compose.start()` and the renderer tier are only initialized on the first\n * explicit use. Concurrent first uses share one initialization; a failed\n * initialization rolls the environment back (through the supplied rollback)\n * and every later use observes the same failure.\n */\nimport { TestWoodError } from './test_wood.cjs';\n\n/** A value that is initialized on first `acquire()` and readable once live. */\nexport interface LazyValue<T> {\n readonly current: T | null;\n acquire(): Promise<T>;\n}\n\nexport class LazyRuntimeStage<T> implements LazyValue<T> {\n private pending: Promise<T> | null = null;\n /** The bare `init()` attempt, settled (never rejecting) — excludes rollback. */\n private attempt: Promise<void> | null = null;\n private value: T | null = null;\n\n constructor(\n private readonly init: () => Promise<T>,\n private readonly rollback: (error: unknown) => Promise<never>,\n ) {}\n\n /** The initialized value, or null while this stage has not been used yet. */\n get current(): T | null {\n return this.value;\n }\n\n /** True once initialization has been requested (whether or not it finished). */\n get requested(): boolean {\n return this.pending !== null;\n }\n\n /** Initialize exactly once; concurrent callers share the same promise. */\n acquire(): Promise<T> {\n if (!this.pending) {\n const attempt = this.init();\n this.attempt = attempt.then(() => undefined, () => undefined);\n this.pending = attempt.then(\n (value) => {\n this.value = value;\n return value;\n },\n (error) => this.rollback(error),\n );\n }\n return this.pending;\n }\n\n /**\n * Resolves once an in-flight initialization attempt has finished (either\n * way); immediately when none was requested. Deliberately excludes the\n * rollback so a disposer invoked *by* the rollback never waits on itself.\n */\n settled(): Promise<void> {\n return this.attempt ?? Promise.resolve();\n }\n\n /** A derived view over one member of the initialized value. */\n map<U>(project: (value: T) => U): LazyValue<U> {\n const stage = this;\n return {\n get current() {\n return stage.value === null ? null : project(stage.value);\n },\n acquire: async () => project(await stage.acquire()),\n };\n }\n}\n\n/**\n * A member whose runtime is initialized on first use. Async methods listed in\n * `asyncMethods` initialize the runtime and then delegate; every other member\n * delegates once initialized and fails with guidance before that.\n */\nexport function lazyMember<T extends object>(\n label: string,\n value: LazyValue<T>,\n asyncMethods: ReadonlyArray<keyof T & string>,\n hint: string,\n): T {\n const isAsync = (property: PropertyKey): property is keyof T & string =>\n typeof property === 'string' && asyncMethods.includes(property as keyof T & string);\n return new Proxy({} as T, {\n get(_target, property) {\n const live = value.current;\n if (live) {\n const member = Reflect.get(live as object, property, live);\n return typeof member === 'function' ? member.bind(live) : member;\n }\n if (isAsync(property)) {\n return async (...args: unknown[]): Promise<unknown> => {\n const initialized = await value.acquire();\n const method = Reflect.get(initialized as object, property, initialized) as (...a: unknown[]) => unknown;\n return method.apply(initialized, args);\n };\n }\n // Promise resolution and inspection probe these; never treat them as use.\n if (typeof property === 'symbol' || property === 'then' || property === 'constructor') return undefined;\n throw new TestWoodError(\n `${label}.${property} is not available before the runtime is initialized; ${hint}`,\n );\n },\n has(_target, property) {\n const live = value.current;\n return live ? property in (live as object) : isAsync(property);\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,uBAA8B;AAQvB,MAAM,iBAA4C;AAAA,EAMvD,YACmB,MACA,UACjB;AAFiB;AACA;AAPnB,SAAQ,UAA6B;AAErC;AAAA,SAAQ,UAAgC;AACxC,SAAQ,QAAkB;AAAA,EAKvB;AAAA;AAAA,EAGH,IAAI,UAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,YAAqB;AACvB,WAAO,KAAK,YAAY;AAAA,EAC1B;AAAA;AAAA,EAGA,UAAsB;AACpB,QAAI,CAAC,KAAK,SAAS;AACjB,YAAM,UAAU,KAAK,KAAK;AAC1B,WAAK,UAAU,QAAQ,KAAK,MAAM,QAAW,MAAM,MAAS;AAC5D,WAAK,UAAU,QAAQ;AAAA,QACrB,CAAC,UAAU;AACT,eAAK,QAAQ;AACb,iBAAO;AAAA,QACT;AAAA,QACA,CAAC,UAAU,KAAK,SAAS,KAAK;AAAA,MAChC;AAAA,IACF;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UAAyB;AACvB,WAAO,KAAK,WAAW,QAAQ,QAAQ;AAAA,EACzC;AAAA;AAAA,EAGA,IAAO,SAAwC;AAC7C,UAAM,QAAQ;AACd,WAAO;AAAA,MACL,IAAI,UAAU;AACZ,eAAO,MAAM,UAAU,OAAO,OAAO,QAAQ,MAAM,KAAK;AAAA,MAC1D;AAAA,MACA,SAAS,YAAY,QAAQ,MAAM,MAAM,QAAQ,CAAC;AAAA,IACpD;AAAA,EACF;AACF;AAOO,SAAS,WACd,OACA,OACA,cACA,MACG;AACH,QAAM,UAAU,CAAC,aACf,OAAO,aAAa,YAAY,aAAa,SAAS,QAA4B;AACpF,SAAO,IAAI,MAAM,CAAC,GAAQ;AAAA,IACxB,IAAI,SAAS,UAAU;AACrB,YAAM,OAAO,MAAM;AACnB,UAAI,MAAM;AACR,cAAM,SAAS,QAAQ,IAAI,MAAgB,UAAU,IAAI;AACzD,eAAO,OAAO,WAAW,aAAa,OAAO,KAAK,IAAI,IAAI;AAAA,MAC5D;AACA,UAAI,QAAQ,QAAQ,GAAG;AACrB,eAAO,UAAU,SAAsC;AACrD,gBAAM,cAAc,MAAM,MAAM,QAAQ;AACxC,gBAAM,SAAS,QAAQ,IAAI,aAAuB,UAAU,WAAW;AACvE,iBAAO,OAAO,MAAM,aAAa,IAAI;AAAA,QACvC;AAAA,MACF;AAEA,UAAI,OAAO,aAAa,YAAY,aAAa,UAAU,aAAa,cAAe,QAAO;AAC9F,YAAM,IAAI;AAAA,QACR,GAAG,KAAK,IAAI,QAAQ,wDAAwD,IAAI;AAAA,MAClF;AAAA,IACF;AAAA,IACA,IAAI,SAAS,UAAU;AACrB,YAAM,OAAO,MAAM;AACnB,aAAO,OAAO,YAAa,OAAkB,QAAQ,QAAQ;AAAA,IAC/D;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -0,0 +1,36 @@
1
+ /** A value that is initialized on first `acquire()` and readable once live. */
2
+ interface LazyValue<T> {
3
+ readonly current: T | null;
4
+ acquire(): Promise<T>;
5
+ }
6
+ declare class LazyRuntimeStage<T> implements LazyValue<T> {
7
+ private readonly init;
8
+ private readonly rollback;
9
+ private pending;
10
+ /** The bare `init()` attempt, settled (never rejecting) — excludes rollback. */
11
+ private attempt;
12
+ private value;
13
+ constructor(init: () => Promise<T>, rollback: (error: unknown) => Promise<never>);
14
+ /** The initialized value, or null while this stage has not been used yet. */
15
+ get current(): T | null;
16
+ /** True once initialization has been requested (whether or not it finished). */
17
+ get requested(): boolean;
18
+ /** Initialize exactly once; concurrent callers share the same promise. */
19
+ acquire(): Promise<T>;
20
+ /**
21
+ * Resolves once an in-flight initialization attempt has finished (either
22
+ * way); immediately when none was requested. Deliberately excludes the
23
+ * rollback so a disposer invoked *by* the rollback never waits on itself.
24
+ */
25
+ settled(): Promise<void>;
26
+ /** A derived view over one member of the initialized value. */
27
+ map<U>(project: (value: T) => U): LazyValue<U>;
28
+ }
29
+ /**
30
+ * A member whose runtime is initialized on first use. Async methods listed in
31
+ * `asyncMethods` initialize the runtime and then delegate; every other member
32
+ * delegates once initialized and fails with guidance before that.
33
+ */
34
+ declare function lazyMember<T extends object>(label: string, value: LazyValue<T>, asyncMethods: ReadonlyArray<keyof T & string>, hint: string): T;
35
+
36
+ export { LazyRuntimeStage, type LazyValue, lazyMember };
@@ -0,0 +1,36 @@
1
+ /** A value that is initialized on first `acquire()` and readable once live. */
2
+ interface LazyValue<T> {
3
+ readonly current: T | null;
4
+ acquire(): Promise<T>;
5
+ }
6
+ declare class LazyRuntimeStage<T> implements LazyValue<T> {
7
+ private readonly init;
8
+ private readonly rollback;
9
+ private pending;
10
+ /** The bare `init()` attempt, settled (never rejecting) — excludes rollback. */
11
+ private attempt;
12
+ private value;
13
+ constructor(init: () => Promise<T>, rollback: (error: unknown) => Promise<never>);
14
+ /** The initialized value, or null while this stage has not been used yet. */
15
+ get current(): T | null;
16
+ /** True once initialization has been requested (whether or not it finished). */
17
+ get requested(): boolean;
18
+ /** Initialize exactly once; concurrent callers share the same promise. */
19
+ acquire(): Promise<T>;
20
+ /**
21
+ * Resolves once an in-flight initialization attempt has finished (either
22
+ * way); immediately when none was requested. Deliberately excludes the
23
+ * rollback so a disposer invoked *by* the rollback never waits on itself.
24
+ */
25
+ settled(): Promise<void>;
26
+ /** A derived view over one member of the initialized value. */
27
+ map<U>(project: (value: T) => U): LazyValue<U>;
28
+ }
29
+ /**
30
+ * A member whose runtime is initialized on first use. Async methods listed in
31
+ * `asyncMethods` initialize the runtime and then delegate; every other member
32
+ * delegates once initialized and fails with guidance before that.
33
+ */
34
+ declare function lazyMember<T extends object>(label: string, value: LazyValue<T>, asyncMethods: ReadonlyArray<keyof T & string>, hint: string): T;
35
+
36
+ export { LazyRuntimeStage, type LazyValue, lazyMember };
@@ -0,0 +1,84 @@
1
+ import { TestWoodError } from "./test_wood.js";
2
+ class LazyRuntimeStage {
3
+ constructor(init, rollback) {
4
+ this.init = init;
5
+ this.rollback = rollback;
6
+ this.pending = null;
7
+ /** The bare `init()` attempt, settled (never rejecting) — excludes rollback. */
8
+ this.attempt = null;
9
+ this.value = null;
10
+ }
11
+ /** The initialized value, or null while this stage has not been used yet. */
12
+ get current() {
13
+ return this.value;
14
+ }
15
+ /** True once initialization has been requested (whether or not it finished). */
16
+ get requested() {
17
+ return this.pending !== null;
18
+ }
19
+ /** Initialize exactly once; concurrent callers share the same promise. */
20
+ acquire() {
21
+ if (!this.pending) {
22
+ const attempt = this.init();
23
+ this.attempt = attempt.then(() => void 0, () => void 0);
24
+ this.pending = attempt.then(
25
+ (value) => {
26
+ this.value = value;
27
+ return value;
28
+ },
29
+ (error) => this.rollback(error)
30
+ );
31
+ }
32
+ return this.pending;
33
+ }
34
+ /**
35
+ * Resolves once an in-flight initialization attempt has finished (either
36
+ * way); immediately when none was requested. Deliberately excludes the
37
+ * rollback so a disposer invoked *by* the rollback never waits on itself.
38
+ */
39
+ settled() {
40
+ return this.attempt ?? Promise.resolve();
41
+ }
42
+ /** A derived view over one member of the initialized value. */
43
+ map(project) {
44
+ const stage = this;
45
+ return {
46
+ get current() {
47
+ return stage.value === null ? null : project(stage.value);
48
+ },
49
+ acquire: async () => project(await stage.acquire())
50
+ };
51
+ }
52
+ }
53
+ function lazyMember(label, value, asyncMethods, hint) {
54
+ const isAsync = (property) => typeof property === "string" && asyncMethods.includes(property);
55
+ return new Proxy({}, {
56
+ get(_target, property) {
57
+ const live = value.current;
58
+ if (live) {
59
+ const member = Reflect.get(live, property, live);
60
+ return typeof member === "function" ? member.bind(live) : member;
61
+ }
62
+ if (isAsync(property)) {
63
+ return async (...args) => {
64
+ const initialized = await value.acquire();
65
+ const method = Reflect.get(initialized, property, initialized);
66
+ return method.apply(initialized, args);
67
+ };
68
+ }
69
+ if (typeof property === "symbol" || property === "then" || property === "constructor") return void 0;
70
+ throw new TestWoodError(
71
+ `${label}.${property} is not available before the runtime is initialized; ${hint}`
72
+ );
73
+ },
74
+ has(_target, property) {
75
+ const live = value.current;
76
+ return live ? property in live : isAsync(property);
77
+ }
78
+ });
79
+ }
80
+ export {
81
+ LazyRuntimeStage,
82
+ lazyMember
83
+ };
84
+ //# sourceMappingURL=wood_lazy_runtime.js.map