@noego/wood 0.6.1 → 0.7.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 (247) 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 +8 -4
  13. package/dist/client/index.d.ts +8 -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 +13 -2
  43. package/dist/frontend/frontend_surface.d.ts +13 -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 +3 -0
  47. package/dist/frontend/index.d.ts +3 -0
  48. package/dist/index.cjs +5 -0
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.d.cts +27 -1
  51. package/dist/index.d.ts +27 -1
  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 +6 -28
  77. package/dist/navigation/index.d.ts +6 -28
  78. package/dist/navigation/index.js +8 -1
  79. package/dist/navigation/index.js.map +1 -1
  80. package/dist/navigation/layout_preservation.cjs.map +1 -1
  81. package/dist/navigation/layout_preservation.d.cts +27 -2
  82. package/dist/navigation/layout_preservation.d.ts +27 -2
  83. package/dist/navigation/layout_preservation.js.map +1 -1
  84. package/dist/runtime/get_container.cjs +2 -0
  85. package/dist/runtime/get_container.cjs.map +1 -1
  86. package/dist/runtime/get_container.js +2 -0
  87. package/dist/runtime/get_container.js.map +1 -1
  88. package/dist/runtime/index.cjs +5 -0
  89. package/dist/runtime/index.cjs.map +1 -1
  90. package/dist/runtime/index.d.cts +2 -0
  91. package/dist/runtime/index.d.ts +2 -0
  92. package/dist/runtime/index.js +3 -0
  93. package/dist/runtime/index.js.map +1 -1
  94. package/dist/runtime/runtime_capabilities.cjs +35 -0
  95. package/dist/runtime/runtime_capabilities.cjs.map +1 -0
  96. package/dist/runtime/runtime_capabilities.d.cts +6 -0
  97. package/dist/runtime/runtime_capabilities.d.ts +6 -0
  98. package/dist/runtime/runtime_capabilities.js +11 -0
  99. package/dist/runtime/runtime_capabilities.js.map +1 -0
  100. package/dist/runtime/runtime_module.cjs +2 -0
  101. package/dist/runtime/runtime_module.cjs.map +1 -1
  102. package/dist/runtime/runtime_module.js +2 -0
  103. package/dist/runtime/runtime_module.js.map +1 -1
  104. package/dist/test-wood.cjs +150 -0
  105. package/dist/test-wood.cjs.map +1 -0
  106. package/dist/test-wood.d.cts +62 -0
  107. package/dist/test-wood.d.ts +62 -0
  108. package/dist/test-wood.js +115 -0
  109. package/dist/test-wood.js.map +1 -0
  110. package/dist/testing/browser.cjs +92 -163
  111. package/dist/testing/browser.cjs.map +1 -1
  112. package/dist/testing/browser.d.cts +14 -13
  113. package/dist/testing/browser.d.ts +14 -13
  114. package/dist/testing/browser.js +88 -162
  115. package/dist/testing/browser.js.map +1 -1
  116. package/dist/testing/create_controller_harness.d.cts +3 -1
  117. package/dist/testing/create_controller_harness.d.ts +3 -1
  118. package/dist/testing/create_router_bridge.cjs +19 -15
  119. package/dist/testing/create_router_bridge.cjs.map +1 -1
  120. package/dist/testing/create_router_bridge.d.cts +8 -1
  121. package/dist/testing/create_router_bridge.d.ts +8 -1
  122. package/dist/testing/create_router_bridge.js +18 -15
  123. package/dist/testing/create_router_bridge.js.map +1 -1
  124. package/dist/testing/index.cjs +12 -4
  125. package/dist/testing/index.cjs.map +1 -1
  126. package/dist/testing/index.d.cts +12 -5
  127. package/dist/testing/index.d.ts +12 -5
  128. package/dist/testing/index.js +9 -3
  129. package/dist/testing/index.js.map +1 -1
  130. package/dist/testing/native_browser/browser_pool.cjs +31 -0
  131. package/dist/testing/native_browser/browser_pool.cjs.map +1 -0
  132. package/dist/testing/native_browser/browser_pool.d.cts +1 -0
  133. package/dist/testing/native_browser/browser_pool.d.ts +1 -0
  134. package/dist/testing/native_browser/browser_pool.js +6 -0
  135. package/dist/testing/native_browser/browser_pool.js.map +1 -0
  136. package/dist/testing/native_browser/happy_dom_tab.cjs +107 -0
  137. package/dist/testing/native_browser/happy_dom_tab.cjs.map +1 -0
  138. package/dist/testing/native_browser/happy_dom_tab.d.cts +35 -0
  139. package/dist/testing/native_browser/happy_dom_tab.d.ts +35 -0
  140. package/dist/testing/native_browser/happy_dom_tab.js +73 -0
  141. package/dist/testing/native_browser/happy_dom_tab.js.map +1 -0
  142. package/dist/testing/native_browser/happy_dom_transfer.cjs +29 -0
  143. package/dist/testing/native_browser/happy_dom_transfer.cjs.map +1 -0
  144. package/dist/testing/native_browser/happy_dom_transfer.d.cts +1 -0
  145. package/dist/testing/native_browser/happy_dom_transfer.d.ts +1 -0
  146. package/dist/testing/native_browser/happy_dom_transfer.js +5 -0
  147. package/dist/testing/native_browser/happy_dom_transfer.js.map +1 -0
  148. package/dist/testing/native_browser/host_transport.cjs +89 -0
  149. package/dist/testing/native_browser/host_transport.cjs.map +1 -0
  150. package/dist/testing/native_browser/host_transport.d.cts +44 -0
  151. package/dist/testing/native_browser/host_transport.d.ts +44 -0
  152. package/dist/testing/native_browser/host_transport.js +65 -0
  153. package/dist/testing/native_browser/host_transport.js.map +1 -0
  154. package/dist/testing/native_browser/native_browser.types.cjs +17 -0
  155. package/dist/testing/native_browser/native_browser.types.cjs.map +1 -0
  156. package/dist/testing/native_browser/native_browser.types.d.cts +31 -0
  157. package/dist/testing/native_browser/native_browser.types.d.ts +31 -0
  158. package/dist/testing/native_browser/native_browser.types.js +1 -0
  159. package/dist/testing/native_browser/native_browser.types.js.map +1 -0
  160. package/dist/testing/native_browser/native_browser_environment.cjs +143 -0
  161. package/dist/testing/native_browser/native_browser_environment.cjs.map +1 -0
  162. package/dist/testing/native_browser/native_browser_environment.d.cts +36 -0
  163. package/dist/testing/native_browser/native_browser_environment.d.ts +36 -0
  164. package/dist/testing/native_browser/native_browser_environment.js +109 -0
  165. package/dist/testing/native_browser/native_browser_environment.js.map +1 -0
  166. package/dist/testing/native_browser/native_browser_lifecycle.cjs +31 -0
  167. package/dist/testing/native_browser/native_browser_lifecycle.cjs.map +1 -0
  168. package/dist/testing/native_browser/native_browser_lifecycle.d.cts +1 -0
  169. package/dist/testing/native_browser/native_browser_lifecycle.d.ts +1 -0
  170. package/dist/testing/native_browser/native_browser_lifecycle.js +6 -0
  171. package/dist/testing/native_browser/native_browser_lifecycle.js.map +1 -0
  172. package/dist/testing/native_browser/playwright_tab.cjs +124 -0
  173. package/dist/testing/native_browser/playwright_tab.cjs.map +1 -0
  174. package/dist/testing/native_browser/playwright_tab.d.cts +35 -0
  175. package/dist/testing/native_browser/playwright_tab.d.ts +35 -0
  176. package/dist/testing/native_browser/playwright_tab.js +100 -0
  177. package/dist/testing/native_browser/playwright_tab.js.map +1 -0
  178. package/dist/testing/native_browser/renderer_compilation.cjs +149 -0
  179. package/dist/testing/native_browser/renderer_compilation.cjs.map +1 -0
  180. package/dist/testing/native_browser/renderer_compilation.d.cts +35 -0
  181. package/dist/testing/native_browser/renderer_compilation.d.ts +35 -0
  182. package/dist/testing/native_browser/renderer_compilation.js +115 -0
  183. package/dist/testing/native_browser/renderer_compilation.js.map +1 -0
  184. package/dist/testing/native_browser/renderer_entry.cjs +148 -0
  185. package/dist/testing/native_browser/renderer_entry.cjs.map +1 -0
  186. package/dist/testing/native_browser/renderer_entry.d.cts +13 -0
  187. package/dist/testing/native_browser/renderer_entry.d.ts +13 -0
  188. package/dist/testing/native_browser/renderer_entry.js +114 -0
  189. package/dist/testing/native_browser/renderer_entry.js.map +1 -0
  190. package/dist/testing/registration_only_root.cjs +61 -5
  191. package/dist/testing/registration_only_root.cjs.map +1 -1
  192. package/dist/testing/registration_only_root.d.cts +25 -4
  193. package/dist/testing/registration_only_root.d.ts +25 -4
  194. package/dist/testing/registration_only_root.js +60 -5
  195. package/dist/testing/registration_only_root.js.map +1 -1
  196. package/dist/testing/runner_importer.cjs +75 -0
  197. package/dist/testing/runner_importer.cjs.map +1 -0
  198. package/dist/testing/runner_importer.d.cts +8 -0
  199. package/dist/testing/runner_importer.d.ts +8 -0
  200. package/dist/testing/runner_importer.js +41 -0
  201. package/dist/testing/runner_importer.js.map +1 -0
  202. package/dist/testing/test_wood_config.cjs +387 -272
  203. package/dist/testing/test_wood_config.cjs.map +1 -1
  204. package/dist/testing/test_wood_config.d.cts +19 -123
  205. package/dist/testing/test_wood_config.d.ts +19 -123
  206. package/dist/testing/test_wood_config.js +396 -270
  207. package/dist/testing/test_wood_config.js.map +1 -1
  208. package/dist/testing/wood_config_source.cjs +12 -0
  209. package/dist/testing/wood_config_source.cjs.map +1 -1
  210. package/dist/testing/wood_config_source.d.cts +10 -0
  211. package/dist/testing/wood_config_source.d.ts +10 -0
  212. package/dist/testing/wood_config_source.js +12 -0
  213. package/dist/testing/wood_config_source.js.map +1 -1
  214. package/dist/testing/wood_env_renderer.cjs +28 -3
  215. package/dist/testing/wood_env_renderer.cjs.map +1 -1
  216. package/dist/testing/wood_env_renderer.d.cts +13 -1
  217. package/dist/testing/wood_env_renderer.d.ts +13 -1
  218. package/dist/testing/wood_env_renderer.js +28 -3
  219. package/dist/testing/wood_env_renderer.js.map +1 -1
  220. package/dist/testing/wood_lazy_runtime.cjs +109 -0
  221. package/dist/testing/wood_lazy_runtime.cjs.map +1 -0
  222. package/dist/testing/wood_lazy_runtime.d.cts +36 -0
  223. package/dist/testing/wood_lazy_runtime.d.ts +36 -0
  224. package/dist/testing/wood_lazy_runtime.js +84 -0
  225. package/dist/testing/wood_lazy_runtime.js.map +1 -0
  226. package/dist/testing/wood_test_selection.cjs +34 -55
  227. package/dist/testing/wood_test_selection.cjs.map +1 -1
  228. package/dist/testing/wood_test_selection.d.cts +18 -24
  229. package/dist/testing/wood_test_selection.d.ts +18 -24
  230. package/dist/testing/wood_test_selection.js +30 -54
  231. package/dist/testing/wood_test_selection.js.map +1 -1
  232. package/dist/testing-api.cjs +17 -0
  233. package/dist/testing-api.cjs.map +1 -0
  234. package/dist/testing-api.d.cts +18 -0
  235. package/dist/testing-api.d.ts +18 -0
  236. package/dist/testing-api.js +1 -0
  237. package/dist/testing-api.js.map +1 -0
  238. package/dist/types/config.cjs.map +1 -1
  239. package/dist/types/config.d.cts +4 -0
  240. package/dist/types/config.d.ts +4 -0
  241. package/docs/native-browser-testing.md +164 -0
  242. package/package.json +31 -5
  243. package/src/components/NavigationShell.browser.test.ts +10 -10
  244. package/src/components/NavigationShell.cleanup.browser.test.ts +57 -0
  245. package/src/components/NavigationShell.scopes.browser.test.ts +5 -5
  246. package/src/components/NavigationShell.svelte +50 -24
  247. package/src/navigation/layout_preservation.ts +3 -1
@@ -0,0 +1,66 @@
1
+ import { RequestData } from '../types/context.cjs';
2
+
3
+ /**
4
+ * Shared page/layout data-loader orchestration.
5
+ *
6
+ * One implementation, three callers: the generated production renderer app
7
+ * (`createControllerSet`), the Node frontend application surface
8
+ * (`constructPage`) and the main-process `LoaderRunner`. It is deliberately
9
+ * free of Node APIs so it ships to the browser through `@noego/wood/navigation`.
10
+ *
11
+ * Convention: a view `pages/chat.svelte` MAY have a companion loader
12
+ * `pages/chat.load.ts` whose DEFAULT export is `(request: RequestData) =>
13
+ * data`. Discovery (does the companion exist?) is a build-time concern of the
14
+ * caller (codegen, the test config source, the runner); this module only
15
+ * defines the companion identity and executes an already-resolved set:
16
+ * layouts in stack order (outermost first), then the view. A layout carried
17
+ * over from the previous page (`preservedDepth`) is never re-run.
18
+ */
19
+
20
+ type DataLoader = (request: RequestData) => unknown;
21
+ /** Companion suffixes, in resolution order; more than one present is ambiguous. */
22
+ declare const LOADER_COMPANION_SUFFIXES: readonly [".load.ts", ".load.js"];
23
+ /** The companion loader candidates for a `.svelte` view path (empty for other files). */
24
+ declare function loaderCompanionCandidates(viewPath: string): string[];
25
+ /** Loaders resolved for one view: index-aligned with the layout stack; `null` = no loader. */
26
+ interface ViewLoaderSet {
27
+ readonly layouts: ReadonlyArray<DataLoader | null>;
28
+ readonly view: DataLoader | null;
29
+ }
30
+ /** A loader ran and produced `data` (which may legitimately be `undefined`). */
31
+ interface LoadedData {
32
+ readonly data: unknown;
33
+ }
34
+ /** Index-aligned outcome; `null` where no loader exists or the layout was preserved. */
35
+ interface ViewLoaderResult {
36
+ readonly layouts: ReadonlyArray<LoadedData | null>;
37
+ readonly view: LoadedData | null;
38
+ }
39
+ interface RouteInput {
40
+ readonly params: Record<string, string>;
41
+ readonly query: Record<string, string>;
42
+ }
43
+ declare const NO_LOADERS: ViewLoaderSet;
44
+ /** The request a page/layout loader receives (`url` is the page identity). */
45
+ declare function createLoaderRequest(page: string, route: RouteInput): RequestData;
46
+ /** A loader module's default export must be a function; anything else is a source error. */
47
+ declare function assertDataLoader(value: unknown, label: string): DataLoader;
48
+ /**
49
+ * Execute a view's loaders: layouts from `preservedDepth` in stack order,
50
+ * then the view. Any failure rejects — the caller owns rollback.
51
+ */
52
+ declare function runViewLoaders(set: ViewLoaderSet, request: RequestData, preservedDepth?: number): Promise<ViewLoaderResult>;
53
+ /**
54
+ * The argument a controller's `initialize()` receives: `{ params, query }`
55
+ * exactly as before when the controller has no loader, plus `data` when one
56
+ * ran for it.
57
+ */
58
+ declare function initializeArgument(route: RouteInput, loaded: LoadedData | null | undefined): {
59
+ params: Record<string, string>;
60
+ query: Record<string, string>;
61
+ data?: unknown;
62
+ };
63
+ /** Call `initialize(argument)` when the controller declares one. */
64
+ declare function initializeController(controller: unknown, argument: unknown): Promise<void>;
65
+
66
+ export { type DataLoader, LOADER_COMPANION_SUFFIXES, type LoadedData, NO_LOADERS, type RouteInput, type ViewLoaderResult, type ViewLoaderSet, assertDataLoader, createLoaderRequest, initializeArgument, initializeController, loaderCompanionCandidates, runViewLoaders };
@@ -0,0 +1,66 @@
1
+ import { RequestData } from '../types/context.js';
2
+
3
+ /**
4
+ * Shared page/layout data-loader orchestration.
5
+ *
6
+ * One implementation, three callers: the generated production renderer app
7
+ * (`createControllerSet`), the Node frontend application surface
8
+ * (`constructPage`) and the main-process `LoaderRunner`. It is deliberately
9
+ * free of Node APIs so it ships to the browser through `@noego/wood/navigation`.
10
+ *
11
+ * Convention: a view `pages/chat.svelte` MAY have a companion loader
12
+ * `pages/chat.load.ts` whose DEFAULT export is `(request: RequestData) =>
13
+ * data`. Discovery (does the companion exist?) is a build-time concern of the
14
+ * caller (codegen, the test config source, the runner); this module only
15
+ * defines the companion identity and executes an already-resolved set:
16
+ * layouts in stack order (outermost first), then the view. A layout carried
17
+ * over from the previous page (`preservedDepth`) is never re-run.
18
+ */
19
+
20
+ type DataLoader = (request: RequestData) => unknown;
21
+ /** Companion suffixes, in resolution order; more than one present is ambiguous. */
22
+ declare const LOADER_COMPANION_SUFFIXES: readonly [".load.ts", ".load.js"];
23
+ /** The companion loader candidates for a `.svelte` view path (empty for other files). */
24
+ declare function loaderCompanionCandidates(viewPath: string): string[];
25
+ /** Loaders resolved for one view: index-aligned with the layout stack; `null` = no loader. */
26
+ interface ViewLoaderSet {
27
+ readonly layouts: ReadonlyArray<DataLoader | null>;
28
+ readonly view: DataLoader | null;
29
+ }
30
+ /** A loader ran and produced `data` (which may legitimately be `undefined`). */
31
+ interface LoadedData {
32
+ readonly data: unknown;
33
+ }
34
+ /** Index-aligned outcome; `null` where no loader exists or the layout was preserved. */
35
+ interface ViewLoaderResult {
36
+ readonly layouts: ReadonlyArray<LoadedData | null>;
37
+ readonly view: LoadedData | null;
38
+ }
39
+ interface RouteInput {
40
+ readonly params: Record<string, string>;
41
+ readonly query: Record<string, string>;
42
+ }
43
+ declare const NO_LOADERS: ViewLoaderSet;
44
+ /** The request a page/layout loader receives (`url` is the page identity). */
45
+ declare function createLoaderRequest(page: string, route: RouteInput): RequestData;
46
+ /** A loader module's default export must be a function; anything else is a source error. */
47
+ declare function assertDataLoader(value: unknown, label: string): DataLoader;
48
+ /**
49
+ * Execute a view's loaders: layouts from `preservedDepth` in stack order,
50
+ * then the view. Any failure rejects — the caller owns rollback.
51
+ */
52
+ declare function runViewLoaders(set: ViewLoaderSet, request: RequestData, preservedDepth?: number): Promise<ViewLoaderResult>;
53
+ /**
54
+ * The argument a controller's `initialize()` receives: `{ params, query }`
55
+ * exactly as before when the controller has no loader, plus `data` when one
56
+ * ran for it.
57
+ */
58
+ declare function initializeArgument(route: RouteInput, loaded: LoadedData | null | undefined): {
59
+ params: Record<string, string>;
60
+ query: Record<string, string>;
61
+ data?: unknown;
62
+ };
63
+ /** Call `initialize(argument)` when the controller declares one. */
64
+ declare function initializeController(controller: unknown, argument: unknown): Promise<void>;
65
+
66
+ export { type DataLoader, LOADER_COMPANION_SUFFIXES, type LoadedData, NO_LOADERS, type RouteInput, type ViewLoaderResult, type ViewLoaderSet, assertDataLoader, createLoaderRequest, initializeArgument, initializeController, loaderCompanionCandidates, runViewLoaders };
@@ -0,0 +1,51 @@
1
+ const LOADER_COMPANION_SUFFIXES = [".load.ts", ".load.js"];
2
+ function loaderCompanionCandidates(viewPath) {
3
+ if (!/\.svelte$/.test(viewPath)) return [];
4
+ const base = viewPath.replace(/\.svelte$/, "");
5
+ return LOADER_COMPANION_SUFFIXES.map((suffix) => base + suffix);
6
+ }
7
+ const NO_LOADERS = Object.freeze({ layouts: Object.freeze([]), view: null });
8
+ function createLoaderRequest(page, route) {
9
+ return {
10
+ url: page,
11
+ params: { ...route.params },
12
+ query: { ...route.query },
13
+ context: {},
14
+ headers: {}
15
+ };
16
+ }
17
+ function assertDataLoader(value, label) {
18
+ if (typeof value !== "function") {
19
+ throw new TypeError(`${label} must default-export a loader function (request) => data`);
20
+ }
21
+ return value;
22
+ }
23
+ async function runViewLoaders(set, request, preservedDepth = 0) {
24
+ const layouts = [];
25
+ for (let index = 0; index < set.layouts.length; index += 1) {
26
+ const loader = index < preservedDepth ? null : set.layouts[index];
27
+ layouts.push(loader ? { data: await loader(request) } : null);
28
+ }
29
+ const view = set.view ? { data: await set.view(request) } : null;
30
+ return { layouts, view };
31
+ }
32
+ function initializeArgument(route, loaded) {
33
+ const base = { params: route.params, query: route.query };
34
+ return loaded ? { ...base, data: loaded.data } : base;
35
+ }
36
+ async function initializeController(controller, argument) {
37
+ const initialize = controller?.initialize;
38
+ if (typeof initialize !== "function") return;
39
+ await initialize.call(controller, argument);
40
+ }
41
+ export {
42
+ LOADER_COMPANION_SUFFIXES,
43
+ NO_LOADERS,
44
+ assertDataLoader,
45
+ createLoaderRequest,
46
+ initializeArgument,
47
+ initializeController,
48
+ loaderCompanionCandidates,
49
+ runViewLoaders
50
+ };
51
+ //# sourceMappingURL=loader_orchestration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/loader/loader_orchestration.ts"],"sourcesContent":["/**\n * Shared page/layout data-loader orchestration.\n *\n * One implementation, three callers: the generated production renderer app\n * (`createControllerSet`), the Node frontend application surface\n * (`constructPage`) and the main-process `LoaderRunner`. It is deliberately\n * free of Node APIs so it ships to the browser through `@noego/wood/navigation`.\n *\n * Convention: a view `pages/chat.svelte` MAY have a companion loader\n * `pages/chat.load.ts` whose DEFAULT export is `(request: RequestData) =>\n * data`. Discovery (does the companion exist?) is a build-time concern of the\n * caller (codegen, the test config source, the runner); this module only\n * defines the companion identity and executes an already-resolved set:\n * layouts in stack order (outermost first), then the view. A layout carried\n * over from the previous page (`preservedDepth`) is never re-run.\n */\nimport type { RequestData } from '../types/context.js';\n\nexport type DataLoader = (request: RequestData) => unknown;\n\n/** Companion suffixes, in resolution order; more than one present is ambiguous. */\nexport const LOADER_COMPANION_SUFFIXES = ['.load.ts', '.load.js'] as const;\n\n/** The companion loader candidates for a `.svelte` view path (empty for other files). */\nexport function loaderCompanionCandidates(viewPath: string): string[] {\n if (!/\\.svelte$/.test(viewPath)) return [];\n const base = viewPath.replace(/\\.svelte$/, '');\n return LOADER_COMPANION_SUFFIXES.map((suffix) => base + suffix);\n}\n\n/** Loaders resolved for one view: index-aligned with the layout stack; `null` = no loader. */\nexport interface ViewLoaderSet {\n readonly layouts: ReadonlyArray<DataLoader | null>;\n readonly view: DataLoader | null;\n}\n\n/** A loader ran and produced `data` (which may legitimately be `undefined`). */\nexport interface LoadedData {\n readonly data: unknown;\n}\n\n/** Index-aligned outcome; `null` where no loader exists or the layout was preserved. */\nexport interface ViewLoaderResult {\n readonly layouts: ReadonlyArray<LoadedData | null>;\n readonly view: LoadedData | null;\n}\n\nexport interface RouteInput {\n readonly params: Record<string, string>;\n readonly query: Record<string, string>;\n}\n\nexport const NO_LOADERS: ViewLoaderSet = Object.freeze({ layouts: Object.freeze([]), view: null });\n\n/** The request a page/layout loader receives (`url` is the page identity). */\nexport function createLoaderRequest(page: string, route: RouteInput): RequestData {\n return {\n url: page,\n params: { ...route.params },\n query: { ...route.query },\n context: {},\n headers: {},\n };\n}\n\n/** A loader module's default export must be a function; anything else is a source error. */\nexport function assertDataLoader(value: unknown, label: string): DataLoader {\n if (typeof value !== 'function') {\n throw new TypeError(`${label} must default-export a loader function (request) => data`);\n }\n return value as DataLoader;\n}\n\n/**\n * Execute a view's loaders: layouts from `preservedDepth` in stack order,\n * then the view. Any failure rejects — the caller owns rollback.\n */\nexport async function runViewLoaders(\n set: ViewLoaderSet,\n request: RequestData,\n preservedDepth = 0,\n): Promise<ViewLoaderResult> {\n const layouts: Array<LoadedData | null> = [];\n for (let index = 0; index < set.layouts.length; index += 1) {\n const loader = index < preservedDepth ? null : set.layouts[index];\n layouts.push(loader ? { data: await loader(request) } : null);\n }\n const view = set.view ? { data: await set.view(request) } : null;\n return { layouts, view };\n}\n\n/**\n * The argument a controller's `initialize()` receives: `{ params, query }`\n * exactly as before when the controller has no loader, plus `data` when one\n * ran for it.\n */\nexport function initializeArgument(\n route: RouteInput,\n loaded: LoadedData | null | undefined,\n): { params: Record<string, string>; query: Record<string, string>; data?: unknown } {\n const base = { params: route.params, query: route.query };\n return loaded ? { ...base, data: loaded.data } : base;\n}\n\n/** Call `initialize(argument)` when the controller declares one. */\nexport async function initializeController(controller: unknown, argument: unknown): Promise<void> {\n const initialize = (controller as { initialize?: unknown } | null)?.initialize;\n if (typeof initialize !== 'function') return;\n await (initialize as (argument: unknown) => unknown).call(controller, argument);\n}\n"],"mappings":"AAqBO,MAAM,4BAA4B,CAAC,YAAY,UAAU;AAGzD,SAAS,0BAA0B,UAA4B;AACpE,MAAI,CAAC,YAAY,KAAK,QAAQ,EAAG,QAAO,CAAC;AACzC,QAAM,OAAO,SAAS,QAAQ,aAAa,EAAE;AAC7C,SAAO,0BAA0B,IAAI,CAAC,WAAW,OAAO,MAAM;AAChE;AAwBO,MAAM,aAA4B,OAAO,OAAO,EAAE,SAAS,OAAO,OAAO,CAAC,CAAC,GAAG,MAAM,KAAK,CAAC;AAG1F,SAAS,oBAAoB,MAAc,OAAgC;AAChF,SAAO;AAAA,IACL,KAAK;AAAA,IACL,QAAQ,EAAE,GAAG,MAAM,OAAO;AAAA,IAC1B,OAAO,EAAE,GAAG,MAAM,MAAM;AAAA,IACxB,SAAS,CAAC;AAAA,IACV,SAAS,CAAC;AAAA,EACZ;AACF;AAGO,SAAS,iBAAiB,OAAgB,OAA2B;AAC1E,MAAI,OAAO,UAAU,YAAY;AAC/B,UAAM,IAAI,UAAU,GAAG,KAAK,0DAA0D;AAAA,EACxF;AACA,SAAO;AACT;AAMA,eAAsB,eACpB,KACA,SACA,iBAAiB,GACU;AAC3B,QAAM,UAAoC,CAAC;AAC3C,WAAS,QAAQ,GAAG,QAAQ,IAAI,QAAQ,QAAQ,SAAS,GAAG;AAC1D,UAAM,SAAS,QAAQ,iBAAiB,OAAO,IAAI,QAAQ,KAAK;AAChE,YAAQ,KAAK,SAAS,EAAE,MAAM,MAAM,OAAO,OAAO,EAAE,IAAI,IAAI;AAAA,EAC9D;AACA,QAAM,OAAO,IAAI,OAAO,EAAE,MAAM,MAAM,IAAI,KAAK,OAAO,EAAE,IAAI;AAC5D,SAAO,EAAE,SAAS,KAAK;AACzB;AAOO,SAAS,mBACd,OACA,QACmF;AACnF,QAAM,OAAO,EAAE,QAAQ,MAAM,QAAQ,OAAO,MAAM,MAAM;AACxD,SAAO,SAAS,EAAE,GAAG,MAAM,MAAM,OAAO,KAAK,IAAI;AACnD;AAGA,eAAsB,qBAAqB,YAAqB,UAAkC;AAChG,QAAM,aAAc,YAAgD;AACpE,MAAI,OAAO,eAAe,WAAY;AACtC,QAAO,WAA8C,KAAK,YAAY,QAAQ;AAChF;","names":[]}
@@ -35,69 +35,74 @@ var import_path = __toESM(require("path"), 1);
35
35
  var import_node_url = require("node:url");
36
36
  var import_fs = require("fs");
37
37
  var import_logger = require("@noego/logger");
38
+ var import_loader_orchestration = require("./loader_orchestration.cjs");
38
39
  const logger = (0, import_logger.getLogger)("wood:loader");
39
40
  function toImportSpecifier(spec) {
40
41
  return import_path.default.isAbsolute(spec) ? (0, import_node_url.pathToFileURL)(spec).href : spec;
41
42
  }
43
+ function mtimeOf(filePath) {
44
+ try {
45
+ return (0, import_fs.statSync)(filePath).mtimeMs;
46
+ } catch (error) {
47
+ if (error.code === "ENOENT") return null;
48
+ throw error;
49
+ }
50
+ }
42
51
  class LoaderRunner {
43
52
  constructor(componentDir) {
44
53
  this.cache = /* @__PURE__ */ new Map();
45
54
  this.componentDir = componentDir;
46
55
  }
47
56
  /**
48
- * Discover the loader file for a view component.
49
- * Convention: for `pages/chat/main.svelte`, look for `pages/chat/main.load.ts`
57
+ * Discover the loader file for a view component. Exactly one companion
58
+ * may exist; more than one is ambiguous and an error, none is `null`.
50
59
  */
51
60
  getLoaderPath(viewPath) {
52
- const basePath = viewPath.replace(/\.svelte$/, ".load.ts");
53
- return import_path.default.resolve(this.componentDir, basePath);
61
+ const present = [];
62
+ for (const candidate of (0, import_loader_orchestration.loaderCompanionCandidates)(viewPath)) {
63
+ const filePath = import_path.default.resolve(this.componentDir, candidate);
64
+ const mtime = mtimeOf(filePath);
65
+ if (mtime !== null) present.push({ filePath, mtime });
66
+ }
67
+ if (present.length > 1) {
68
+ throw new Error(
69
+ `Ambiguous loader for "${viewPath}": ${present.map((entry) => entry.filePath).join(", ")}; keep exactly one companion`
70
+ );
71
+ }
72
+ return present[0] ?? null;
54
73
  }
55
74
  /**
56
- * Load a loader function with mtime-based cache invalidation.
57
- * Returns null when no loader file exists (not an error).
75
+ * Load a loader function with mtime-based cache invalidation. Returns
76
+ * `null` only when no companion exists; an import failure or a module
77
+ * without a default loader function is an error.
58
78
  */
59
- async loadLoader(loaderPath) {
60
- try {
61
- const stat = (0, import_fs.statSync)(loaderPath);
62
- const mtime = stat.mtimeMs;
63
- const cached = this.cache.get(loaderPath);
64
- if (cached && cached.mtime === mtime) {
65
- return cached.fn;
66
- }
67
- const mod = await import(`${toImportSpecifier(loaderPath)}?t=${Date.now()}`);
68
- const fn = mod.default;
69
- if (typeof fn !== "function") {
70
- logger.warn(`Loader at "${loaderPath}" does not export a default function`);
71
- return null;
72
- }
73
- logger.debug(`Loaded loader: ${loaderPath}`);
74
- this.cache.set(loaderPath, { fn, mtime, filePath: loaderPath });
75
- return fn;
76
- } catch {
77
- return null;
79
+ async loadLoader(viewPath) {
80
+ const located = this.getLoaderPath(viewPath);
81
+ if (!located) return null;
82
+ const { filePath, mtime } = located;
83
+ const cached = this.cache.get(filePath);
84
+ if (cached && cached.mtime === mtime) {
85
+ return cached.fn;
78
86
  }
87
+ const mod = await import(`${toImportSpecifier(filePath)}?t=${Date.now()}`);
88
+ const fn = (0, import_loader_orchestration.assertDataLoader)(mod.default, `Loader at "${filePath}"`);
89
+ logger.debug(`Loaded loader: ${filePath}`);
90
+ this.cache.set(filePath, { fn, mtime, filePath });
91
+ return fn;
79
92
  }
80
93
  /**
81
- * Execute all loaders for a matched route.
82
- * Layout loaders run in parallel, view loader runs after.
94
+ * Execute all loaders for a matched route: layouts in stack order, then
95
+ * the view (shared orchestration).
83
96
  */
84
97
  async executeLoaders(viewDef, requestData) {
85
- const layoutLoaderPromises = viewDef.layouts.map(async (layoutPath) => {
86
- const loaderPath = this.getLoaderPath(layoutPath);
87
- const loader = await this.loadLoader(loaderPath);
88
- if (loader) {
89
- return loader(requestData);
90
- }
91
- return void 0;
92
- });
93
- const layoutData = await Promise.all(layoutLoaderPromises);
94
- const viewLoaderPath = this.getLoaderPath(viewDef.viewPath);
95
- const viewLoader = await this.loadLoader(viewLoaderPath);
96
- const viewData = viewLoader ? await viewLoader(requestData) : {};
98
+ const layouts = [];
99
+ for (const layoutPath of viewDef.layouts) layouts.push(await this.loadLoader(layoutPath));
100
+ const view = await this.loadLoader(viewDef.viewPath);
101
+ const result = await (0, import_loader_orchestration.runViewLoaders)({ layouts, view }, requestData);
97
102
  logger.debug(`Executed loaders for view "${viewDef.pageName}": ${viewDef.layouts.length} layout(s)`);
98
103
  return {
99
- layoutData: layoutData.filter((d) => d !== void 0),
100
- viewData
104
+ layoutData: result.layouts.filter((entry) => entry !== null).map((entry) => entry.data),
105
+ viewData: result.view ? result.view.data : {}
101
106
  };
102
107
  }
103
108
  /** Clear the loader cache. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/loader/loader_runner.ts"],"sourcesContent":["import path from 'path';\nimport { pathToFileURL } from 'node:url';\nimport { statSync } from 'fs';\nimport { getLogger } from '@noego/logger';\nimport type { RequestData, LoaderResult } from '../types/context.cjs';\nimport type { ViewDef } from '../types/views.cjs';\n\nconst logger = getLogger('wood:loader');\n\n// Node's ESM loader rejects raw absolute Windows paths (`C:\\...`) — they must\n// be file:// URLs. Bare specifiers and relative paths pass through unchanged.\nfunction toImportSpecifier(spec: string): string {\n return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;\n}\n\ninterface LoaderEntry {\n fn: (request: RequestData) => Promise<unknown>;\n mtime: number;\n filePath: string;\n}\n\nexport class LoaderRunner {\n private cache = new Map<string, LoaderEntry>();\n private componentDir: string;\n\n constructor(componentDir: string) {\n this.componentDir = componentDir;\n }\n\n /**\n * Discover the loader file for a view component.\n * Convention: for `pages/chat/main.svelte`, look for `pages/chat/main.load.ts`\n */\n private getLoaderPath(viewPath: string): string {\n const basePath = viewPath.replace(/\\.svelte$/, '.load.ts');\n return path.resolve(this.componentDir, basePath);\n }\n\n /**\n * Load a loader function with mtime-based cache invalidation.\n * Returns null when no loader file exists (not an error).\n */\n private async loadLoader(loaderPath: string): Promise<((request: RequestData) => Promise<unknown>) | null> {\n try {\n const stat = statSync(loaderPath);\n const mtime = stat.mtimeMs;\n\n const cached = this.cache.get(loaderPath);\n if (cached && cached.mtime === mtime) {\n return cached.fn;\n }\n\n // Dynamic import with cache-bust query for dev reloading\n const mod = await import(`${toImportSpecifier(loaderPath)}?t=${Date.now()}`);\n const fn = mod.default;\n\n if (typeof fn !== 'function') {\n logger.warn(`Loader at \"${loaderPath}\" does not export a default function`);\n return null;\n }\n\n logger.debug(`Loaded loader: ${loaderPath}`);\n this.cache.set(loaderPath, { fn, mtime, filePath: loaderPath });\n return fn;\n } catch {\n return null;\n }\n }\n\n /**\n * Execute all loaders for a matched route.\n * Layout loaders run in parallel, view loader runs after.\n */\n async executeLoaders(\n viewDef: ViewDef,\n requestData: RequestData,\n ): Promise<LoaderResult> {\n // Load layout loaders in parallel\n const layoutLoaderPromises = viewDef.layouts.map(async (layoutPath) => {\n const loaderPath = this.getLoaderPath(layoutPath);\n const loader = await this.loadLoader(loaderPath);\n if (loader) {\n return loader(requestData);\n }\n return undefined;\n });\n\n const layoutData = await Promise.all(layoutLoaderPromises);\n\n // Load view loader\n const viewLoaderPath = this.getLoaderPath(viewDef.viewPath);\n const viewLoader = await this.loadLoader(viewLoaderPath);\n const viewData = viewLoader ? await viewLoader(requestData) : {};\n\n logger.debug(`Executed loaders for view \"${viewDef.pageName}\": ${viewDef.layouts.length} layout(s)`);\n\n return {\n layoutData: layoutData.filter((d) => d !== undefined),\n viewData,\n };\n }\n\n /** Clear the loader cache. */\n clearCache(): void {\n this.cache.clear();\n logger.debug('Loader cache cleared');\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAiB;AACjB,sBAA8B;AAC9B,gBAAyB;AACzB,oBAA0B;AAI1B,MAAM,aAAS,yBAAU,aAAa;AAItC,SAAS,kBAAkB,MAAsB;AAC/C,SAAO,YAAAA,QAAK,WAAW,IAAI,QAAI,+BAAc,IAAI,EAAE,OAAO;AAC5D;AAQO,MAAM,aAAa;AAAA,EAIxB,YAAY,cAAsB;AAHlC,SAAQ,QAAQ,oBAAI,IAAyB;AAI3C,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,cAAc,UAA0B;AAC9C,UAAM,WAAW,SAAS,QAAQ,aAAa,UAAU;AACzD,WAAO,YAAAA,QAAK,QAAQ,KAAK,cAAc,QAAQ;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,WAAW,YAAkF;AACzG,QAAI;AACF,YAAM,WAAO,oBAAS,UAAU;AAChC,YAAM,QAAQ,KAAK;AAEnB,YAAM,SAAS,KAAK,MAAM,IAAI,UAAU;AACxC,UAAI,UAAU,OAAO,UAAU,OAAO;AACpC,eAAO,OAAO;AAAA,MAChB;AAGA,YAAM,MAAM,MAAM,OAAO,GAAG,kBAAkB,UAAU,CAAC,MAAM,KAAK,IAAI,CAAC;AACzE,YAAM,KAAK,IAAI;AAEf,UAAI,OAAO,OAAO,YAAY;AAC5B,eAAO,KAAK,cAAc,UAAU,sCAAsC;AAC1E,eAAO;AAAA,MACT;AAEA,aAAO,MAAM,kBAAkB,UAAU,EAAE;AAC3C,WAAK,MAAM,IAAI,YAAY,EAAE,IAAI,OAAO,UAAU,WAAW,CAAC;AAC9D,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eACJ,SACA,aACuB;AAEvB,UAAM,uBAAuB,QAAQ,QAAQ,IAAI,OAAO,eAAe;AACrE,YAAM,aAAa,KAAK,cAAc,UAAU;AAChD,YAAM,SAAS,MAAM,KAAK,WAAW,UAAU;AAC/C,UAAI,QAAQ;AACV,eAAO,OAAO,WAAW;AAAA,MAC3B;AACA,aAAO;AAAA,IACT,CAAC;AAED,UAAM,aAAa,MAAM,QAAQ,IAAI,oBAAoB;AAGzD,UAAM,iBAAiB,KAAK,cAAc,QAAQ,QAAQ;AAC1D,UAAM,aAAa,MAAM,KAAK,WAAW,cAAc;AACvD,UAAM,WAAW,aAAa,MAAM,WAAW,WAAW,IAAI,CAAC;AAE/D,WAAO,MAAM,8BAA8B,QAAQ,QAAQ,MAAM,QAAQ,QAAQ,MAAM,YAAY;AAEnG,WAAO;AAAA,MACL,YAAY,WAAW,OAAO,CAAC,MAAM,MAAM,MAAS;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,aAAmB;AACjB,SAAK,MAAM,MAAM;AACjB,WAAO,MAAM,sBAAsB;AAAA,EACrC;AACF;","names":["path"]}
1
+ {"version":3,"sources":["../../src/loader/loader_runner.ts"],"sourcesContent":["import path from 'path';\nimport { pathToFileURL } from 'node:url';\nimport { statSync } from 'fs';\nimport { getLogger } from '@noego/logger';\nimport type { RequestData, LoaderResult } from '../types/context.cjs';\nimport type { ViewDef } from '../types/views.cjs';\nimport {\n assertDataLoader,\n loaderCompanionCandidates,\n runViewLoaders,\n type DataLoader,\n} from './loader_orchestration.cjs';\n\nconst logger = getLogger('wood:loader');\n\n// Node's ESM loader rejects raw absolute Windows paths (`C:\\...`) — they must\n// be file:// URLs. Bare specifiers and relative paths pass through unchanged.\nfunction toImportSpecifier(spec: string): string {\n return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;\n}\n\ninterface LoaderEntry {\n fn: DataLoader;\n mtime: number;\n filePath: string;\n}\n\nfunction mtimeOf(filePath: string): number | null {\n try {\n return statSync(filePath).mtimeMs;\n } catch (error) {\n if ((error as { code?: string }).code === 'ENOENT') return null;\n throw error;\n }\n}\n\n/**\n * Main-process loader runner over the shared companion convention\n * (`pages/chat/main.svelte` → `pages/chat/main.load.ts`), executed through\n * the same orchestration the renderer and the Node frontend surface use.\n */\nexport class LoaderRunner {\n private cache = new Map<string, LoaderEntry>();\n private componentDir: string;\n\n constructor(componentDir: string) {\n this.componentDir = componentDir;\n }\n\n /**\n * Discover the loader file for a view component. Exactly one companion\n * may exist; more than one is ambiguous and an error, none is `null`.\n */\n private getLoaderPath(viewPath: string): { filePath: string; mtime: number } | null {\n const present: Array<{ filePath: string; mtime: number }> = [];\n for (const candidate of loaderCompanionCandidates(viewPath)) {\n const filePath = path.resolve(this.componentDir, candidate);\n const mtime = mtimeOf(filePath);\n if (mtime !== null) present.push({ filePath, mtime });\n }\n if (present.length > 1) {\n throw new Error(\n `Ambiguous loader for \"${viewPath}\": ${present.map((entry) => entry.filePath).join(', ')}; keep exactly one companion`,\n );\n }\n return present[0] ?? null;\n }\n\n /**\n * Load a loader function with mtime-based cache invalidation. Returns\n * `null` only when no companion exists; an import failure or a module\n * without a default loader function is an error.\n */\n private async loadLoader(viewPath: string): Promise<DataLoader | null> {\n const located = this.getLoaderPath(viewPath);\n if (!located) return null;\n const { filePath, mtime } = located;\n\n const cached = this.cache.get(filePath);\n if (cached && cached.mtime === mtime) {\n return cached.fn;\n }\n\n // Dynamic import with cache-bust query for dev reloading\n const mod = (await import(`${toImportSpecifier(filePath)}?t=${Date.now()}`)) as { default?: unknown };\n const fn = assertDataLoader(mod.default, `Loader at \"${filePath}\"`);\n\n logger.debug(`Loaded loader: ${filePath}`);\n this.cache.set(filePath, { fn, mtime, filePath });\n return fn;\n }\n\n /**\n * Execute all loaders for a matched route: layouts in stack order, then\n * the view (shared orchestration).\n */\n async executeLoaders(\n viewDef: ViewDef,\n requestData: RequestData,\n ): Promise<LoaderResult> {\n const layouts: Array<DataLoader | null> = [];\n for (const layoutPath of viewDef.layouts) layouts.push(await this.loadLoader(layoutPath));\n const view = await this.loadLoader(viewDef.viewPath);\n\n const result = await runViewLoaders({ layouts, view }, requestData);\n\n logger.debug(`Executed loaders for view \"${viewDef.pageName}\": ${viewDef.layouts.length} layout(s)`);\n\n return {\n layoutData: result.layouts.filter((entry) => entry !== null).map((entry) => entry.data),\n viewData: result.view ? result.view.data : {},\n };\n }\n\n /** Clear the loader cache. */\n clearCache(): void {\n this.cache.clear();\n logger.debug('Loader cache cleared');\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAiB;AACjB,sBAA8B;AAC9B,gBAAyB;AACzB,oBAA0B;AAG1B,kCAKO;AAEP,MAAM,aAAS,yBAAU,aAAa;AAItC,SAAS,kBAAkB,MAAsB;AAC/C,SAAO,YAAAA,QAAK,WAAW,IAAI,QAAI,+BAAc,IAAI,EAAE,OAAO;AAC5D;AAQA,SAAS,QAAQ,UAAiC;AAChD,MAAI;AACF,eAAO,oBAAS,QAAQ,EAAE;AAAA,EAC5B,SAAS,OAAO;AACd,QAAK,MAA4B,SAAS,SAAU,QAAO;AAC3D,UAAM;AAAA,EACR;AACF;AAOO,MAAM,aAAa;AAAA,EAIxB,YAAY,cAAsB;AAHlC,SAAQ,QAAQ,oBAAI,IAAyB;AAI3C,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,cAAc,UAA8D;AAClF,UAAM,UAAsD,CAAC;AAC7D,eAAW,iBAAa,uDAA0B,QAAQ,GAAG;AAC3D,YAAM,WAAW,YAAAA,QAAK,QAAQ,KAAK,cAAc,SAAS;AAC1D,YAAM,QAAQ,QAAQ,QAAQ;AAC9B,UAAI,UAAU,KAAM,SAAQ,KAAK,EAAE,UAAU,MAAM,CAAC;AAAA,IACtD;AACA,QAAI,QAAQ,SAAS,GAAG;AACtB,YAAM,IAAI;AAAA,QACR,yBAAyB,QAAQ,MAAM,QAAQ,IAAI,CAAC,UAAU,MAAM,QAAQ,EAAE,KAAK,IAAI,CAAC;AAAA,MAC1F;AAAA,IACF;AACA,WAAO,QAAQ,CAAC,KAAK;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,WAAW,UAA8C;AACrE,UAAM,UAAU,KAAK,cAAc,QAAQ;AAC3C,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,EAAE,UAAU,MAAM,IAAI;AAE5B,UAAM,SAAS,KAAK,MAAM,IAAI,QAAQ;AACtC,QAAI,UAAU,OAAO,UAAU,OAAO;AACpC,aAAO,OAAO;AAAA,IAChB;AAGA,UAAM,MAAO,MAAM,OAAO,GAAG,kBAAkB,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC;AACxE,UAAM,SAAK,8CAAiB,IAAI,SAAS,cAAc,QAAQ,GAAG;AAElE,WAAO,MAAM,kBAAkB,QAAQ,EAAE;AACzC,SAAK,MAAM,IAAI,UAAU,EAAE,IAAI,OAAO,SAAS,CAAC;AAChD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eACJ,SACA,aACuB;AACvB,UAAM,UAAoC,CAAC;AAC3C,eAAW,cAAc,QAAQ,QAAS,SAAQ,KAAK,MAAM,KAAK,WAAW,UAAU,CAAC;AACxF,UAAM,OAAO,MAAM,KAAK,WAAW,QAAQ,QAAQ;AAEnD,UAAM,SAAS,UAAM,4CAAe,EAAE,SAAS,KAAK,GAAG,WAAW;AAElE,WAAO,MAAM,8BAA8B,QAAQ,QAAQ,MAAM,QAAQ,QAAQ,MAAM,YAAY;AAEnG,WAAO;AAAA,MACL,YAAY,OAAO,QAAQ,OAAO,CAAC,UAAU,UAAU,IAAI,EAAE,IAAI,CAAC,UAAU,MAAM,IAAI;AAAA,MACtF,UAAU,OAAO,OAAO,OAAO,KAAK,OAAO,CAAC;AAAA,IAC9C;AAAA,EACF;AAAA;AAAA,EAGA,aAAmB;AACjB,SAAK,MAAM,MAAM;AACjB,WAAO,MAAM,sBAAsB;AAAA,EACrC;AACF;","names":["path"]}
@@ -1,23 +1,29 @@
1
1
  import { RequestData, LoaderResult } from '../types/context.cjs';
2
2
  import { ViewDef } from '../types/views.cjs';
3
3
 
4
+ /**
5
+ * Main-process loader runner over the shared companion convention
6
+ * (`pages/chat/main.svelte` → `pages/chat/main.load.ts`), executed through
7
+ * the same orchestration the renderer and the Node frontend surface use.
8
+ */
4
9
  declare class LoaderRunner {
5
10
  private cache;
6
11
  private componentDir;
7
12
  constructor(componentDir: string);
8
13
  /**
9
- * Discover the loader file for a view component.
10
- * Convention: for `pages/chat/main.svelte`, look for `pages/chat/main.load.ts`
14
+ * Discover the loader file for a view component. Exactly one companion
15
+ * may exist; more than one is ambiguous and an error, none is `null`.
11
16
  */
12
17
  private getLoaderPath;
13
18
  /**
14
- * Load a loader function with mtime-based cache invalidation.
15
- * Returns null when no loader file exists (not an error).
19
+ * Load a loader function with mtime-based cache invalidation. Returns
20
+ * `null` only when no companion exists; an import failure or a module
21
+ * without a default loader function is an error.
16
22
  */
17
23
  private loadLoader;
18
24
  /**
19
- * Execute all loaders for a matched route.
20
- * Layout loaders run in parallel, view loader runs after.
25
+ * Execute all loaders for a matched route: layouts in stack order, then
26
+ * the view (shared orchestration).
21
27
  */
22
28
  executeLoaders(viewDef: ViewDef, requestData: RequestData): Promise<LoaderResult>;
23
29
  /** Clear the loader cache. */
@@ -1,23 +1,29 @@
1
1
  import { RequestData, LoaderResult } from '../types/context.js';
2
2
  import { ViewDef } from '../types/views.js';
3
3
 
4
+ /**
5
+ * Main-process loader runner over the shared companion convention
6
+ * (`pages/chat/main.svelte` → `pages/chat/main.load.ts`), executed through
7
+ * the same orchestration the renderer and the Node frontend surface use.
8
+ */
4
9
  declare class LoaderRunner {
5
10
  private cache;
6
11
  private componentDir;
7
12
  constructor(componentDir: string);
8
13
  /**
9
- * Discover the loader file for a view component.
10
- * Convention: for `pages/chat/main.svelte`, look for `pages/chat/main.load.ts`
14
+ * Discover the loader file for a view component. Exactly one companion
15
+ * may exist; more than one is ambiguous and an error, none is `null`.
11
16
  */
12
17
  private getLoaderPath;
13
18
  /**
14
- * Load a loader function with mtime-based cache invalidation.
15
- * Returns null when no loader file exists (not an error).
19
+ * Load a loader function with mtime-based cache invalidation. Returns
20
+ * `null` only when no companion exists; an import failure or a module
21
+ * without a default loader function is an error.
16
22
  */
17
23
  private loadLoader;
18
24
  /**
19
- * Execute all loaders for a matched route.
20
- * Layout loaders run in parallel, view loader runs after.
25
+ * Execute all loaders for a matched route: layouts in stack order, then
26
+ * the view (shared orchestration).
21
27
  */
22
28
  executeLoaders(viewDef: ViewDef, requestData: RequestData): Promise<LoaderResult>;
23
29
  /** Clear the loader cache. */
@@ -2,69 +2,78 @@ import path from "path";
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { statSync } from "fs";
4
4
  import { getLogger } from "@noego/logger";
5
+ import {
6
+ assertDataLoader,
7
+ loaderCompanionCandidates,
8
+ runViewLoaders
9
+ } from "./loader_orchestration.js";
5
10
  const logger = getLogger("wood:loader");
6
11
  function toImportSpecifier(spec) {
7
12
  return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;
8
13
  }
14
+ function mtimeOf(filePath) {
15
+ try {
16
+ return statSync(filePath).mtimeMs;
17
+ } catch (error) {
18
+ if (error.code === "ENOENT") return null;
19
+ throw error;
20
+ }
21
+ }
9
22
  class LoaderRunner {
10
23
  constructor(componentDir) {
11
24
  this.cache = /* @__PURE__ */ new Map();
12
25
  this.componentDir = componentDir;
13
26
  }
14
27
  /**
15
- * Discover the loader file for a view component.
16
- * Convention: for `pages/chat/main.svelte`, look for `pages/chat/main.load.ts`
28
+ * Discover the loader file for a view component. Exactly one companion
29
+ * may exist; more than one is ambiguous and an error, none is `null`.
17
30
  */
18
31
  getLoaderPath(viewPath) {
19
- const basePath = viewPath.replace(/\.svelte$/, ".load.ts");
20
- return path.resolve(this.componentDir, basePath);
32
+ const present = [];
33
+ for (const candidate of loaderCompanionCandidates(viewPath)) {
34
+ const filePath = path.resolve(this.componentDir, candidate);
35
+ const mtime = mtimeOf(filePath);
36
+ if (mtime !== null) present.push({ filePath, mtime });
37
+ }
38
+ if (present.length > 1) {
39
+ throw new Error(
40
+ `Ambiguous loader for "${viewPath}": ${present.map((entry) => entry.filePath).join(", ")}; keep exactly one companion`
41
+ );
42
+ }
43
+ return present[0] ?? null;
21
44
  }
22
45
  /**
23
- * Load a loader function with mtime-based cache invalidation.
24
- * Returns null when no loader file exists (not an error).
46
+ * Load a loader function with mtime-based cache invalidation. Returns
47
+ * `null` only when no companion exists; an import failure or a module
48
+ * without a default loader function is an error.
25
49
  */
26
- async loadLoader(loaderPath) {
27
- try {
28
- const stat = statSync(loaderPath);
29
- const mtime = stat.mtimeMs;
30
- const cached = this.cache.get(loaderPath);
31
- if (cached && cached.mtime === mtime) {
32
- return cached.fn;
33
- }
34
- const mod = await import(`${toImportSpecifier(loaderPath)}?t=${Date.now()}`);
35
- const fn = mod.default;
36
- if (typeof fn !== "function") {
37
- logger.warn(`Loader at "${loaderPath}" does not export a default function`);
38
- return null;
39
- }
40
- logger.debug(`Loaded loader: ${loaderPath}`);
41
- this.cache.set(loaderPath, { fn, mtime, filePath: loaderPath });
42
- return fn;
43
- } catch {
44
- return null;
50
+ async loadLoader(viewPath) {
51
+ const located = this.getLoaderPath(viewPath);
52
+ if (!located) return null;
53
+ const { filePath, mtime } = located;
54
+ const cached = this.cache.get(filePath);
55
+ if (cached && cached.mtime === mtime) {
56
+ return cached.fn;
45
57
  }
58
+ const mod = await import(`${toImportSpecifier(filePath)}?t=${Date.now()}`);
59
+ const fn = assertDataLoader(mod.default, `Loader at "${filePath}"`);
60
+ logger.debug(`Loaded loader: ${filePath}`);
61
+ this.cache.set(filePath, { fn, mtime, filePath });
62
+ return fn;
46
63
  }
47
64
  /**
48
- * Execute all loaders for a matched route.
49
- * Layout loaders run in parallel, view loader runs after.
65
+ * Execute all loaders for a matched route: layouts in stack order, then
66
+ * the view (shared orchestration).
50
67
  */
51
68
  async executeLoaders(viewDef, requestData) {
52
- const layoutLoaderPromises = viewDef.layouts.map(async (layoutPath) => {
53
- const loaderPath = this.getLoaderPath(layoutPath);
54
- const loader = await this.loadLoader(loaderPath);
55
- if (loader) {
56
- return loader(requestData);
57
- }
58
- return void 0;
59
- });
60
- const layoutData = await Promise.all(layoutLoaderPromises);
61
- const viewLoaderPath = this.getLoaderPath(viewDef.viewPath);
62
- const viewLoader = await this.loadLoader(viewLoaderPath);
63
- const viewData = viewLoader ? await viewLoader(requestData) : {};
69
+ const layouts = [];
70
+ for (const layoutPath of viewDef.layouts) layouts.push(await this.loadLoader(layoutPath));
71
+ const view = await this.loadLoader(viewDef.viewPath);
72
+ const result = await runViewLoaders({ layouts, view }, requestData);
64
73
  logger.debug(`Executed loaders for view "${viewDef.pageName}": ${viewDef.layouts.length} layout(s)`);
65
74
  return {
66
- layoutData: layoutData.filter((d) => d !== void 0),
67
- viewData
75
+ layoutData: result.layouts.filter((entry) => entry !== null).map((entry) => entry.data),
76
+ viewData: result.view ? result.view.data : {}
68
77
  };
69
78
  }
70
79
  /** Clear the loader cache. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/loader/loader_runner.ts"],"sourcesContent":["import path from 'path';\nimport { pathToFileURL } from 'node:url';\nimport { statSync } from 'fs';\nimport { getLogger } from '@noego/logger';\nimport type { RequestData, LoaderResult } from '../types/context.js';\nimport type { ViewDef } from '../types/views.js';\n\nconst logger = getLogger('wood:loader');\n\n// Node's ESM loader rejects raw absolute Windows paths (`C:\\...`) — they must\n// be file:// URLs. Bare specifiers and relative paths pass through unchanged.\nfunction toImportSpecifier(spec: string): string {\n return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;\n}\n\ninterface LoaderEntry {\n fn: (request: RequestData) => Promise<unknown>;\n mtime: number;\n filePath: string;\n}\n\nexport class LoaderRunner {\n private cache = new Map<string, LoaderEntry>();\n private componentDir: string;\n\n constructor(componentDir: string) {\n this.componentDir = componentDir;\n }\n\n /**\n * Discover the loader file for a view component.\n * Convention: for `pages/chat/main.svelte`, look for `pages/chat/main.load.ts`\n */\n private getLoaderPath(viewPath: string): string {\n const basePath = viewPath.replace(/\\.svelte$/, '.load.ts');\n return path.resolve(this.componentDir, basePath);\n }\n\n /**\n * Load a loader function with mtime-based cache invalidation.\n * Returns null when no loader file exists (not an error).\n */\n private async loadLoader(loaderPath: string): Promise<((request: RequestData) => Promise<unknown>) | null> {\n try {\n const stat = statSync(loaderPath);\n const mtime = stat.mtimeMs;\n\n const cached = this.cache.get(loaderPath);\n if (cached && cached.mtime === mtime) {\n return cached.fn;\n }\n\n // Dynamic import with cache-bust query for dev reloading\n const mod = await import(`${toImportSpecifier(loaderPath)}?t=${Date.now()}`);\n const fn = mod.default;\n\n if (typeof fn !== 'function') {\n logger.warn(`Loader at \"${loaderPath}\" does not export a default function`);\n return null;\n }\n\n logger.debug(`Loaded loader: ${loaderPath}`);\n this.cache.set(loaderPath, { fn, mtime, filePath: loaderPath });\n return fn;\n } catch {\n return null;\n }\n }\n\n /**\n * Execute all loaders for a matched route.\n * Layout loaders run in parallel, view loader runs after.\n */\n async executeLoaders(\n viewDef: ViewDef,\n requestData: RequestData,\n ): Promise<LoaderResult> {\n // Load layout loaders in parallel\n const layoutLoaderPromises = viewDef.layouts.map(async (layoutPath) => {\n const loaderPath = this.getLoaderPath(layoutPath);\n const loader = await this.loadLoader(loaderPath);\n if (loader) {\n return loader(requestData);\n }\n return undefined;\n });\n\n const layoutData = await Promise.all(layoutLoaderPromises);\n\n // Load view loader\n const viewLoaderPath = this.getLoaderPath(viewDef.viewPath);\n const viewLoader = await this.loadLoader(viewLoaderPath);\n const viewData = viewLoader ? await viewLoader(requestData) : {};\n\n logger.debug(`Executed loaders for view \"${viewDef.pageName}\": ${viewDef.layouts.length} layout(s)`);\n\n return {\n layoutData: layoutData.filter((d) => d !== undefined),\n viewData,\n };\n }\n\n /** Clear the loader cache. */\n clearCache(): void {\n this.cache.clear();\n logger.debug('Loader cache cleared');\n }\n}\n"],"mappings":"AAAA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAI1B,MAAM,SAAS,UAAU,aAAa;AAItC,SAAS,kBAAkB,MAAsB;AAC/C,SAAO,KAAK,WAAW,IAAI,IAAI,cAAc,IAAI,EAAE,OAAO;AAC5D;AAQO,MAAM,aAAa;AAAA,EAIxB,YAAY,cAAsB;AAHlC,SAAQ,QAAQ,oBAAI,IAAyB;AAI3C,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,cAAc,UAA0B;AAC9C,UAAM,WAAW,SAAS,QAAQ,aAAa,UAAU;AACzD,WAAO,KAAK,QAAQ,KAAK,cAAc,QAAQ;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,WAAW,YAAkF;AACzG,QAAI;AACF,YAAM,OAAO,SAAS,UAAU;AAChC,YAAM,QAAQ,KAAK;AAEnB,YAAM,SAAS,KAAK,MAAM,IAAI,UAAU;AACxC,UAAI,UAAU,OAAO,UAAU,OAAO;AACpC,eAAO,OAAO;AAAA,MAChB;AAGA,YAAM,MAAM,MAAM,OAAO,GAAG,kBAAkB,UAAU,CAAC,MAAM,KAAK,IAAI,CAAC;AACzE,YAAM,KAAK,IAAI;AAEf,UAAI,OAAO,OAAO,YAAY;AAC5B,eAAO,KAAK,cAAc,UAAU,sCAAsC;AAC1E,eAAO;AAAA,MACT;AAEA,aAAO,MAAM,kBAAkB,UAAU,EAAE;AAC3C,WAAK,MAAM,IAAI,YAAY,EAAE,IAAI,OAAO,UAAU,WAAW,CAAC;AAC9D,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eACJ,SACA,aACuB;AAEvB,UAAM,uBAAuB,QAAQ,QAAQ,IAAI,OAAO,eAAe;AACrE,YAAM,aAAa,KAAK,cAAc,UAAU;AAChD,YAAM,SAAS,MAAM,KAAK,WAAW,UAAU;AAC/C,UAAI,QAAQ;AACV,eAAO,OAAO,WAAW;AAAA,MAC3B;AACA,aAAO;AAAA,IACT,CAAC;AAED,UAAM,aAAa,MAAM,QAAQ,IAAI,oBAAoB;AAGzD,UAAM,iBAAiB,KAAK,cAAc,QAAQ,QAAQ;AAC1D,UAAM,aAAa,MAAM,KAAK,WAAW,cAAc;AACvD,UAAM,WAAW,aAAa,MAAM,WAAW,WAAW,IAAI,CAAC;AAE/D,WAAO,MAAM,8BAA8B,QAAQ,QAAQ,MAAM,QAAQ,QAAQ,MAAM,YAAY;AAEnG,WAAO;AAAA,MACL,YAAY,WAAW,OAAO,CAAC,MAAM,MAAM,MAAS;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,aAAmB;AACjB,SAAK,MAAM,MAAM;AACjB,WAAO,MAAM,sBAAsB;AAAA,EACrC;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/loader/loader_runner.ts"],"sourcesContent":["import path from 'path';\nimport { pathToFileURL } from 'node:url';\nimport { statSync } from 'fs';\nimport { getLogger } from '@noego/logger';\nimport type { RequestData, LoaderResult } from '../types/context.js';\nimport type { ViewDef } from '../types/views.js';\nimport {\n assertDataLoader,\n loaderCompanionCandidates,\n runViewLoaders,\n type DataLoader,\n} from './loader_orchestration.js';\n\nconst logger = getLogger('wood:loader');\n\n// Node's ESM loader rejects raw absolute Windows paths (`C:\\...`) — they must\n// be file:// URLs. Bare specifiers and relative paths pass through unchanged.\nfunction toImportSpecifier(spec: string): string {\n return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;\n}\n\ninterface LoaderEntry {\n fn: DataLoader;\n mtime: number;\n filePath: string;\n}\n\nfunction mtimeOf(filePath: string): number | null {\n try {\n return statSync(filePath).mtimeMs;\n } catch (error) {\n if ((error as { code?: string }).code === 'ENOENT') return null;\n throw error;\n }\n}\n\n/**\n * Main-process loader runner over the shared companion convention\n * (`pages/chat/main.svelte` → `pages/chat/main.load.ts`), executed through\n * the same orchestration the renderer and the Node frontend surface use.\n */\nexport class LoaderRunner {\n private cache = new Map<string, LoaderEntry>();\n private componentDir: string;\n\n constructor(componentDir: string) {\n this.componentDir = componentDir;\n }\n\n /**\n * Discover the loader file for a view component. Exactly one companion\n * may exist; more than one is ambiguous and an error, none is `null`.\n */\n private getLoaderPath(viewPath: string): { filePath: string; mtime: number } | null {\n const present: Array<{ filePath: string; mtime: number }> = [];\n for (const candidate of loaderCompanionCandidates(viewPath)) {\n const filePath = path.resolve(this.componentDir, candidate);\n const mtime = mtimeOf(filePath);\n if (mtime !== null) present.push({ filePath, mtime });\n }\n if (present.length > 1) {\n throw new Error(\n `Ambiguous loader for \"${viewPath}\": ${present.map((entry) => entry.filePath).join(', ')}; keep exactly one companion`,\n );\n }\n return present[0] ?? null;\n }\n\n /**\n * Load a loader function with mtime-based cache invalidation. Returns\n * `null` only when no companion exists; an import failure or a module\n * without a default loader function is an error.\n */\n private async loadLoader(viewPath: string): Promise<DataLoader | null> {\n const located = this.getLoaderPath(viewPath);\n if (!located) return null;\n const { filePath, mtime } = located;\n\n const cached = this.cache.get(filePath);\n if (cached && cached.mtime === mtime) {\n return cached.fn;\n }\n\n // Dynamic import with cache-bust query for dev reloading\n const mod = (await import(`${toImportSpecifier(filePath)}?t=${Date.now()}`)) as { default?: unknown };\n const fn = assertDataLoader(mod.default, `Loader at \"${filePath}\"`);\n\n logger.debug(`Loaded loader: ${filePath}`);\n this.cache.set(filePath, { fn, mtime, filePath });\n return fn;\n }\n\n /**\n * Execute all loaders for a matched route: layouts in stack order, then\n * the view (shared orchestration).\n */\n async executeLoaders(\n viewDef: ViewDef,\n requestData: RequestData,\n ): Promise<LoaderResult> {\n const layouts: Array<DataLoader | null> = [];\n for (const layoutPath of viewDef.layouts) layouts.push(await this.loadLoader(layoutPath));\n const view = await this.loadLoader(viewDef.viewPath);\n\n const result = await runViewLoaders({ layouts, view }, requestData);\n\n logger.debug(`Executed loaders for view \"${viewDef.pageName}\": ${viewDef.layouts.length} layout(s)`);\n\n return {\n layoutData: result.layouts.filter((entry) => entry !== null).map((entry) => entry.data),\n viewData: result.view ? result.view.data : {},\n };\n }\n\n /** Clear the loader cache. */\n clearCache(): void {\n this.cache.clear();\n logger.debug('Loader cache cleared');\n }\n}\n"],"mappings":"AAAA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAG1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,MAAM,SAAS,UAAU,aAAa;AAItC,SAAS,kBAAkB,MAAsB;AAC/C,SAAO,KAAK,WAAW,IAAI,IAAI,cAAc,IAAI,EAAE,OAAO;AAC5D;AAQA,SAAS,QAAQ,UAAiC;AAChD,MAAI;AACF,WAAO,SAAS,QAAQ,EAAE;AAAA,EAC5B,SAAS,OAAO;AACd,QAAK,MAA4B,SAAS,SAAU,QAAO;AAC3D,UAAM;AAAA,EACR;AACF;AAOO,MAAM,aAAa;AAAA,EAIxB,YAAY,cAAsB;AAHlC,SAAQ,QAAQ,oBAAI,IAAyB;AAI3C,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,cAAc,UAA8D;AAClF,UAAM,UAAsD,CAAC;AAC7D,eAAW,aAAa,0BAA0B,QAAQ,GAAG;AAC3D,YAAM,WAAW,KAAK,QAAQ,KAAK,cAAc,SAAS;AAC1D,YAAM,QAAQ,QAAQ,QAAQ;AAC9B,UAAI,UAAU,KAAM,SAAQ,KAAK,EAAE,UAAU,MAAM,CAAC;AAAA,IACtD;AACA,QAAI,QAAQ,SAAS,GAAG;AACtB,YAAM,IAAI;AAAA,QACR,yBAAyB,QAAQ,MAAM,QAAQ,IAAI,CAAC,UAAU,MAAM,QAAQ,EAAE,KAAK,IAAI,CAAC;AAAA,MAC1F;AAAA,IACF;AACA,WAAO,QAAQ,CAAC,KAAK;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,WAAW,UAA8C;AACrE,UAAM,UAAU,KAAK,cAAc,QAAQ;AAC3C,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,EAAE,UAAU,MAAM,IAAI;AAE5B,UAAM,SAAS,KAAK,MAAM,IAAI,QAAQ;AACtC,QAAI,UAAU,OAAO,UAAU,OAAO;AACpC,aAAO,OAAO;AAAA,IAChB;AAGA,UAAM,MAAO,MAAM,OAAO,GAAG,kBAAkB,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC;AACxE,UAAM,KAAK,iBAAiB,IAAI,SAAS,cAAc,QAAQ,GAAG;AAElE,WAAO,MAAM,kBAAkB,QAAQ,EAAE;AACzC,SAAK,MAAM,IAAI,UAAU,EAAE,IAAI,OAAO,SAAS,CAAC;AAChD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eACJ,SACA,aACuB;AACvB,UAAM,UAAoC,CAAC;AAC3C,eAAW,cAAc,QAAQ,QAAS,SAAQ,KAAK,MAAM,KAAK,WAAW,UAAU,CAAC;AACxF,UAAM,OAAO,MAAM,KAAK,WAAW,QAAQ,QAAQ;AAEnD,UAAM,SAAS,MAAM,eAAe,EAAE,SAAS,KAAK,GAAG,WAAW;AAElE,WAAO,MAAM,8BAA8B,QAAQ,QAAQ,MAAM,QAAQ,QAAQ,MAAM,YAAY;AAEnG,WAAO;AAAA,MACL,YAAY,OAAO,QAAQ,OAAO,CAAC,UAAU,UAAU,IAAI,EAAE,IAAI,CAAC,UAAU,MAAM,IAAI;AAAA,MACtF,UAAU,OAAO,OAAO,OAAO,KAAK,OAAO,CAAC;AAAA,IAC9C;AAAA,EACF;AAAA;AAAA,EAGA,aAAmB;AACjB,SAAK,MAAM,MAAM;AACjB,WAAO,MAAM,sBAAsB;AAAA,EACrC;AACF;","names":[]}