@noego/wood 0.4.10 → 0.5.0

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 (382) hide show
  1. package/README.md +118 -42
  2. package/dist/bridge/backend_bridge.cjs.map +1 -1
  3. package/dist/bridge/backend_bridge.js.map +1 -1
  4. package/dist/client/index.cjs +78 -21
  5. package/dist/client/index.cjs.map +1 -1
  6. package/dist/client/index.d.cts +18 -3
  7. package/dist/client/index.d.ts +18 -3
  8. package/dist/client/index.js +80 -22
  9. package/dist/client/index.js.map +1 -1
  10. package/dist/client/renderer_log_transport.cjs +151 -0
  11. package/dist/client/renderer_log_transport.cjs.map +1 -0
  12. package/dist/client/renderer_log_transport.d.cts +54 -0
  13. package/dist/client/renderer_log_transport.d.ts +54 -0
  14. package/dist/client/renderer_log_transport.js +123 -0
  15. package/dist/client/renderer_log_transport.js.map +1 -0
  16. package/dist/codegen/controller_registration_generator.cjs +13 -2
  17. package/dist/codegen/controller_registration_generator.cjs.map +1 -1
  18. package/dist/codegen/controller_registration_generator.d.cts +12 -1
  19. package/dist/codegen/controller_registration_generator.d.ts +12 -1
  20. package/dist/codegen/controller_registration_generator.js +10 -1
  21. package/dist/codegen/controller_registration_generator.js.map +1 -1
  22. package/dist/codegen/index.d.cts +1 -0
  23. package/dist/codegen/index.d.ts +1 -0
  24. package/dist/codegen/preload_generator.cjs +13 -6
  25. package/dist/codegen/preload_generator.cjs.map +1 -1
  26. package/dist/codegen/preload_generator.d.cts +11 -2
  27. package/dist/codegen/preload_generator.d.ts +11 -2
  28. package/dist/codegen/preload_generator.js +13 -6
  29. package/dist/codegen/preload_generator.js.map +1 -1
  30. package/dist/codegen/renderer_app_generator.cjs +78 -42
  31. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  32. package/dist/codegen/renderer_app_generator.js +78 -42
  33. package/dist/codegen/renderer_app_generator.js.map +1 -1
  34. package/dist/codegen/type_generator.cjs +1 -0
  35. package/dist/codegen/type_generator.cjs.map +1 -1
  36. package/dist/codegen/type_generator.js +1 -0
  37. package/dist/codegen/type_generator.js.map +1 -1
  38. package/dist/config/config_parser.cjs +95 -4
  39. package/dist/config/config_parser.cjs.map +1 -1
  40. package/dist/config/config_parser.d.cts +10 -2
  41. package/dist/config/config_parser.d.ts +10 -2
  42. package/dist/config/config_parser.js +92 -3
  43. package/dist/config/config_parser.js.map +1 -1
  44. package/dist/config/index.cjs +6 -2
  45. package/dist/config/index.cjs.map +1 -1
  46. package/dist/config/index.d.cts +2 -2
  47. package/dist/config/index.d.ts +2 -2
  48. package/dist/config/index.js +8 -2
  49. package/dist/config/index.js.map +1 -1
  50. package/dist/controller/controller_resolver.cjs +51 -3
  51. package/dist/controller/controller_resolver.cjs.map +1 -1
  52. package/dist/controller/controller_resolver.d.cts +27 -4
  53. package/dist/controller/controller_resolver.d.ts +27 -4
  54. package/dist/controller/controller_resolver.js +48 -2
  55. package/dist/controller/controller_resolver.js.map +1 -1
  56. package/dist/controller/index.cjs +6 -2
  57. package/dist/controller/index.cjs.map +1 -1
  58. package/dist/controller/index.d.cts +1 -1
  59. package/dist/controller/index.d.ts +1 -1
  60. package/dist/controller/index.js +4 -2
  61. package/dist/controller/index.js.map +1 -1
  62. package/dist/dev/wood_dev_server.cjs +4 -1
  63. package/dist/dev/wood_dev_server.cjs.map +1 -1
  64. package/dist/dev/wood_dev_server.js +5 -2
  65. package/dist/dev/wood_dev_server.js.map +1 -1
  66. package/dist/dev/yaml_watcher.cjs +1 -1
  67. package/dist/dev/yaml_watcher.cjs.map +1 -1
  68. package/dist/dev/yaml_watcher.d.cts +4 -0
  69. package/dist/dev/yaml_watcher.d.ts +4 -0
  70. package/dist/dev/yaml_watcher.js +1 -1
  71. package/dist/dev/yaml_watcher.js.map +1 -1
  72. package/dist/frontend/frontend_lifecycle.cjs +74 -0
  73. package/dist/frontend/frontend_lifecycle.cjs.map +1 -0
  74. package/dist/frontend/frontend_lifecycle.d.cts +50 -0
  75. package/dist/frontend/frontend_lifecycle.d.ts +50 -0
  76. package/dist/frontend/frontend_lifecycle.js +50 -0
  77. package/dist/frontend/frontend_lifecycle.js.map +1 -0
  78. package/dist/frontend/frontend_page.cjs +122 -0
  79. package/dist/frontend/frontend_page.cjs.map +1 -0
  80. package/dist/frontend/frontend_page.d.cts +62 -0
  81. package/dist/frontend/frontend_page.d.ts +62 -0
  82. package/dist/frontend/frontend_page.js +98 -0
  83. package/dist/frontend/frontend_page.js.map +1 -0
  84. package/dist/frontend/frontend_surface.cjs +249 -0
  85. package/dist/frontend/frontend_surface.cjs.map +1 -0
  86. package/dist/frontend/frontend_surface.d.cts +64 -0
  87. package/dist/frontend/frontend_surface.d.ts +64 -0
  88. package/dist/frontend/frontend_surface.js +224 -0
  89. package/dist/frontend/frontend_surface.js.map +1 -0
  90. package/dist/frontend/index.cjs +52 -0
  91. package/dist/frontend/index.cjs.map +1 -0
  92. package/dist/frontend/index.d.cts +10 -0
  93. package/dist/frontend/index.d.ts +10 -0
  94. package/dist/frontend/index.js +19 -0
  95. package/dist/frontend/index.js.map +1 -0
  96. package/dist/frontend/page_catalog.cjs +135 -0
  97. package/dist/frontend/page_catalog.cjs.map +1 -0
  98. package/dist/frontend/page_catalog.d.cts +56 -0
  99. package/dist/frontend/page_catalog.d.ts +56 -0
  100. package/dist/frontend/page_catalog.js +108 -0
  101. package/dist/frontend/page_catalog.js.map +1 -0
  102. package/dist/frontend/tracked_transport.cjs +55 -0
  103. package/dist/frontend/tracked_transport.cjs.map +1 -0
  104. package/dist/frontend/tracked_transport.d.cts +36 -0
  105. package/dist/frontend/tracked_transport.d.ts +36 -0
  106. package/dist/frontend/tracked_transport.js +31 -0
  107. package/dist/frontend/tracked_transport.js.map +1 -0
  108. package/dist/frontend/work_registry.cjs +153 -0
  109. package/dist/frontend/work_registry.cjs.map +1 -0
  110. package/dist/frontend/work_registry.d.cts +70 -0
  111. package/dist/frontend/work_registry.d.ts +70 -0
  112. package/dist/frontend/work_registry.js +128 -0
  113. package/dist/frontend/work_registry.js.map +1 -0
  114. package/dist/index.cjs +24 -0
  115. package/dist/index.cjs.map +1 -1
  116. package/dist/index.d.cts +10 -7
  117. package/dist/index.d.ts +10 -7
  118. package/dist/index.js +27 -4
  119. package/dist/index.js.map +1 -1
  120. package/dist/main/index.cjs +10 -0
  121. package/dist/main/index.cjs.map +1 -1
  122. package/dist/main/index.d.cts +14 -2
  123. package/dist/main/index.d.ts +14 -2
  124. package/dist/main/index.js +16 -0
  125. package/dist/main/index.js.map +1 -1
  126. package/dist/middleware/middleware_resolver.cjs +22 -1
  127. package/dist/middleware/middleware_resolver.cjs.map +1 -1
  128. package/dist/middleware/middleware_resolver.d.cts +13 -1
  129. package/dist/middleware/middleware_resolver.d.ts +13 -1
  130. package/dist/middleware/middleware_resolver.js +22 -1
  131. package/dist/middleware/middleware_resolver.js.map +1 -1
  132. package/dist/navigation/index.cjs +3 -0
  133. package/dist/navigation/index.cjs.map +1 -1
  134. package/dist/navigation/index.d.cts +25 -1
  135. package/dist/navigation/index.d.ts +25 -1
  136. package/dist/navigation/index.js +2 -0
  137. package/dist/navigation/index.js.map +1 -1
  138. package/dist/{middleware/__fixtures__/middleware/auth.cjs → navigation/layout_preservation.cjs} +18 -20
  139. package/dist/navigation/layout_preservation.cjs.map +1 -0
  140. package/dist/navigation/layout_preservation.d.cts +2 -0
  141. package/dist/navigation/layout_preservation.d.ts +2 -0
  142. package/dist/navigation/layout_preservation.js +18 -0
  143. package/dist/navigation/layout_preservation.js.map +1 -0
  144. package/dist/parser/index.cjs +2 -0
  145. package/dist/parser/index.cjs.map +1 -1
  146. package/dist/parser/index.d.cts +1 -1
  147. package/dist/parser/index.d.ts +1 -1
  148. package/dist/parser/index.js +2 -1
  149. package/dist/parser/index.js.map +1 -1
  150. package/dist/parser/operations_parser.cjs +6 -0
  151. package/dist/parser/operations_parser.cjs.map +1 -1
  152. package/dist/parser/operations_parser.d.cts +9 -2
  153. package/dist/parser/operations_parser.d.ts +9 -2
  154. package/dist/parser/operations_parser.js +5 -0
  155. package/dist/parser/operations_parser.js.map +1 -1
  156. package/dist/parser/views_parser.cjs.map +1 -1
  157. package/dist/parser/views_parser.js.map +1 -1
  158. package/dist/router/index.d.cts +1 -0
  159. package/dist/router/index.d.ts +1 -0
  160. package/dist/router/wood_router.cjs +35 -3
  161. package/dist/router/wood_router.cjs.map +1 -1
  162. package/dist/router/wood_router.d.cts +15 -0
  163. package/dist/router/wood_router.d.ts +15 -0
  164. package/dist/router/wood_router.js +36 -4
  165. package/dist/router/wood_router.js.map +1 -1
  166. package/dist/runtime/get_container.cjs +9 -34
  167. package/dist/runtime/get_container.cjs.map +1 -1
  168. package/dist/runtime/get_container.d.cts +24 -4
  169. package/dist/runtime/get_container.d.ts +24 -4
  170. package/dist/runtime/get_container.js +8 -23
  171. package/dist/runtime/get_container.js.map +1 -1
  172. package/dist/runtime/index.cjs +9 -0
  173. package/dist/runtime/index.cjs.map +1 -1
  174. package/dist/runtime/index.d.cts +5 -2
  175. package/dist/runtime/index.d.ts +5 -2
  176. package/dist/runtime/index.js +10 -1
  177. package/dist/runtime/index.js.map +1 -1
  178. package/dist/runtime/renderer_config_extension.cjs +50 -0
  179. package/dist/runtime/renderer_config_extension.cjs.map +1 -0
  180. package/dist/runtime/renderer_config_extension.d.cts +19 -0
  181. package/dist/runtime/renderer_config_extension.d.ts +19 -0
  182. package/dist/runtime/renderer_config_extension.js +24 -0
  183. package/dist/runtime/renderer_config_extension.js.map +1 -0
  184. package/dist/runtime/renderer_log_batch_receiver.cjs +73 -0
  185. package/dist/runtime/renderer_log_batch_receiver.cjs.map +1 -0
  186. package/dist/runtime/renderer_log_batch_receiver.d.cts +29 -0
  187. package/dist/runtime/renderer_log_batch_receiver.d.ts +29 -0
  188. package/dist/runtime/renderer_log_batch_receiver.js +48 -0
  189. package/dist/runtime/renderer_log_batch_receiver.js.map +1 -0
  190. package/dist/runtime/renderer_trace_batch_receiver_registry.d.cts +1 -1
  191. package/dist/runtime/renderer_trace_batch_receiver_registry.d.ts +1 -1
  192. package/dist/runtime/runtime.cjs +14 -0
  193. package/dist/runtime/runtime.cjs.map +1 -1
  194. package/dist/runtime/runtime.d.cts +2 -1
  195. package/dist/runtime/runtime.d.ts +2 -1
  196. package/dist/runtime/runtime.js +14 -0
  197. package/dist/runtime/runtime.js.map +1 -1
  198. package/dist/stick/index.cjs.map +1 -1
  199. package/dist/stick/index.js.map +1 -1
  200. package/dist/testing/browser.cjs +104 -23
  201. package/dist/testing/browser.cjs.map +1 -1
  202. package/dist/testing/browser.d.cts +8 -3
  203. package/dist/testing/browser.d.ts +8 -3
  204. package/dist/testing/browser.js +102 -22
  205. package/dist/testing/browser.js.map +1 -1
  206. package/dist/testing/create_controller_harness.cjs +28 -8
  207. package/dist/testing/create_controller_harness.cjs.map +1 -1
  208. package/dist/testing/create_controller_harness.d.cts +17 -2
  209. package/dist/testing/create_controller_harness.d.ts +17 -2
  210. package/dist/testing/create_controller_harness.js +28 -8
  211. package/dist/testing/create_controller_harness.js.map +1 -1
  212. package/dist/testing/create_router_bridge.d.cts +1 -0
  213. package/dist/testing/create_router_bridge.d.ts +1 -0
  214. package/dist/testing/create_test_router.d.cts +1 -0
  215. package/dist/testing/create_test_router.d.ts +1 -0
  216. package/dist/testing/index.cjs +31 -0
  217. package/dist/testing/index.cjs.map +1 -1
  218. package/dist/testing/index.d.cts +20 -5
  219. package/dist/testing/index.d.ts +20 -5
  220. package/dist/testing/index.js +19 -1
  221. package/dist/testing/index.js.map +1 -1
  222. package/dist/{controller/__fixtures__/controllers/not_a_class.cjs → testing/test_wood.cjs} +15 -7
  223. package/dist/testing/test_wood.cjs.map +1 -0
  224. package/dist/testing/test_wood.d.cts +16 -0
  225. package/dist/testing/test_wood.d.ts +16 -0
  226. package/dist/testing/test_wood.js +12 -0
  227. package/dist/testing/test_wood.js.map +1 -0
  228. package/dist/testing/test_wood_config.cjs +272 -0
  229. package/dist/testing/test_wood_config.cjs.map +1 -0
  230. package/dist/testing/test_wood_config.d.cts +111 -0
  231. package/dist/testing/test_wood_config.d.ts +111 -0
  232. package/dist/testing/test_wood_config.js +247 -0
  233. package/dist/testing/test_wood_config.js.map +1 -0
  234. package/dist/testing/wood_config_source.cjs +146 -0
  235. package/dist/testing/wood_config_source.cjs.map +1 -0
  236. package/dist/testing/wood_config_source.d.cts +36 -0
  237. package/dist/testing/wood_config_source.d.ts +36 -0
  238. package/dist/testing/wood_config_source.js +112 -0
  239. package/dist/testing/wood_config_source.js.map +1 -0
  240. package/dist/testing/wood_env_renderer.cjs +210 -0
  241. package/dist/testing/wood_env_renderer.cjs.map +1 -0
  242. package/dist/testing/wood_env_renderer.d.cts +74 -0
  243. package/dist/testing/wood_env_renderer.d.ts +74 -0
  244. package/dist/testing/wood_env_renderer.js +188 -0
  245. package/dist/testing/wood_env_renderer.js.map +1 -0
  246. package/dist/testing/wood_test_selection.cjs +94 -0
  247. package/dist/testing/wood_test_selection.cjs.map +1 -0
  248. package/dist/testing/wood_test_selection.d.cts +39 -0
  249. package/dist/testing/wood_test_selection.d.ts +39 -0
  250. package/dist/testing/wood_test_selection.js +68 -0
  251. package/dist/testing/wood_test_selection.js.map +1 -0
  252. package/dist/trace-testing/index.d.cts +1 -2
  253. package/dist/trace-testing/index.d.ts +1 -2
  254. package/dist/trace-testing/trace_recorder.cjs +1 -1
  255. package/dist/trace-testing/trace_recorder.cjs.map +1 -1
  256. package/dist/trace-testing/trace_recorder.d.cts +1 -2
  257. package/dist/trace-testing/trace_recorder.d.ts +1 -2
  258. package/dist/trace-testing/trace_recorder.js +1 -1
  259. package/dist/trace-testing/trace_recorder.js.map +1 -1
  260. package/dist/trace-transport/index.d.cts +1 -1
  261. package/dist/trace-transport/index.d.ts +1 -1
  262. package/dist/{trace_hub-Cob54qRx.d.cts → trace_hub-Cpo_nVze.d.cts} +21 -1
  263. package/dist/{trace_hub-Cob54qRx.d.ts → trace_hub-Cpo_nVze.d.ts} +21 -1
  264. package/dist/tracing/index.cjs +9 -2
  265. package/dist/tracing/index.cjs.map +1 -1
  266. package/dist/tracing/index.d.cts +3 -2
  267. package/dist/tracing/index.d.ts +3 -2
  268. package/dist/tracing/index.js +7 -1
  269. package/dist/tracing/index.js.map +1 -1
  270. package/dist/tracing/renderer_trace_batch_host.d.cts +1 -1
  271. package/dist/tracing/renderer_trace_batch_host.d.ts +1 -1
  272. package/dist/tracing/renderer_trace_batch_transport.cjs +48 -6
  273. package/dist/tracing/renderer_trace_batch_transport.cjs.map +1 -1
  274. package/dist/tracing/renderer_trace_batch_transport.d.cts +17 -1
  275. package/dist/tracing/renderer_trace_batch_transport.d.ts +17 -1
  276. package/dist/tracing/renderer_trace_batch_transport.js +48 -6
  277. package/dist/tracing/renderer_trace_batch_transport.js.map +1 -1
  278. package/dist/tracing/trace_batch_adapter.cjs +110 -0
  279. package/dist/tracing/trace_batch_adapter.cjs.map +1 -0
  280. package/dist/tracing/trace_batch_adapter.d.cts +36 -0
  281. package/dist/tracing/trace_batch_adapter.d.ts +36 -0
  282. package/dist/tracing/trace_batch_adapter.js +86 -0
  283. package/dist/tracing/trace_batch_adapter.js.map +1 -0
  284. package/dist/tracing/trace_commit_writer.d.cts +1 -1
  285. package/dist/tracing/trace_commit_writer.d.ts +1 -1
  286. package/dist/tracing/trace_hub.cjs +87 -20
  287. package/dist/tracing/trace_hub.cjs.map +1 -1
  288. package/dist/tracing/trace_hub.d.cts +1 -1
  289. package/dist/tracing/trace_hub.d.ts +1 -1
  290. package/dist/tracing/trace_hub.js +85 -20
  291. package/dist/tracing/trace_hub.js.map +1 -1
  292. package/dist/tracing/trace_provider.cjs +21 -6
  293. package/dist/tracing/trace_provider.cjs.map +1 -1
  294. package/dist/tracing/trace_provider.d.cts +13 -4
  295. package/dist/tracing/trace_provider.d.ts +13 -4
  296. package/dist/tracing/trace_provider.js +22 -7
  297. package/dist/tracing/trace_provider.js.map +1 -1
  298. package/dist/tracing/types/wood_trace_batch.types.cjs.map +1 -1
  299. package/dist/tracing/types/wood_trace_batch.types.d.cts +15 -1
  300. package/dist/tracing/types/wood_trace_batch.types.d.ts +15 -1
  301. package/dist/tracing/types/wood_trace_sink.types.d.cts +1 -1
  302. package/dist/tracing/types/wood_trace_sink.types.d.ts +1 -1
  303. package/dist/tracing/wood_trace_batch_contract.cjs +2 -0
  304. package/dist/tracing/wood_trace_batch_contract.cjs.map +1 -1
  305. package/dist/tracing/wood_trace_batch_contract.d.cts +1 -1
  306. package/dist/tracing/wood_trace_batch_contract.d.ts +1 -1
  307. package/dist/tracing/wood_trace_batch_contract.js +2 -0
  308. package/dist/tracing/wood_trace_batch_contract.js.map +1 -1
  309. package/dist/transport/build_transport_bridge.cjs +4 -2
  310. package/dist/transport/build_transport_bridge.cjs.map +1 -1
  311. package/dist/transport/build_transport_bridge.d.cts +7 -1
  312. package/dist/transport/build_transport_bridge.d.ts +7 -1
  313. package/dist/transport/build_transport_bridge.js +2 -1
  314. package/dist/transport/build_transport_bridge.js.map +1 -1
  315. package/dist/transport/in_process_router_transport.d.cts +1 -0
  316. package/dist/transport/in_process_router_transport.d.ts +1 -0
  317. package/dist/transport/index.d.cts +1 -0
  318. package/dist/transport/index.d.ts +1 -0
  319. package/dist/types/config.cjs.map +1 -1
  320. package/dist/types/config.d.cts +47 -1
  321. package/dist/types/config.d.ts +47 -1
  322. package/dist/types/context.cjs +12 -0
  323. package/dist/types/context.cjs.map +1 -1
  324. package/dist/types/context.d.cts +27 -1
  325. package/dist/types/context.d.ts +27 -1
  326. package/dist/types/context.js +4 -0
  327. package/dist/types/context.js.map +1 -1
  328. package/dist/types/index.d.cts +2 -2
  329. package/dist/types/index.d.ts +2 -2
  330. package/dist/types/operations.cjs.map +1 -1
  331. package/dist/types/operations.d.cts +2 -0
  332. package/dist/types/operations.d.ts +2 -0
  333. package/docs/axe.md +706 -0
  334. package/package.json +13 -5
  335. package/src/components/NavigationShell.scopes.browser.test.ts +363 -0
  336. package/src/components/NavigationShell.svelte +126 -49
  337. package/dist/controller/__fixtures__/controllers/chat.cjs +0 -32
  338. package/dist/controller/__fixtures__/controllers/chat.cjs.map +0 -1
  339. package/dist/controller/__fixtures__/controllers/chat.d.cts +0 -11
  340. package/dist/controller/__fixtures__/controllers/chat.d.ts +0 -11
  341. package/dist/controller/__fixtures__/controllers/chat.js +0 -12
  342. package/dist/controller/__fixtures__/controllers/chat.js.map +0 -1
  343. package/dist/controller/__fixtures__/controllers/not_a_class.cjs.map +0 -1
  344. package/dist/controller/__fixtures__/controllers/not_a_class.d.cts +0 -3
  345. package/dist/controller/__fixtures__/controllers/not_a_class.d.ts +0 -3
  346. package/dist/controller/__fixtures__/controllers/not_a_class.js +0 -5
  347. package/dist/controller/__fixtures__/controllers/not_a_class.js.map +0 -1
  348. package/dist/controller/__fixtures__/controllers/settings.controller.cjs +0 -32
  349. package/dist/controller/__fixtures__/controllers/settings.controller.cjs.map +0 -1
  350. package/dist/controller/__fixtures__/controllers/settings.controller.d.cts +0 -12
  351. package/dist/controller/__fixtures__/controllers/settings.controller.d.ts +0 -12
  352. package/dist/controller/__fixtures__/controllers/settings.controller.js +0 -12
  353. package/dist/controller/__fixtures__/controllers/settings.controller.js.map +0 -1
  354. package/dist/controller/__fixtures__/controllers/test.cjs +0 -29
  355. package/dist/controller/__fixtures__/controllers/test.cjs.map +0 -1
  356. package/dist/controller/__fixtures__/controllers/test.d.cts +0 -7
  357. package/dist/controller/__fixtures__/controllers/test.d.ts +0 -7
  358. package/dist/controller/__fixtures__/controllers/test.js +0 -9
  359. package/dist/controller/__fixtures__/controllers/test.js.map +0 -1
  360. package/dist/loader/__fixtures__/layouts/app.load.cjs +0 -27
  361. package/dist/loader/__fixtures__/layouts/app.load.cjs.map +0 -1
  362. package/dist/loader/__fixtures__/layouts/app.load.d.cts +0 -5
  363. package/dist/loader/__fixtures__/layouts/app.load.d.ts +0 -5
  364. package/dist/loader/__fixtures__/layouts/app.load.js +0 -7
  365. package/dist/loader/__fixtures__/layouts/app.load.js.map +0 -1
  366. package/dist/loader/__fixtures__/views/test/page.load.cjs +0 -27
  367. package/dist/loader/__fixtures__/views/test/page.load.cjs.map +0 -1
  368. package/dist/loader/__fixtures__/views/test/page.load.d.cts +0 -6
  369. package/dist/loader/__fixtures__/views/test/page.load.d.ts +0 -6
  370. package/dist/loader/__fixtures__/views/test/page.load.js +0 -7
  371. package/dist/loader/__fixtures__/views/test/page.load.js.map +0 -1
  372. package/dist/middleware/__fixtures__/middleware/auth.cjs.map +0 -1
  373. package/dist/middleware/__fixtures__/middleware/auth.d.cts +0 -6
  374. package/dist/middleware/__fixtures__/middleware/auth.d.ts +0 -6
  375. package/dist/middleware/__fixtures__/middleware/auth.js +0 -19
  376. package/dist/middleware/__fixtures__/middleware/auth.js.map +0 -1
  377. package/dist/middleware/__fixtures__/middleware/sequence.cjs +0 -47
  378. package/dist/middleware/__fixtures__/middleware/sequence.cjs.map +0 -1
  379. package/dist/middleware/__fixtures__/middleware/sequence.d.cts +0 -7
  380. package/dist/middleware/__fixtures__/middleware/sequence.d.ts +0 -7
  381. package/dist/middleware/__fixtures__/middleware/sequence.js +0 -21
  382. package/dist/middleware/__fixtures__/middleware/sequence.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noego/wood",
3
- "version": "0.4.10",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -125,20 +125,22 @@
125
125
  "prepublishOnly": "npm run build && npm run typecheck && npm test"
126
126
  },
127
127
  "dependencies": {
128
- "@noego/ioc": "^0.3.1",
129
- "@noego/logger": "^0.0.4",
128
+ "@noego/ioc": "^0.7.0",
129
+ "@noego/logger": "^0.2.0",
130
130
  "@noego/stitch": "^1.0.3",
131
- "@noego/telemetry": "^0.0.1",
132
- "@noego/trace": "^0.0.6",
131
+ "@noego/telemetry": "^0.1.0",
132
+ "@noego/trace": "^0.2.0",
133
133
  "ajv": "^8.x",
134
134
  "ajv-formats": "^3.x",
135
135
  "chokidar": "^4.x",
136
136
  "glob": "^11.x",
137
137
  "js-yaml": "^4.x",
138
138
  "path-to-regexp": "^8.x",
139
+ "reflect-metadata": "^0.2.2",
139
140
  "rxjs": "^7.x"
140
141
  },
141
142
  "peerDependencies": {
143
+ "@noego/testing": ">=0.5.0",
142
144
  "@sveltejs/vite-plugin-svelte": "^4.0.0",
143
145
  "electron": ">=28.0.0",
144
146
  "playwright": "^1.40.0",
@@ -150,12 +152,18 @@
150
152
  },
151
153
  "playwright": {
152
154
  "optional": true
155
+ },
156
+ "@noego/testing": {
157
+ "optional": true
153
158
  }
154
159
  },
155
160
  "devDependencies": {
161
+ "@noego/testing": "^0.5.0",
156
162
  "@sveltejs/vite-plugin-svelte": "^4.0.4",
163
+ "@types/better-sqlite3": "^9.6.0",
157
164
  "@types/js-yaml": "^4.x",
158
165
  "@types/node": "^20.10.4",
166
+ "better-sqlite3": "^12.11.1",
159
167
  "electron-builder": "^25.x",
160
168
  "electron-vite": "^2.x",
161
169
  "esbuild": "^0.25.x",
@@ -0,0 +1,363 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, describe, expect, test } from 'vitest';
5
+ import { browser } from '../testing/browser';
6
+
7
+ type ScopeReproSnapshot = {
8
+ scopeDisposeCounts: Record<string, number>;
9
+ controllerDisposeCounts: Record<string, number>;
10
+ setCreateCalls: string[];
11
+ createdIds: string[];
12
+ rendered: string;
13
+ pageErrors: string[];
14
+ };
15
+
16
+ describe('NavigationShell renderer scope ownership', () => {
17
+ const harnesses: Array<{ destroy(): Promise<void> }> = [];
18
+ const tempRoots: string[] = [];
19
+
20
+ afterEach(async () => {
21
+ while (harnesses.length > 0) {
22
+ await harnesses.pop()?.destroy();
23
+ }
24
+ while (tempRoots.length > 0) {
25
+ rmSync(tempRoots.pop()!, { recursive: true, force: true });
26
+ }
27
+ });
28
+
29
+ test(
30
+ 'disposes page scopes, preserves layout scopes, and retains borrowed page scopes across transitions',
31
+ async () => {
32
+ const woodRoot = path.resolve(__dirname, '../..');
33
+ const rootDir = mkdtempSync(path.join(os.tmpdir(), 'wood-navigation-shell-scopes-repro-'));
34
+ tempRoots.push(rootDir);
35
+ mkdirSync(path.join(rootDir, 'ui'), { recursive: true });
36
+
37
+ writeFileSync(
38
+ path.join(rootDir, 'ui', 'ReproView.svelte'),
39
+ `<script lang="ts">
40
+ let { data = {} } = $props<{ data?: { id?: string } }>();
41
+ </script>
42
+
43
+ <main data-repro-view>controller={data.id ?? 'missing'}</main>`,
44
+ );
45
+
46
+ writeFileSync(
47
+ path.join(rootDir, 'ui', 'ReproLayout.svelte'),
48
+ `<script lang="ts">
49
+ import type { Snippet } from 'svelte';
50
+ let { children }: { children: Snippet } = $props();
51
+ </script>
52
+
53
+ <section data-repro-layout>{@render children()}</section>`,
54
+ );
55
+
56
+ writeFileSync(
57
+ path.join(rootDir, 'ui', 'NavigationShellScopesRepro.svelte'),
58
+ `<script lang="ts">
59
+ import '/@fs/${woodRoot}/src/client/global_polyfill.ts';
60
+ import NavigationShell from '/@fs/${woodRoot}/src/components/NavigationShell.svelte';
61
+ import { getNavigation } from '/@fs/${woodRoot}/src/navigation/index.ts';
62
+ import ReproLayout from './ReproLayout.svelte';
63
+ import ReproView from './ReproView.svelte';
64
+
65
+ type ScopeReproState = {
66
+ scopeDisposeCounts: Record<string, number>;
67
+ controllerDisposeCounts: Record<string, number>;
68
+ setCreateCalls: string[];
69
+ createdIds: string[];
70
+ };
71
+
72
+ const state: ScopeReproState = {
73
+ scopeDisposeCounts: {},
74
+ controllerDisposeCounts: {},
75
+ setCreateCalls: [],
76
+ createdIds: [],
77
+ };
78
+ (window as unknown as { __WOOD_SCOPE_REPRO__: ScopeReproState })
79
+ .__WOOD_SCOPE_REPRO__ = state;
80
+
81
+ function makeScope(id: string) {
82
+ state.scopeDisposeCounts[id] = 0;
83
+ state.createdIds.push(id);
84
+ return {
85
+ id,
86
+ disposed: false,
87
+ async dispose() {
88
+ this.disposed = true;
89
+ state.scopeDisposeCounts[id] += 1;
90
+ },
91
+ };
92
+ }
93
+
94
+ function makeController(id: string) {
95
+ state.controllerDisposeCounts[id] = 0;
96
+ state.createdIds.push(id);
97
+ return {
98
+ data: { id },
99
+ async dispose() {
100
+ state.controllerDisposeCounts[id] += 1;
101
+ },
102
+ };
103
+ }
104
+
105
+ function makeEntry(
106
+ pageName: string,
107
+ layoutControllerName: string | null,
108
+ preserve: boolean,
109
+ ) {
110
+ return {
111
+ key: \`main.\${pageName}\`,
112
+ windowName: 'main',
113
+ pageName,
114
+ layoutPaths: layoutControllerName ? ['ui/ReproLayout.svelte'] : [],
115
+ layoutControllers: layoutControllerName ? [layoutControllerName] : [],
116
+ layoutPreserve: layoutControllerName ? [preserve] : [],
117
+ view: ReproView,
118
+ layouts: layoutControllerName ? [ReproLayout] : [],
119
+ };
120
+ }
121
+
122
+ // A/B share a preservable layout L1; C has no layouts; D/E share a
123
+ // NON-preservable layout L2 (page-scope owned); F has no layouts.
124
+ const entries = [
125
+ makeEntry('A', 'L1', true),
126
+ makeEntry('B', 'L1', true),
127
+ makeEntry('C', null, false),
128
+ makeEntry('D', 'L2', false),
129
+ makeEntry('E', 'L2', false),
130
+ makeEntry('F', null, false),
131
+ ];
132
+ const manifest = Object.fromEntries(entries.map((entry) => [entry.key, entry]));
133
+
134
+ // Mirrors the generated createControllerSet factory in
135
+ // src/codegen/renderer_app_generator.ts: one page scope per set, a
136
+ // dedicated scope per preservable layout, null (page-scope owned)
137
+ // otherwise, and undefined/null placeholders below preservedDepth.
138
+ function buildControllerSet(entry: any, preservedDepth = 0) {
139
+ state.setCreateCalls.push(\`\${entry.pageName}:\${preservedDepth}\`);
140
+ const pageScope = makeScope(\`scope:\${entry.pageName}\`);
141
+ const layoutControllers: unknown[] = [];
142
+ const layoutScopes: Array<unknown> = [];
143
+ for (let i = 0; i < preservedDepth; i += 1) {
144
+ layoutControllers.push(undefined);
145
+ layoutScopes.push(null);
146
+ }
147
+ for (let i = preservedDepth; i < entry.layoutControllers.length; i += 1) {
148
+ const name = entry.layoutControllers[i];
149
+ const preservable = entry.layoutPreserve?.[i] === true;
150
+ layoutControllers.push(makeController(\`ctrl:\${name}\`));
151
+ layoutScopes.push(preservable ? makeScope(\`scope:\${name}\`) : null);
152
+ }
153
+ const controller = makeController(\`ctrl:\${entry.pageName}\`);
154
+ return { controller, layoutControllers, pageScope, layoutScopes };
155
+ }
156
+
157
+ const initialControllerSet = buildControllerSet(entries[0], 0);
158
+
159
+ async function createControllerSetFn(entry: any, preservedDepth = 0) {
160
+ return buildControllerSet(entry, preservedDepth);
161
+ }
162
+
163
+ function getViewManifestEntryFn(viewKey: string) {
164
+ return manifest[viewKey as keyof typeof manifest] ?? null;
165
+ }
166
+
167
+ const navigation = getNavigation();
168
+ navigation.replace(entries[0].key, {}, {});
169
+ (window as unknown as {
170
+ __WOOD_SCOPE_REPRO_GO__: (pageName: string, options?: Record<string, unknown>) => void;
171
+ }).__WOOD_SCOPE_REPRO_GO__ = (pageName: string, options?: Record<string, unknown>) => {
172
+ navigation.go(\`main.\${pageName}\`, {}, {}, options ?? {});
173
+ };
174
+ </script>
175
+
176
+ <NavigationShell
177
+ {manifest}
178
+ initialEntry={entries[0]}
179
+ {initialControllerSet}
180
+ {createControllerSetFn}
181
+ {getViewManifestEntryFn}
182
+ />`,
183
+ );
184
+
185
+ const pageErrors: string[] = [];
186
+ const consoleErrors: string[] = [];
187
+ const harness = await browser.createHarness({
188
+ rootDir: woodRoot,
189
+ browser: { headless: true },
190
+ page: {
191
+ beforeMount({ page }) {
192
+ page.on('pageerror', (error: Error) => pageErrors.push(error.message));
193
+ page.on('console', (message: { type(): string; text(): string }) => {
194
+ if (message.type() === 'error') consoleErrors.push(message.text());
195
+ });
196
+ },
197
+ },
198
+ });
199
+ harnesses.push(harness);
200
+ const mounted = await harness.mountComponent(
201
+ path.join(rootDir, 'ui', 'NavigationShellScopesRepro.svelte'),
202
+ );
203
+ await mounted.locator('[data-repro-view]').waitFor({ state: 'visible', timeout: 5_000 }).catch(
204
+ async (error: unknown) => {
205
+ throw new Error(
206
+ `${error instanceof Error ? error.message : String(error)}\n` +
207
+ `Page errors: ${pageErrors.join(' | ')}\n` +
208
+ `Console errors: ${consoleErrors.join(' | ')}\n` +
209
+ `HTML: ${await mounted.page.content()}`,
210
+ );
211
+ },
212
+ );
213
+
214
+ const go = async (pageName: string, options?: Record<string, unknown>) => {
215
+ await mounted.page.evaluate(
216
+ ({ pageName, options }: { pageName: string; options?: Record<string, unknown> }) => {
217
+ (window as unknown as {
218
+ __WOOD_SCOPE_REPRO_GO__: (pageName: string, options?: Record<string, unknown>) => void;
219
+ }).__WOOD_SCOPE_REPRO_GO__(pageName, options);
220
+ },
221
+ { pageName, options },
222
+ );
223
+ };
224
+
225
+ const renderedController = async () =>
226
+ (await mounted.locator('[data-repro-view]').textContent())?.replace(/\s+/g, ' ').trim();
227
+
228
+ const snapshot = async (): Promise<ScopeReproSnapshot> => {
229
+ const result = await mounted.evaluate<Omit<ScopeReproSnapshot, 'rendered' | 'pageErrors'> & { rendered: string }>(() => {
230
+ const state = (window as unknown as {
231
+ __WOOD_SCOPE_REPRO__: Omit<ScopeReproSnapshot, 'rendered' | 'pageErrors'>;
232
+ }).__WOOD_SCOPE_REPRO__;
233
+ return {
234
+ scopeDisposeCounts: { ...state.scopeDisposeCounts },
235
+ controllerDisposeCounts: { ...state.controllerDisposeCounts },
236
+ setCreateCalls: [...state.setCreateCalls],
237
+ createdIds: [...state.createdIds],
238
+ rendered: document.querySelector('[data-repro-view]')?.textContent?.replace(/\s+/g, ' ').trim() ?? '',
239
+ };
240
+ });
241
+ return { ...result, pageErrors: [...pageErrors] };
242
+ };
243
+
244
+ await expect.poll(renderedController).toBe('controller=ctrl:A');
245
+
246
+ // --- Behavior 1: A -> B with a preserved layout (layoutPreserve [true],
247
+ // default options): A's page scope disposed, layout scope kept, layout
248
+ // controller carried over (factory called with preservedDepth 1, no
249
+ // ctrl:L1 re-creation, no ctrl:L1/scope:L1 disposal).
250
+ await go('B');
251
+ await expect.poll(renderedController).toBe('controller=ctrl:B');
252
+ const afterB = await snapshot();
253
+ expect(afterB.pageErrors, 'no page errors after A -> B').toEqual([]);
254
+ expect(afterB.setCreateCalls, 'B set built with preservedDepth 1').toEqual(['A:0', 'B:1']);
255
+ expect(
256
+ afterB.createdIds.filter((id) => id === 'ctrl:L1'),
257
+ 'preserved layout controller must not be re-created',
258
+ ).toHaveLength(1);
259
+ expect(afterB.scopeDisposeCounts, 'A page scope disposed; L1 layout scope preserved').toEqual({
260
+ 'scope:A': 1,
261
+ 'scope:L1': 0,
262
+ 'scope:B': 0,
263
+ });
264
+ expect(afterB.controllerDisposeCounts, 'A page controller disposed; L1 layout controller preserved').toEqual({
265
+ 'ctrl:A': 1,
266
+ 'ctrl:L1': 0,
267
+ 'ctrl:B': 0,
268
+ });
269
+
270
+ // --- Behavior 2: B -> C drops the preserved layout: the layout scope
271
+ // and layout controller are disposed along with B's page scope.
272
+ await go('C');
273
+ await expect.poll(renderedController).toBe('controller=ctrl:C');
274
+ const afterC = await snapshot();
275
+ expect(afterC.pageErrors, 'no page errors after B -> C').toEqual([]);
276
+ expect(afterC.scopeDisposeCounts, 'dropping the layout disposes its scope and B page scope').toEqual({
277
+ 'scope:A': 1,
278
+ 'scope:L1': 1,
279
+ 'scope:B': 1,
280
+ 'scope:C': 0,
281
+ });
282
+ expect(afterC.controllerDisposeCounts).toEqual({
283
+ 'ctrl:A': 1,
284
+ 'ctrl:L1': 1,
285
+ 'ctrl:B': 1,
286
+ 'ctrl:C': 0,
287
+ });
288
+
289
+ // --- Behavior 3 setup: C -> D. D's layout L2 is non-preservable, so its
290
+ // controller lives in D's page scope (layoutScopes entry null).
291
+ await go('D');
292
+ await expect.poll(renderedController).toBe('controller=ctrl:D');
293
+ const afterD = await snapshot();
294
+ expect(afterD.pageErrors, 'no page errors after C -> D').toEqual([]);
295
+ expect(afterD.scopeDisposeCounts['scope:C'], 'C page scope disposed').toBe(1);
296
+ expect(afterD.controllerDisposeCounts['ctrl:C'], 'C page controller disposed').toBe(1);
297
+ expect(afterD.createdIds, 'no dedicated scope created for a non-preservable layout')
298
+ .not.toContain('scope:L2');
299
+
300
+ // --- Behavior 3: D -> E with forced { preserveLayouts: true }. The
301
+ // borrowed layout controller (owned by D's page scope) is carried
302
+ // forward, so D's page scope must be RETAINED, not disposed.
303
+ await go('E', { preserveLayouts: true });
304
+ await expect.poll(renderedController).toBe('controller=ctrl:E');
305
+ const afterE = await snapshot();
306
+ expect(afterE.pageErrors, 'no page errors after D -> E').toEqual([]);
307
+ expect(afterE.setCreateCalls.at(-1), 'E set built with preservedDepth 1').toBe('E:1');
308
+ expect(
309
+ afterE.createdIds.filter((id) => id === 'ctrl:L2'),
310
+ 'borrowed layout controller must not be re-created',
311
+ ).toHaveLength(1);
312
+ expect(
313
+ afterE.scopeDisposeCounts['scope:D'],
314
+ 'D page scope must be retained while its layout controller is borrowed',
315
+ ).toBe(0);
316
+ expect(afterE.controllerDisposeCounts['ctrl:L2'], 'borrowed layout controller kept alive').toBe(0);
317
+ expect(afterE.controllerDisposeCounts['ctrl:D'], 'D page controller still disposed').toBe(1);
318
+
319
+ // --- Behavior 3 release: E -> F drops the borrowed layout, so the
320
+ // retained page scope of D is finally disposed, along with ctrl:L2.
321
+ await go('F');
322
+ await expect.poll(renderedController).toBe('controller=ctrl:F');
323
+ const finalSnapshot = await snapshot();
324
+ expect(
325
+ finalSnapshot,
326
+ 'Final scope/controller ledger after A->B->C->D->E(forced)->F.',
327
+ ).toEqual({
328
+ scopeDisposeCounts: {
329
+ 'scope:A': 1,
330
+ 'scope:L1': 1,
331
+ 'scope:B': 1,
332
+ 'scope:C': 1,
333
+ 'scope:D': 1,
334
+ 'scope:E': 1,
335
+ 'scope:F': 0,
336
+ },
337
+ controllerDisposeCounts: {
338
+ 'ctrl:A': 1,
339
+ 'ctrl:L1': 1,
340
+ 'ctrl:B': 1,
341
+ 'ctrl:C': 1,
342
+ 'ctrl:D': 1,
343
+ 'ctrl:L2': 1,
344
+ 'ctrl:E': 1,
345
+ 'ctrl:F': 0,
346
+ },
347
+ setCreateCalls: ['A:0', 'B:1', 'C:0', 'D:0', 'E:1', 'F:0'],
348
+ createdIds: [
349
+ 'scope:A', 'ctrl:L1', 'scope:L1', 'ctrl:A',
350
+ 'scope:B', 'ctrl:B',
351
+ 'scope:C', 'ctrl:C',
352
+ 'scope:D', 'ctrl:L2', 'ctrl:D',
353
+ 'scope:E', 'ctrl:E',
354
+ 'scope:F', 'ctrl:F',
355
+ ],
356
+ rendered: 'controller=ctrl:F',
357
+ pageErrors: [],
358
+ });
359
+ expect(consoleErrors, 'no console errors across the whole flow').toEqual([]);
360
+ },
361
+ 60_000,
362
+ );
363
+ });
@@ -3,6 +3,7 @@
3
3
  import { getNavigation, type NavigationEntry } from '@noego/wood/navigation';
4
4
  import { disposeControllerLifecycle } from '../controller/controller_lifecycle';
5
5
  import { createNavigationMountKey } from '../navigation/navigation_identity';
6
+ import { computePreservedLayoutDepth as computeSharedPreservedLayoutDepth } from '../navigation/layout_preservation';
6
7
  import WoodRecursiveRender from './WoodRecursiveRender.svelte';
7
8
  import { getWoodAppEnv } from '../client/app_env';
8
9
 
@@ -20,6 +21,21 @@
20
21
 
21
22
  type CreateViewControllerFn = (entry: ViewManifestEntry) => Promise<unknown>;
22
23
  type CreateLayoutControllersFn = (entry: ViewManifestEntry, startIndex?: number) => Promise<Array<unknown>>;
24
+ type ScopeLike = { dispose?: () => Promise<void> };
25
+ /**
26
+ * A view lifecycle's controllers plus the IoC scopes that own them:
27
+ * one page scope shared by the view controller and non-preservable layout
28
+ * controllers, and a dedicated scope per preservable layout (null entries
29
+ * mean "owned by the page scope"). Legacy factory props produce sets with
30
+ * null scopes — controller-only lifecycle, no scope disposal.
31
+ */
32
+ type ControllerSet = {
33
+ controller: unknown;
34
+ layoutControllers: Array<unknown>;
35
+ pageScope?: ScopeLike | null;
36
+ layoutScopes?: Array<ScopeLike | null>;
37
+ };
38
+ type CreateControllerSetFn = (entry: ViewManifestEntry, preservedDepth?: number) => Promise<ControllerSet>;
23
39
  type GetViewManifestEntryFn = (viewKey: string) => ViewManifestEntry | null;
24
40
  type NavigationOptions = {
25
41
  preserveLayouts?: boolean;
@@ -41,27 +57,50 @@
41
57
  initialLayoutControllers,
42
58
  createViewControllerFn,
43
59
  createLayoutControllersFn,
60
+ initialControllerSet,
61
+ createControllerSetFn,
44
62
  getViewManifestEntryFn,
45
63
  }: {
46
64
  manifest: Record<string, ViewManifestEntry>;
47
65
  initialEntry: ViewManifestEntry;
48
- initialController: unknown;
49
- initialLayoutControllers: Array<unknown>;
50
- createViewControllerFn: CreateViewControllerFn;
51
- createLayoutControllersFn: CreateLayoutControllersFn;
66
+ initialController?: unknown;
67
+ initialLayoutControllers?: Array<unknown>;
68
+ createViewControllerFn?: CreateViewControllerFn;
69
+ createLayoutControllersFn?: CreateLayoutControllersFn;
70
+ initialControllerSet?: ControllerSet;
71
+ createControllerSetFn?: CreateControllerSetFn;
52
72
  getViewManifestEntryFn: GetViewManifestEntryFn;
53
73
  } = $props();
54
74
 
75
+ // Unified controller-set factory: prefer the scope-aware contract; wrap
76
+ // the legacy split factories (scope-less) for backward compatibility.
77
+ const buildControllerSet: CreateControllerSetFn = createControllerSetFn
78
+ ?? (async (entry, preservedDepth = 0) => ({
79
+ controller: await createViewControllerFn?.(entry),
80
+ layoutControllers: (await createLayoutControllersFn?.(entry, preservedDepth)) ?? [],
81
+ pageScope: null,
82
+ layoutScopes: [],
83
+ }));
84
+
85
+ async function disposeScope(scope: ScopeLike | null | undefined, context: string): Promise<void> {
86
+ if (!scope || typeof scope.dispose !== 'function') return;
87
+ try {
88
+ await scope.dispose();
89
+ } catch (error) {
90
+ reportNavigationError(context, error);
91
+ }
92
+ }
93
+
55
94
  function getInitialEntry(): ViewManifestEntry {
56
95
  return initialEntry;
57
96
  }
58
97
 
59
98
  function getInitialController(): unknown {
60
- return initialController;
99
+ return initialControllerSet ? initialControllerSet.controller : initialController;
61
100
  }
62
101
 
63
102
  function getInitialLayoutControllers(): Array<unknown> {
64
- return initialLayoutControllers;
103
+ return initialControllerSet ? initialControllerSet.layoutControllers : (initialLayoutControllers ?? []);
65
104
  }
66
105
 
67
106
  let activeEntry = $state(getInitialEntry());
@@ -78,6 +117,13 @@
78
117
  let navigationTransitionQueue: Promise<void> = Promise.resolve();
79
118
  let destroyed = false;
80
119
 
120
+ // Scope ownership (null = owned by the page scope / legacy scope-less set)
121
+ let activePageScope: ScopeLike | null = initialControllerSet?.pageScope ?? null;
122
+ let activeLayoutScopes: Array<ScopeLike | null> = [...(initialControllerSet?.layoutScopes ?? [])];
123
+ // Outgoing page scopes kept alive because forced preservation carried one
124
+ // of their layout controllers forward; disposed when those layouts leave.
125
+ let retainedPageScopes: Array<{ scope: ScopeLike; keys: Set<string> }> = [];
126
+
81
127
  let activePage = $derived({
82
128
  key: activeEntry.key,
83
129
  windowName: activeEntry.windowName,
@@ -161,49 +207,14 @@
161
207
  );
162
208
  }
163
209
 
164
- function layoutIdentityMatches(
165
- left: ViewManifestEntry,
166
- right: ViewManifestEntry,
167
- index: number,
168
- ): boolean {
169
- return (
170
- left.layoutPaths[index] === right.layoutPaths[index] &&
171
- left.layoutControllers[index] === right.layoutControllers[index]
172
- );
173
- }
174
-
175
- function shouldPreserveLayout(
176
- from: ViewManifestEntry,
177
- to: ViewManifestEntry,
178
- index: number,
179
- options: NavigationOptions,
180
- ): boolean {
181
- if (options.preserveLayouts === false) {
182
- return false;
183
- }
184
- if (options.preserveLayouts === true) {
185
- return true;
186
- }
187
- return from.layoutPreserve?.[index] === true && to.layoutPreserve?.[index] === true;
188
- }
189
-
190
210
  function computePreservedLayoutDepth(
191
211
  from: ViewManifestEntry,
192
212
  to: ViewManifestEntry,
193
213
  options: NavigationOptions,
194
214
  ): number {
195
- const maxDepth = Math.min(from.layoutPaths.length, to.layoutPaths.length);
196
- let depth = 0;
197
- for (let index = 0; index < maxDepth; index += 1) {
198
- if (!layoutIdentityMatches(from, to, index)) {
199
- break;
200
- }
201
- if (!shouldPreserveLayout(from, to, index, options)) {
202
- break;
203
- }
204
- depth += 1;
205
- }
206
- return depth;
215
+ // The rule itself lives in navigation/layout_preservation so the Node
216
+ // application slice preserves exactly what this component preserves.
217
+ return computeSharedPreservedLayoutDepth(from, to, options);
207
218
  }
208
219
 
209
220
  async function disposeControllerSet(
@@ -250,16 +261,24 @@
250
261
  async function createTransitionControllers(
251
262
  entry: ViewManifestEntry,
252
263
  preservedDepth: number,
253
- ): Promise<{ controller: unknown; layoutControllers: Array<unknown> }> {
264
+ ): Promise<ControllerSet> {
265
+ if (createControllerSetFn) {
266
+ // The scope-aware factory owns its own failure cleanup (it disposes
267
+ // the scopes it created before rethrowing).
268
+ return createControllerSetFn(entry, preservedDepth);
269
+ }
270
+
254
271
  const [controllerResult, layoutControllersResult] = await Promise.allSettled([
255
- createViewControllerFn(entry),
256
- createLayoutControllersFn(entry, preservedDepth),
272
+ createViewControllerFn!(entry),
273
+ createLayoutControllersFn!(entry, preservedDepth),
257
274
  ]);
258
275
 
259
276
  if (controllerResult.status === 'fulfilled' && layoutControllersResult.status === 'fulfilled') {
260
277
  return {
261
278
  controller: controllerResult.value,
262
279
  layoutControllers: layoutControllersResult.value,
280
+ pageScope: null,
281
+ layoutScopes: [],
263
282
  };
264
283
  }
265
284
 
@@ -311,10 +330,15 @@
311
330
  const preservedLayoutKeys = activeLayoutKeys.slice(0, preservedDepth);
312
331
  const previousController = activeController;
313
332
  const previousLayoutControllers = [...activeLayoutControllers];
314
- const { controller, layoutControllers } = await createTransitionControllers(entry, preservedDepth);
333
+ const previousPageScope = activePageScope;
334
+ const previousLayoutScopes = [...activeLayoutScopes];
335
+ const nextSet = await createTransitionControllers(entry, preservedDepth);
336
+ const { controller, layoutControllers } = nextSet;
315
337
 
316
338
  if (destroyed) {
317
339
  await disposeStagedControllers(controller, layoutControllers);
340
+ for (const scope of nextSet.layoutScopes ?? []) await disposeScope(scope, 'staged layout scope disposal failed');
341
+ await disposeScope(nextSet.pageScope, 'staged page scope disposal failed');
318
342
  return;
319
343
  }
320
344
 
@@ -325,6 +349,15 @@
325
349
  );
326
350
  const nextLayoutKeys = createLayoutMountKeys(entry, nextPageKey, preservedLayoutKeys);
327
351
 
352
+ // Merge scope ownership the same way controllers merge: preserved prefix
353
+ // keeps its scopes, the created suffix brings the new set's scopes.
354
+ const nextLayoutScopes: Array<ScopeLike | null> = [];
355
+ for (let index = 0; index < entry.layoutPaths.length; index += 1) {
356
+ nextLayoutScopes[index] = index < preservedDepth
357
+ ? (previousLayoutScopes[index] ?? null)
358
+ : ((nextSet.layoutScopes ?? [])[index] ?? null);
359
+ }
360
+
328
361
  activeEntry = entry;
329
362
  activeController = controller;
330
363
  activeLayoutControllers = nextLayoutControllers;
@@ -332,8 +365,44 @@
332
365
  activeQuery = { ...navEntry.query };
333
366
  activePageKey = nextPageKey;
334
367
  activeLayoutKeys = nextLayoutKeys;
368
+ activePageScope = nextSet.pageScope ?? null;
369
+ activeLayoutScopes = nextLayoutScopes;
335
370
 
336
371
  await disposeControllerSet(previousController, previousLayoutControllers, preservedDepth);
372
+
373
+ // Scope lifecycle for the outgoing page:
374
+ // - dedicated layout scopes past the preserved prefix are done -> dispose;
375
+ // - the page scope is done UNLESS forced preservation carried one of its
376
+ // page-scope layout controllers forward -> retain it until they leave.
377
+ for (let index = preservedDepth; index < previousLayoutScopes.length; index += 1) {
378
+ await disposeScope(previousLayoutScopes[index], 'layout scope disposal failed');
379
+ }
380
+ const borrowedKeys = new Set<string>();
381
+ for (let index = 0; index < preservedDepth; index += 1) {
382
+ if (previousLayoutControllers[index] !== undefined && (previousLayoutScopes[index] ?? null) === null) {
383
+ borrowedKeys.add(preservedLayoutKeys[index]);
384
+ }
385
+ }
386
+ if (previousPageScope) {
387
+ if (borrowedKeys.size > 0) {
388
+ retainedPageScopes.push({ scope: previousPageScope, keys: borrowedKeys });
389
+ } else {
390
+ await disposeScope(previousPageScope, 'page scope disposal failed');
391
+ }
392
+ }
393
+
394
+ // Retained scopes whose borrowed layouts are no longer preserved are done.
395
+ const liveKeys = new Set(nextLayoutKeys);
396
+ const stillRetained: typeof retainedPageScopes = [];
397
+ for (const retained of retainedPageScopes) {
398
+ retained.keys = new Set([...retained.keys].filter((key) => liveKeys.has(key)));
399
+ if (retained.keys.size > 0) {
400
+ stillRetained.push(retained);
401
+ } else {
402
+ await disposeScope(retained.scope, 'retained page scope disposal failed');
403
+ }
404
+ }
405
+ retainedPageScopes = stillRetained;
337
406
  }
338
407
 
339
408
  function enqueueNavigationEntry(navEntry: NavigationEntry | null): void {
@@ -408,7 +477,15 @@
408
477
  unsubscribe();
409
478
  unsubscribeWindowNavigate?.();
410
479
  void navigationTransitionQueue
411
- .then(() => disposeActiveControllers())
480
+ .then(async () => {
481
+ await disposeActiveControllers();
482
+ for (const scope of activeLayoutScopes) await disposeScope(scope, 'teardown layout scope disposal failed');
483
+ await disposeScope(activePageScope, 'teardown page scope disposal failed');
484
+ for (const retained of retainedPageScopes) {
485
+ await disposeScope(retained.scope, 'teardown retained scope disposal failed');
486
+ }
487
+ retainedPageScopes = [];
488
+ })
412
489
  .catch((error: unknown) => {
413
490
  reportNavigationError('navigation teardown failed', error);
414
491
  });