@noego/wood 0.5.0 → 0.6.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 (142) hide show
  1. package/README.md +31 -0
  2. package/dist/client/index.cjs +18 -11
  3. package/dist/client/index.cjs.map +1 -1
  4. package/dist/client/index.d.cts +21 -1
  5. package/dist/client/index.d.ts +21 -1
  6. package/dist/client/index.js +17 -11
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/codegen/renderer_app_generator.cjs +2 -9
  9. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  10. package/dist/codegen/renderer_app_generator.js +2 -9
  11. package/dist/codegen/renderer_app_generator.js.map +1 -1
  12. package/dist/controller/cleanup.cjs +39 -0
  13. package/dist/controller/cleanup.cjs.map +1 -0
  14. package/dist/controller/cleanup.d.cts +4 -0
  15. package/dist/controller/cleanup.d.ts +4 -0
  16. package/dist/controller/cleanup.js +15 -0
  17. package/dist/controller/cleanup.js.map +1 -0
  18. package/dist/controller/controller_lifecycle.cjs +26 -2
  19. package/dist/controller/controller_lifecycle.cjs.map +1 -1
  20. package/dist/controller/controller_lifecycle.d.cts +3 -1
  21. package/dist/controller/controller_lifecycle.d.ts +3 -1
  22. package/dist/controller/controller_lifecycle.js +24 -1
  23. package/dist/controller/controller_lifecycle.js.map +1 -1
  24. package/dist/frontend/frontend_lifecycle.cjs +5 -7
  25. package/dist/frontend/frontend_lifecycle.cjs.map +1 -1
  26. package/dist/frontend/frontend_lifecycle.d.cts +1 -14
  27. package/dist/frontend/frontend_lifecycle.d.ts +1 -14
  28. package/dist/frontend/frontend_lifecycle.js +5 -7
  29. package/dist/frontend/frontend_lifecycle.js.map +1 -1
  30. package/dist/frontend/frontend_surface.cjs +130 -104
  31. package/dist/frontend/frontend_surface.cjs.map +1 -1
  32. package/dist/frontend/frontend_surface.js +131 -105
  33. package/dist/frontend/frontend_surface.js.map +1 -1
  34. package/dist/frontend/work_registry.cjs +29 -4
  35. package/dist/frontend/work_registry.cjs.map +1 -1
  36. package/dist/frontend/work_registry.d.cts +2 -0
  37. package/dist/frontend/work_registry.d.ts +2 -0
  38. package/dist/frontend/work_registry.js +29 -4
  39. package/dist/frontend/work_registry.js.map +1 -1
  40. package/dist/index.d.cts +2 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/ipc/index.cjs +6 -2
  43. package/dist/ipc/index.cjs.map +1 -1
  44. package/dist/ipc/index.d.cts +9 -0
  45. package/dist/ipc/index.d.ts +9 -0
  46. package/dist/ipc/index.js +6 -2
  47. package/dist/ipc/index.js.map +1 -1
  48. package/dist/main/index.cjs +53 -35
  49. package/dist/main/index.cjs.map +1 -1
  50. package/dist/main/index.d.cts +11 -2
  51. package/dist/main/index.d.ts +11 -2
  52. package/dist/main/index.js +56 -40
  53. package/dist/main/index.js.map +1 -1
  54. package/dist/main/startup_failure_handler.cjs +67 -0
  55. package/dist/main/startup_failure_handler.cjs.map +1 -0
  56. package/dist/main/startup_failure_handler.d.cts +16 -0
  57. package/dist/main/startup_failure_handler.d.ts +16 -0
  58. package/dist/main/startup_failure_handler.js +43 -0
  59. package/dist/main/startup_failure_handler.js.map +1 -0
  60. package/dist/router/index.d.cts +3 -0
  61. package/dist/router/index.d.ts +3 -0
  62. package/dist/router/wood_router.cjs +204 -123
  63. package/dist/router/wood_router.cjs.map +1 -1
  64. package/dist/router/wood_router.d.cts +20 -0
  65. package/dist/router/wood_router.d.ts +20 -0
  66. package/dist/router/wood_router.js +206 -125
  67. package/dist/router/wood_router.js.map +1 -1
  68. package/dist/runtime/index.cjs +13 -0
  69. package/dist/runtime/index.cjs.map +1 -1
  70. package/dist/runtime/index.d.cts +8 -4
  71. package/dist/runtime/index.d.ts +8 -4
  72. package/dist/runtime/index.js +8 -0
  73. package/dist/runtime/index.js.map +1 -1
  74. package/dist/runtime/renderer_config_extension.cjs +1 -2
  75. package/dist/runtime/renderer_config_extension.cjs.map +1 -1
  76. package/dist/runtime/renderer_config_extension.d.cts +1 -1
  77. package/dist/runtime/renderer_config_extension.d.ts +1 -1
  78. package/dist/runtime/renderer_config_extension.js +1 -2
  79. package/dist/runtime/renderer_config_extension.js.map +1 -1
  80. package/dist/runtime/renderer_log_dispatcher.cjs +2 -0
  81. package/dist/runtime/renderer_log_dispatcher.cjs.map +1 -1
  82. package/dist/runtime/renderer_log_dispatcher.d.cts +1 -1
  83. package/dist/runtime/renderer_log_dispatcher.d.ts +1 -1
  84. package/dist/runtime/renderer_log_dispatcher.js +1 -0
  85. package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
  86. package/dist/runtime/runtime.cjs +107 -57
  87. package/dist/runtime/runtime.cjs.map +1 -1
  88. package/dist/runtime/runtime.d.cts +12 -0
  89. package/dist/runtime/runtime.d.ts +12 -0
  90. package/dist/runtime/runtime.js +110 -59
  91. package/dist/runtime/runtime.js.map +1 -1
  92. package/dist/runtime/runtime_module.cjs +58 -0
  93. package/dist/runtime/runtime_module.cjs.map +1 -0
  94. package/dist/runtime/runtime_module.d.cts +22 -0
  95. package/dist/runtime/runtime_module.d.ts +22 -0
  96. package/dist/runtime/runtime_module.js +30 -0
  97. package/dist/runtime/runtime_module.js.map +1 -0
  98. package/dist/testing/create_controller_harness.d.cts +3 -0
  99. package/dist/testing/create_controller_harness.d.ts +3 -0
  100. package/dist/testing/create_router_bridge.d.cts +3 -0
  101. package/dist/testing/create_router_bridge.d.ts +3 -0
  102. package/dist/testing/create_test_router.d.cts +3 -0
  103. package/dist/testing/create_test_router.d.ts +3 -0
  104. package/dist/testing/index.cjs +3 -0
  105. package/dist/testing/index.cjs.map +1 -1
  106. package/dist/testing/index.d.cts +6 -1
  107. package/dist/testing/index.d.ts +6 -1
  108. package/dist/testing/index.js +2 -0
  109. package/dist/testing/index.js.map +1 -1
  110. package/dist/testing/registration_only_root.cjs +168 -0
  111. package/dist/testing/registration_only_root.cjs.map +1 -0
  112. package/dist/testing/registration_only_root.d.cts +46 -0
  113. package/dist/testing/registration_only_root.d.ts +46 -0
  114. package/dist/testing/registration_only_root.js +141 -0
  115. package/dist/testing/registration_only_root.js.map +1 -0
  116. package/dist/testing/test_wood_config.cjs +382 -70
  117. package/dist/testing/test_wood_config.cjs.map +1 -1
  118. package/dist/testing/test_wood_config.d.cts +25 -1
  119. package/dist/testing/test_wood_config.d.ts +25 -1
  120. package/dist/testing/test_wood_config.js +388 -71
  121. package/dist/testing/test_wood_config.js.map +1 -1
  122. package/dist/testing/wood_dom_animation.cjs +213 -0
  123. package/dist/testing/wood_dom_animation.cjs.map +1 -0
  124. package/dist/testing/wood_dom_animation.d.cts +89 -0
  125. package/dist/testing/wood_dom_animation.d.ts +89 -0
  126. package/dist/testing/wood_dom_animation.js +185 -0
  127. package/dist/testing/wood_dom_animation.js.map +1 -0
  128. package/dist/testing/wood_env_renderer.cjs +81 -32
  129. package/dist/testing/wood_env_renderer.cjs.map +1 -1
  130. package/dist/testing/wood_env_renderer.d.cts +32 -1
  131. package/dist/testing/wood_env_renderer.d.ts +32 -1
  132. package/dist/testing/wood_env_renderer.js +84 -32
  133. package/dist/testing/wood_env_renderer.js.map +1 -1
  134. package/dist/transport/in_process_router_transport.d.cts +3 -0
  135. package/dist/transport/in_process_router_transport.d.ts +3 -0
  136. package/dist/transport/index.d.cts +3 -0
  137. package/dist/transport/index.d.ts +3 -0
  138. package/package.json +8 -6
  139. package/src/components/NavigationShell.svelte +12 -7
  140. package/src/controller/cleanup.ts +8 -0
  141. package/src/controller/controller_lifecycle.ts +24 -0
  142. package/src/navigation/layout_preservation.ts +45 -0
@@ -0,0 +1,185 @@
1
+ const PATCHES = /* @__PURE__ */ new WeakMap();
2
+ function defaultTimers() {
3
+ return {
4
+ setTimeout: (callback, ms) => globalThis.setTimeout(callback, ms),
5
+ clearTimeout: (handle) => globalThis.clearTimeout(handle),
6
+ now: () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now()
7
+ };
8
+ }
9
+ function toMs(value) {
10
+ const n = typeof value === "number" ? value : Number(value);
11
+ return Number.isFinite(n) && n > 0 ? n : 0;
12
+ }
13
+ function parseTiming(options) {
14
+ if (typeof options === "number") return { delay: 0, duration: toMs(options), fill: "none", easing: "linear" };
15
+ const o = options && typeof options === "object" ? options : {};
16
+ return {
17
+ delay: toMs(o.delay),
18
+ duration: toMs(o.duration),
19
+ fill: typeof o.fill === "string" ? o.fill : "none",
20
+ easing: typeof o.easing === "string" ? o.easing : "linear"
21
+ };
22
+ }
23
+ class WoodTestAnimation {
24
+ constructor(target, keyframes, timing, timers, onSettled) {
25
+ this.target = target;
26
+ this.keyframes = keyframes;
27
+ this.timing = timing;
28
+ this.timers = timers;
29
+ this.onSettled = onSettled;
30
+ this.onfinish = null;
31
+ this.oncancel = null;
32
+ this.playbackRate = 1;
33
+ this.pending = false;
34
+ this.id = "";
35
+ this.state = "running";
36
+ this.timer = null;
37
+ this.startedAt = timers.now();
38
+ this.finished = new Promise((resolve) => {
39
+ this.resolveFinished = resolve;
40
+ });
41
+ const total = timing.delay + timing.duration;
42
+ this.effect = {
43
+ target,
44
+ getKeyframes: () => keyframes,
45
+ getTiming: () => ({ delay: timing.delay, duration: timing.duration, fill: timing.fill, easing: timing.easing }),
46
+ getComputedTiming: () => ({
47
+ delay: timing.delay,
48
+ duration: timing.duration,
49
+ activeDuration: timing.duration,
50
+ endTime: total,
51
+ fill: timing.fill,
52
+ easing: timing.easing
53
+ })
54
+ };
55
+ this.timer = timers.setTimeout(() => {
56
+ this.timer = null;
57
+ this.complete();
58
+ }, total);
59
+ }
60
+ get playState() {
61
+ return this.state;
62
+ }
63
+ get startTime() {
64
+ return this.state === "idle" ? null : this.startedAt;
65
+ }
66
+ /** Elapsed time in the active interval (delay counts down from negative), clamped like a finished animation. */
67
+ get currentTime() {
68
+ if (this.state === "idle") return null;
69
+ if (this.state === "finished") return this.timing.duration;
70
+ const elapsed = this.timers.now() - this.startedAt - this.timing.delay;
71
+ return Math.min(Math.max(elapsed, 0), this.timing.duration);
72
+ }
73
+ set currentTime(value) {
74
+ if (value === null) return;
75
+ this.startedAt = this.timers.now() - this.timing.delay - toMs(value);
76
+ }
77
+ /** Cancels the animation: no completion, `playState` becomes `idle`, owned timer cleared. */
78
+ cancel() {
79
+ if (this.state === "idle") return;
80
+ const wasRunning = this.state === "running";
81
+ this.clearTimer();
82
+ this.state = "idle";
83
+ this.onSettled(this);
84
+ if (wasRunning) this.oncancel?.({ type: "cancel", target: this, currentTime: null });
85
+ }
86
+ /** Completes now (still notifies `onfinish` synchronously, as an explicit caller request). */
87
+ finish() {
88
+ if (this.state !== "running") return;
89
+ this.clearTimer();
90
+ this.complete();
91
+ }
92
+ play() {
93
+ if (this.state === "running") return;
94
+ this.startedAt = this.timers.now();
95
+ this.state = "running";
96
+ this.timer = this.timers.setTimeout(() => {
97
+ this.timer = null;
98
+ this.complete();
99
+ }, this.timing.delay + this.timing.duration);
100
+ }
101
+ pause() {
102
+ }
103
+ /** Called by the owning realm on release; never fires completion. */
104
+ dispose() {
105
+ this.clearTimer();
106
+ this.state = "idle";
107
+ }
108
+ clearTimer() {
109
+ if (this.timer !== null) {
110
+ this.timers.clearTimeout(this.timer);
111
+ this.timer = null;
112
+ }
113
+ }
114
+ complete() {
115
+ if (this.state !== "running") return;
116
+ this.state = "finished";
117
+ this.onSettled(this);
118
+ this.resolveFinished(this);
119
+ this.onfinish?.({ type: "finish", target: this, currentTime: this.timing.duration });
120
+ }
121
+ }
122
+ function animationRealmOf(target) {
123
+ const view = target?.ownerDocument?.defaultView;
124
+ const ctor = view?.Element ?? globalThis.Element;
125
+ const proto = ctor?.prototype;
126
+ return proto && typeof proto === "object" ? proto : null;
127
+ }
128
+ function hasNativeAnimate(proto) {
129
+ return typeof proto.animate === "function";
130
+ }
131
+ function animationLeaseCount(proto) {
132
+ return PATCHES.get(proto)?.leases ?? 0;
133
+ }
134
+ function install(proto, timers) {
135
+ const descriptor = Object.getOwnPropertyDescriptor(proto, "animate");
136
+ const active = /* @__PURE__ */ new Set();
137
+ const animate = function(keyframes, options) {
138
+ const animation = new WoodTestAnimation(this, keyframes, parseTiming(options), timers, (a) => active.delete(a));
139
+ active.add(animation);
140
+ return animation;
141
+ };
142
+ Object.defineProperty(proto, "animate", { value: animate, configurable: true, writable: true, enumerable: false });
143
+ const patch = { leases: 0, descriptor, animate, active, timers };
144
+ PATCHES.set(proto, patch);
145
+ return patch;
146
+ }
147
+ function uninstall(proto, patch) {
148
+ PATCHES.delete(proto);
149
+ for (const animation of [...patch.active]) animation.dispose();
150
+ patch.active.clear();
151
+ if (proto.animate !== patch.animate) return;
152
+ if (patch.descriptor) Object.defineProperty(proto, "animate", patch.descriptor);
153
+ else delete proto.animate;
154
+ }
155
+ function leaseDomAnimation(target, timers = defaultTimers()) {
156
+ const proto = animationRealmOf(target);
157
+ if (!proto) return { adapted: false, realm: null, release() {
158
+ } };
159
+ let patch = PATCHES.get(proto);
160
+ if (!patch) {
161
+ if (hasNativeAnimate(proto)) return { adapted: false, realm: proto, release() {
162
+ } };
163
+ patch = install(proto, timers);
164
+ }
165
+ patch.leases += 1;
166
+ let released = false;
167
+ return {
168
+ adapted: true,
169
+ realm: proto,
170
+ release() {
171
+ if (released) return;
172
+ released = true;
173
+ patch.leases -= 1;
174
+ if (patch.leases <= 0) uninstall(proto, patch);
175
+ }
176
+ };
177
+ }
178
+ export {
179
+ WoodTestAnimation,
180
+ animationLeaseCount,
181
+ animationRealmOf,
182
+ hasNativeAnimate,
183
+ leaseDomAnimation
184
+ };
185
+ //# sourceMappingURL=wood_dom_animation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/testing/wood_dom_animation.ts"],"sourcesContent":["/**\n * Test-only Web Animations adapter for DOM environments that lack\n * `Element.prototype.animate` (happy-dom). Svelte 5 transitions call\n * `element.animate(keyframes, { duration, fill })` twice per transition — a\n * dummy animation lasting the transition's delay, then the real one — and rely\n * on `onfinish`, `cancel()`, `currentTime`, `playState` and `effect`.\n *\n * The adapter is timing-respecting: `onfinish` fires asynchronously after\n * `delay + duration` on the test's timers (real or fake), never immediately —\n * an immediately-finishing shim completes outros early and wedges real\n * round-trips. `cancel()` suppresses completion.\n *\n * Leases are reference-counted per realm (`Element.prototype` of the target's\n * `ownerDocument.defaultView`). The first lease installs the adapter only when\n * `animate` is absent (native/polyfilled `animate` is never touched); the last\n * release cancels every owned animation, clears its timers and restores the\n * original property descriptor. This module is part of the testing entry and\n * must never be reached by production or browser runtime code.\n */\n\nexport interface WoodAnimationTimers {\n setTimeout(callback: () => void, ms: number): unknown;\n clearTimeout(handle: unknown): void;\n now(): number;\n}\n\nexport interface WoodAnimationLease {\n /** True when this lease installed or shares the adapter (false: native `animate` present or no realm). */\n readonly adapted: boolean;\n /** The `Element.prototype` this lease refers to (null when no realm could be resolved). */\n readonly realm: object | null;\n /** Release the lease; idempotent. */\n release(): void;\n}\n\nexport type WoodAnimationPlayState = 'idle' | 'running' | 'finished';\n\n/** Normalised effect timing derived from the `animate()` options. */\nexport interface WoodAnimationTiming {\n delay: number;\n duration: number;\n fill: string;\n easing: string;\n}\n\ninterface RealmPatch {\n leases: number;\n readonly descriptor: PropertyDescriptor | undefined;\n readonly animate: (this: unknown, keyframes: unknown, options?: unknown) => WoodTestAnimation;\n readonly active: Set<WoodTestAnimation>;\n readonly timers: WoodAnimationTimers;\n}\n\nconst PATCHES = new WeakMap<object, RealmPatch>();\n\nfunction defaultTimers(): WoodAnimationTimers {\n return {\n setTimeout: (callback, ms) => globalThis.setTimeout(callback, ms),\n clearTimeout: (handle) => globalThis.clearTimeout(handle as ReturnType<typeof setTimeout>),\n now: () => (typeof performance !== 'undefined' && typeof performance.now === 'function'\n ? performance.now()\n : Date.now()),\n };\n}\n\nfunction toMs(value: unknown): number {\n const n = typeof value === 'number' ? value : Number(value);\n return Number.isFinite(n) && n > 0 ? n : 0;\n}\n\nfunction parseTiming(options: unknown): WoodAnimationTiming {\n if (typeof options === 'number') return { delay: 0, duration: toMs(options), fill: 'none', easing: 'linear' };\n const o = (options && typeof options === 'object' ? options : {}) as Record<string, unknown>;\n return {\n delay: toMs(o.delay),\n duration: toMs(o.duration),\n fill: typeof o.fill === 'string' ? o.fill : 'none',\n easing: typeof o.easing === 'string' ? o.easing : 'linear',\n };\n}\n\n/** Minimal `Animation` stand-in honouring the members Svelte transitions use. */\nexport class WoodTestAnimation {\n onfinish: ((event: unknown) => void) | null = null;\n oncancel: ((event: unknown) => void) | null = null;\n effect: unknown;\n playbackRate = 1;\n pending = false;\n id = '';\n readonly finished: Promise<WoodTestAnimation>;\n\n private state: WoodAnimationPlayState = 'running';\n private startedAt: number;\n private timer: unknown = null;\n private resolveFinished!: (animation: WoodTestAnimation) => void;\n\n constructor(\n readonly target: unknown,\n readonly keyframes: unknown,\n readonly timing: WoodAnimationTiming,\n private readonly timers: WoodAnimationTimers,\n private readonly onSettled: (animation: WoodTestAnimation) => void,\n ) {\n this.startedAt = timers.now();\n this.finished = new Promise((resolve) => { this.resolveFinished = resolve; });\n const total = timing.delay + timing.duration;\n this.effect = {\n target,\n getKeyframes: () => keyframes,\n getTiming: () => ({ delay: timing.delay, duration: timing.duration, fill: timing.fill, easing: timing.easing }),\n getComputedTiming: () => ({\n delay: timing.delay,\n duration: timing.duration,\n activeDuration: timing.duration,\n endTime: total,\n fill: timing.fill,\n easing: timing.easing,\n }),\n };\n this.timer = timers.setTimeout(() => {\n this.timer = null;\n this.complete();\n }, total);\n }\n\n get playState(): WoodAnimationPlayState {\n return this.state;\n }\n\n get startTime(): number | null {\n return this.state === 'idle' ? null : this.startedAt;\n }\n\n /** Elapsed time in the active interval (delay counts down from negative), clamped like a finished animation. */\n get currentTime(): number | null {\n if (this.state === 'idle') return null;\n if (this.state === 'finished') return this.timing.duration;\n const elapsed = this.timers.now() - this.startedAt - this.timing.delay;\n return Math.min(Math.max(elapsed, 0), this.timing.duration);\n }\n\n set currentTime(value: number | null) {\n if (value === null) return;\n this.startedAt = this.timers.now() - this.timing.delay - toMs(value);\n }\n\n /** Cancels the animation: no completion, `playState` becomes `idle`, owned timer cleared. */\n cancel(): void {\n if (this.state === 'idle') return;\n const wasRunning = this.state === 'running';\n this.clearTimer();\n this.state = 'idle';\n this.onSettled(this);\n if (wasRunning) this.oncancel?.({ type: 'cancel', target: this, currentTime: null });\n }\n\n /** Completes now (still notifies `onfinish` synchronously, as an explicit caller request). */\n finish(): void {\n if (this.state !== 'running') return;\n this.clearTimer();\n this.complete();\n }\n\n play(): void {\n if (this.state === 'running') return;\n this.startedAt = this.timers.now();\n this.state = 'running';\n this.timer = this.timers.setTimeout(() => {\n this.timer = null;\n this.complete();\n }, this.timing.delay + this.timing.duration);\n }\n\n pause(): void {\n // Pausing is not modelled beyond keeping the timer; Svelte never pauses transitions.\n }\n\n /** Called by the owning realm on release; never fires completion. */\n dispose(): void {\n this.clearTimer();\n this.state = 'idle';\n }\n\n private clearTimer(): void {\n if (this.timer !== null) {\n this.timers.clearTimeout(this.timer);\n this.timer = null;\n }\n }\n\n private complete(): void {\n if (this.state !== 'running') return;\n this.state = 'finished';\n this.onSettled(this);\n this.resolveFinished(this);\n this.onfinish?.({ type: 'finish', target: this, currentTime: this.timing.duration });\n }\n}\n\n/** Resolves the `Element.prototype` of the realm owning `target` (its `ownerDocument.defaultView`), or null. */\nexport function animationRealmOf(target: unknown): object | null {\n const view = (target as { ownerDocument?: { defaultView?: unknown } } | null)?.ownerDocument?.defaultView;\n const ctor = (view as { Element?: { prototype?: object } } | undefined)?.Element\n ?? (globalThis as { Element?: { prototype?: object } }).Element;\n const proto = ctor?.prototype;\n return proto && typeof proto === 'object' ? proto : null;\n}\n\n/** True when `proto` (or its chain) already exposes a usable `animate`. */\nexport function hasNativeAnimate(proto: object): boolean {\n return typeof (proto as { animate?: unknown }).animate === 'function';\n}\n\n/** Number of live leases on a realm (0 when the adapter is not installed). */\nexport function animationLeaseCount(proto: object): number {\n return PATCHES.get(proto)?.leases ?? 0;\n}\n\nfunction install(proto: object, timers: WoodAnimationTimers): RealmPatch {\n const descriptor = Object.getOwnPropertyDescriptor(proto, 'animate');\n const active = new Set<WoodTestAnimation>();\n const animate = function (this: unknown, keyframes: unknown, options?: unknown): WoodTestAnimation {\n const animation = new WoodTestAnimation(this, keyframes, parseTiming(options), timers, (a) => active.delete(a));\n active.add(animation);\n return animation;\n };\n Object.defineProperty(proto, 'animate', { value: animate, configurable: true, writable: true, enumerable: false });\n const patch: RealmPatch = { leases: 0, descriptor, animate, active, timers };\n PATCHES.set(proto, patch);\n return patch;\n}\n\nfunction uninstall(proto: object, patch: RealmPatch): void {\n PATCHES.delete(proto);\n for (const animation of [...patch.active]) animation.dispose();\n patch.active.clear();\n if ((proto as { animate?: unknown }).animate !== patch.animate) return; // someone else replaced it; leave theirs\n if (patch.descriptor) Object.defineProperty(proto, 'animate', patch.descriptor);\n else delete (proto as { animate?: unknown }).animate;\n}\n\n/**\n * Acquire a lease on the realm owning `target`. Installs the adapter on first\n * lease when `animate` is absent; returns a no-op lease when a native\n * `animate` exists or no realm resolves.\n */\nexport function leaseDomAnimation(target: unknown, timers: WoodAnimationTimers = defaultTimers()): WoodAnimationLease {\n const proto = animationRealmOf(target);\n if (!proto) return { adapted: false, realm: null, release() {} };\n let patch = PATCHES.get(proto);\n if (!patch) {\n if (hasNativeAnimate(proto)) return { adapted: false, realm: proto, release() {} };\n patch = install(proto, timers);\n }\n patch.leases += 1;\n let released = false;\n return {\n adapted: true,\n realm: proto,\n release() {\n if (released) return;\n released = true;\n patch.leases -= 1;\n if (patch.leases <= 0) uninstall(proto, patch);\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -18,6 +18,7 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var wood_env_renderer_exports = {};
20
20
  __export(wood_env_renderer_exports, {
21
+ MountKeeper: () => MountKeeper,
21
22
  buildRendererTier: () => buildRendererTier
22
23
  });
23
24
  module.exports = __toCommonJS(wood_env_renderer_exports);
@@ -26,14 +27,16 @@ var import_in_process_router_transport = require("../transport/in_process_router
26
27
  var import_build_transport_bridge = require("../transport/build_transport_bridge.js");
27
28
  var import_create_router_bridge = require("./create_router_bridge.js");
28
29
  var import_navigation = require("../navigation/index.js");
29
- var import_get_container = require("../runtime/get_container.js");
30
30
  var import_work_registry = require("../frontend/work_registry.js");
31
31
  var import_tracked_transport = require("../frontend/tracked_transport.js");
32
32
  var import_frontend_surface = require("../frontend/frontend_surface.js");
33
33
  var import_test_wood = require("./test_wood.cjs");
34
34
  var import_wood_config_source = require("./wood_config_source.cjs");
35
- function controllerLoader(source, importer) {
35
+ var import_wood_dom_animation = require("./wood_dom_animation.cjs");
36
+ var import_registration_only_root = require("./registration_only_root.cjs");
37
+ function controllerLoader(source, importer, preloaded) {
36
38
  const cache = /* @__PURE__ */ new Map();
39
+ for (const [identity, controller] of preloaded) cache.set(identity, Promise.resolve(controller));
37
40
  return (identity) => {
38
41
  let pending = cache.get(identity);
39
42
  if (!pending) {
@@ -69,11 +72,16 @@ class MountKeeper {
69
72
  this.host = host;
70
73
  this.mounted = null;
71
74
  this.mountedFor = null;
75
+ this.animation = null;
72
76
  }
73
77
  get target() {
74
78
  if (!this.mounted) throw new import_test_wood.TestWoodError("no page is mounted; open(page) first");
75
79
  return this.mounted.target;
76
80
  }
81
+ /** The Web Animations lease held for the current mount (null when nothing is mounted). */
82
+ get animationLease() {
83
+ return this.animation;
84
+ }
77
85
  async flush() {
78
86
  await this.mounted?.flush?.();
79
87
  }
@@ -83,14 +91,29 @@ class MountKeeper {
83
91
  if (!current) return;
84
92
  const target = document.createElement("div");
85
93
  document.body.appendChild(target);
86
- this.mounted = this.host(current.identity, current, target);
94
+ const lease = (0, import_wood_dom_animation.leaseDomAnimation)(target);
95
+ try {
96
+ this.mounted = this.host(current.identity, current, target);
97
+ } catch (error) {
98
+ lease.release();
99
+ target.remove();
100
+ throw error;
101
+ }
102
+ this.animation = lease;
87
103
  this.mountedFor = current;
88
104
  }
89
105
  unmount() {
90
- this.mounted?.unmount();
91
- this.mounted?.target.remove();
106
+ const mounted = this.mounted;
107
+ const lease = this.animation;
92
108
  this.mounted = null;
93
109
  this.mountedFor = null;
110
+ this.animation = null;
111
+ try {
112
+ mounted?.unmount();
113
+ } finally {
114
+ mounted?.target.remove();
115
+ lease?.release();
116
+ }
94
117
  }
95
118
  }
96
119
  function domDriver(keeper) {
@@ -127,28 +150,54 @@ function domDriver(keeper) {
127
150
  };
128
151
  }
129
152
  async function buildRendererTier(options) {
130
- const { source, router, catalog, operations, importer, hooks, dom } = options;
153
+ const {
154
+ source,
155
+ router,
156
+ catalog,
157
+ operations,
158
+ importer,
159
+ hooks,
160
+ dom,
161
+ ioc,
162
+ registrations,
163
+ controllerClasses
164
+ } = options;
131
165
  const keeper = dom ? new MountKeeper(await loadPageHost(source, importer, dom)) : null;
132
- const rendererRoot = (0, import_get_container.createWoodRoot)();
133
- await hooks?.configureRendererRoot?.(rendererRoot);
134
166
  const registry = new import_work_registry.WorkRegistry();
135
- const navigation = new import_navigation.Navigation();
167
+ const defaultNavigation = new import_navigation.Navigation();
136
168
  const innerTransport = new import_in_process_router_transport.InProcessRouterTransport(router, hooks?.events ?? new import_create_router_bridge.RouterBridgeEvents());
137
169
  let currentOwner = () => "application";
138
170
  const transport = new import_tracked_transport.TrackedTransport(innerTransport, registry, () => currentOwner());
139
171
  const { bridge } = (0, import_build_transport_bridge.buildTransportBridge)(operations, transport);
140
- const registrable = rendererRoot;
141
- registrable.registerFunction(import_navigation.Navigation, () => navigation, { loadAs: import_ioc.LoadAs.Singleton });
142
- registrable.registerFunction(import_test_wood.WOOD_FRONTEND_BRIDGE, () => bridge, { loadAs: import_ioc.LoadAs.Singleton });
143
- const loadController = controllerLoader(source, importer);
144
- const frontend = (0, import_frontend_surface.createFrontendSurface)({
145
- catalog,
146
- rendererRoot,
147
- registry,
148
- navigation,
149
- resolveController: (identity) => loadController(identity),
150
- ...hooks?.configurePageScope ? { configurePageScope: (scope, view, context) => hooks.configurePageScope(scope, view, context) } : {}
151
- });
172
+ registrations.root.registerFunction(import_navigation.Navigation, () => defaultNavigation, { loadAs: import_ioc.LoadAs.Singleton });
173
+ registrations.root.registerFunction(import_test_wood.WOOD_FRONTEND_BRIDGE, () => bridge, { loadAs: import_ioc.LoadAs.Singleton });
174
+ const rendererEnv = await (0, import_registration_only_root.buildRecordedEnvironment)(ioc, registrations, "renderer");
175
+ const rendererRoot = rendererEnv.root;
176
+ let navigation;
177
+ const loadController = controllerLoader(source, importer, controllerClasses);
178
+ let frontend;
179
+ try {
180
+ navigation = await import_ioc.ExecutionContext.run(rendererRoot, () => rendererRoot.get(import_navigation.Navigation));
181
+ frontend = (0, import_frontend_surface.createFrontendSurface)({
182
+ catalog,
183
+ rendererRoot,
184
+ registry,
185
+ navigation,
186
+ resolveController: (identity) => loadController(identity),
187
+ ...hooks?.configurePageScope ? { configurePageScope: (scope, view, context) => hooks.configurePageScope(scope, view, context) } : {}
188
+ });
189
+ } catch (error) {
190
+ try {
191
+ await (0, import_registration_only_root.cleanupAll)("renderer build rollback failed", [
192
+ ["renderer mount", () => keeper?.unmount()],
193
+ ["renderer root", () => import_ioc.ExecutionContext.run(rendererRoot, () => rendererEnv.dispose())]
194
+ ]);
195
+ } catch (cleanupError) {
196
+ const failures = cleanupError instanceof import_registration_only_root.TestWoodCleanupError ? cleanupError.failures : [{ owner: "renderer rollback", error: cleanupError }];
197
+ throw new import_registration_only_root.TestWoodCleanupError("renderer build failed and rollback failed", failures, error);
198
+ }
199
+ throw error;
200
+ }
152
201
  currentOwner = () => {
153
202
  const current = frontend.current();
154
203
  return current ? `page ${current.identity}` : "application";
@@ -165,6 +214,14 @@ async function buildRendererTier(options) {
165
214
  await new Promise((resolve) => setTimeout(resolve, 0));
166
215
  await keeper.flush();
167
216
  };
217
+ let closing = null;
218
+ const dispose = () => closing ?? (closing = (0, import_registration_only_root.cleanupAll)("renderer cleanup failed", [
219
+ ["renderer mount", () => keeper?.unmount()],
220
+ ["renderer work drain (before frontend)", () => import_ioc.ExecutionContext.run(rendererRoot, () => frontend.settle())],
221
+ ["renderer frontend", () => import_ioc.ExecutionContext.run(rendererRoot, () => frontend.dispose())],
222
+ ["renderer work drain (after frontend)", () => import_ioc.ExecutionContext.run(rendererRoot, () => frontend.settle())],
223
+ ["renderer root", () => import_ioc.ExecutionContext.run(rendererRoot, () => rendererEnv.dispose())]
224
+ ]));
168
225
  return {
169
226
  frontend,
170
227
  events: innerTransport.events,
@@ -189,22 +246,14 @@ async function buildRendererTier(options) {
189
246
  current: () => frontend.current(),
190
247
  settle,
191
248
  pendingWork: () => registry.pending(),
249
+ verify: () => rendererEnv.verify(),
192
250
  dom: keeper ? domDriver(keeper) : null,
193
- async dispose() {
194
- keeper?.unmount();
195
- await import_ioc.ExecutionContext.run(rendererRoot, async () => {
196
- await frontend.settle().catch(() => {
197
- });
198
- await frontend.dispose();
199
- await frontend.settle().catch(() => {
200
- });
201
- await rendererRoot.dispose();
202
- });
203
- }
251
+ dispose
204
252
  };
205
253
  }
206
254
  // Annotate the CommonJS export names for ESM import in node:
207
255
  0 && (module.exports = {
256
+ MountKeeper,
208
257
  buildRendererTier
209
258
  });
210
259
  //# sourceMappingURL=wood_env_renderer.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/testing/wood_env_renderer.ts"],"sourcesContent":["/**\n * The renderer tier of a `testWood(configPath)` environment: a fresh renderer\n * root (mirroring the production process split), an environment-local\n * Navigation, the generated bridge client over an in-process transport into\n * the main router, and the frontend application surface over the selected\n * page catalog. With a DOM mode, the page's real component is mounted by the\n * app's host module and driven through a selector-based surface.\n */\nimport type { IContainer } from '@noego/ioc';\nimport { ExecutionContext, LoadAs } from '@noego/ioc';\nimport type { WoodRouter } from '../router/wood_router.cjs';\nimport { InProcessRouterTransport } from '../transport/in_process_router_transport.js';\nimport { buildTransportBridge } from '../transport/build_transport_bridge.js';\nimport { RouterBridgeEvents } from './create_router_bridge.js';\nimport type { OperationDef, ViewDef } from '../types/index.cjs';\nimport { Navigation } from '../navigation/index.js';\nimport { createWoodRoot } from '../runtime/get_container.js';\nimport type { PageCatalog } from '../frontend/page_catalog.js';\nimport { WorkRegistry } from '../frontend/work_registry.js';\nimport { TrackedTransport } from '../frontend/tracked_transport.js';\nimport {\n createFrontendSurface,\n type FrontendApplicationSurface,\n type PageScopeContext,\n} from '../frontend/frontend_surface.js';\nimport type { FrontendPage } from '../frontend/frontend_page.js';\nimport { TestWoodError, WOOD_FRONTEND_BRIDGE } from './test_wood.cjs';\nimport { importDefaultExport, type ModuleImporter, type WoodConfigSource } from './wood_config_source.cjs';\nimport type { WoodDomMode } from './wood_test_selection.cjs';\n\n/** What the app's `main.testing.compose` module may hand back for the renderer side. */\nexport interface WoodRendererHooks {\n /** Backend push emitter feeding renderer subscriptions (default: transport-owned). */\n events?: RouterBridgeEvents;\n /** Register app-owned renderer composition on the fresh renderer root. */\n configureRendererRoot?(root: IContainer): void | Promise<void>;\n /** Register per-scope renderer builtins on a new page/layout scope. */\n configurePageScope?(scope: IContainer, view: ViewDef, context: PageScopeContext): void | Promise<void>;\n}\n\n/** A page's real component mounted under a DOM driver (`renderer.testing.host`). */\nexport interface WoodMountedPage {\n readonly target: Element;\n flush?(): void | Promise<void>;\n unmount(): void;\n}\n\nexport type WoodPageHost = (identity: string, page: FrontendPage, target: Element) => WoodMountedPage;\n\nexport interface RendererTierOptions {\n source: WoodConfigSource;\n router: WoodRouter;\n catalog: PageCatalog;\n operations: Map<string, OperationDef>;\n importer: ModuleImporter;\n hooks: WoodRendererHooks | null;\n dom: WoodDomMode | null;\n}\n\nexport interface WoodDomDriver {\n readonly target: Element;\n click(selector: string): Promise<void>;\n type(selector: string, text: string): Promise<void>;\n submit(selector: string): Promise<void>;\n text(selector: string): Promise<string>;\n exists(selector: string): Promise<boolean>;\n}\n\nexport interface RendererTier {\n readonly frontend: FrontendApplicationSurface;\n readonly events: RouterBridgeEvents;\n readonly rendererRoot: IContainer;\n open(page: string, params?: Record<string, string>, query?: Record<string, string>): Promise<FrontendPage>;\n navigate(page: string, params?: Record<string, string>, query?: Record<string, string>): Promise<FrontendPage>;\n current(): FrontendPage | null;\n settle(): Promise<void>;\n pendingWork(): ReturnType<WorkRegistry['pending']>;\n dom: WoodDomDriver | null;\n dispose(): Promise<void>;\n}\n\ntype ControllerClass = new (...args: never[]) => unknown;\n\nfunction controllerLoader(source: WoodConfigSource, importer: ModuleImporter) {\n const cache = new Map<string, Promise<ControllerClass>>();\n return (identity: string): Promise<ControllerClass> => {\n let pending = cache.get(identity);\n if (!pending) {\n pending = importDefaultExport<ControllerClass>(\n importer,\n source.rendererControllerFile(identity),\n `renderer controller \"${identity}\"`,\n );\n cache.set(identity, pending);\n }\n return pending;\n };\n}\n\nasync function loadPageHost(\n source: WoodConfigSource,\n importer: ModuleImporter,\n dom: WoodDomMode,\n): Promise<WoodPageHost> {\n if (dom === 'chromium') {\n throw new TestWoodError(\"select({ dom: 'chromium' }) is not available yet; use 'happy-dom'\");\n }\n if (typeof document === 'undefined') {\n throw new TestWoodError(\n \"select({ dom: 'happy-dom' }) requires a DOM environment (// @vitest-environment happy-dom)\",\n );\n }\n const hostPath = source.hookModule(source.config.renderer.testing?.host);\n if (!hostPath) {\n throw new TestWoodError(\n 'select({ dom }) requires renderer.testing.host in wood.config.yml (a module whose default export mounts a page)',\n );\n }\n return importDefaultExport<WoodPageHost>(importer, hostPath, 'renderer.testing.host');\n}\n\n/** Keeps the mounted component in step with the surface's current page. */\nclass MountKeeper {\n private mounted: WoodMountedPage | null = null;\n private mountedFor: FrontendPage | null = null;\n\n constructor(private readonly host: WoodPageHost) {}\n\n get target(): Element {\n if (!this.mounted) throw new TestWoodError('no page is mounted; open(page) first');\n return this.mounted.target;\n }\n\n async flush(): Promise<void> {\n await this.mounted?.flush?.();\n }\n\n sync(current: FrontendPage | null): void {\n if (current === this.mountedFor) return;\n this.unmount();\n if (!current) return;\n const target = document.createElement('div');\n document.body.appendChild(target);\n this.mounted = this.host(current.identity, current, target);\n this.mountedFor = current;\n }\n\n unmount(): void {\n this.mounted?.unmount();\n this.mounted?.target.remove();\n this.mounted = null;\n this.mountedFor = null;\n }\n}\n\nfunction domDriver(keeper: MountKeeper): WoodDomDriver {\n const find = (selector: string): Element => {\n const found = keeper.target.querySelector(selector);\n if (!found) throw new TestWoodError(`no element matches \"${selector}\"`);\n return found;\n };\n return {\n get target() {\n return keeper.target;\n },\n async click(selector) {\n (find(selector) as HTMLElement).click();\n await keeper.flush();\n },\n async type(selector, text) {\n const input = find(selector) as HTMLInputElement;\n input.focus();\n input.value = text;\n input.dispatchEvent(new Event('input', { bubbles: true }));\n await keeper.flush();\n },\n async submit(selector) {\n find(selector).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));\n await keeper.flush();\n },\n async text(selector) {\n return keeper.target.querySelector(selector)?.textContent ?? '';\n },\n async exists(selector) {\n return keeper.target.querySelector(selector) !== null;\n },\n };\n}\n\nexport async function buildRendererTier(options: RendererTierOptions): Promise<RendererTier> {\n const { source, router, catalog, operations, importer, hooks, dom } = options;\n const keeper = dom ? new MountKeeper(await loadPageHost(source, importer, dom)) : null;\n\n const rendererRoot = createWoodRoot();\n await hooks?.configureRendererRoot?.(rendererRoot);\n\n const registry = new WorkRegistry();\n const navigation = new Navigation();\n const innerTransport = new InProcessRouterTransport(router, hooks?.events ?? new RouterBridgeEvents());\n let currentOwner: () => string = () => 'application';\n const transport = new TrackedTransport(innerTransport, registry, () => currentOwner());\n const { bridge } = buildTransportBridge(operations, transport);\n\n const registrable = rendererRoot as unknown as {\n registerFunction(token: unknown, factory: () => unknown, options: { loadAs: LoadAs }): void;\n };\n registrable.registerFunction(Navigation, () => navigation, { loadAs: LoadAs.Singleton });\n registrable.registerFunction(WOOD_FRONTEND_BRIDGE, () => bridge, { loadAs: LoadAs.Singleton });\n\n const loadController = controllerLoader(source, importer);\n const frontend = createFrontendSurface({\n catalog,\n rendererRoot: rendererRoot as never,\n registry,\n navigation,\n resolveController: (identity) => loadController(identity),\n ...(hooks?.configurePageScope\n ? { configurePageScope: (scope, view, context) => hooks.configurePageScope!(scope as IContainer, view, context) }\n : {}),\n });\n currentOwner = () => {\n const current = frontend.current();\n return current ? `page ${current.identity}` : 'application';\n };\n\n const syncMount = async (): Promise<void> => {\n if (!keeper) return;\n keeper.sync(frontend.current());\n await keeper.flush();\n };\n const settle = async (): Promise<void> => {\n await frontend.settle();\n if (!keeper) return;\n await syncMount();\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\n await keeper.flush();\n };\n\n return {\n frontend,\n events: innerTransport.events,\n rendererRoot,\n async open(page, params, query) {\n const opened = await frontend.open({\n page,\n ...(params ? { params } : {}),\n ...(query ? { query } : {}),\n });\n await syncMount();\n return opened;\n },\n async navigate(page, params, query) {\n if (!frontend.current()) throw new TestWoodError('navigate() needs an open page; call open(page) first');\n await frontend.act(() => navigation.go(page, params, query));\n await syncMount();\n const current = frontend.current();\n if (!current) throw new TestWoodError(`navigate(\"${page}\") left no page open`);\n return current;\n },\n current: () => frontend.current(),\n settle,\n pendingWork: () => registry.pending(),\n dom: keeper ? domDriver(keeper) : null,\n async dispose() {\n keeper?.unmount();\n await ExecutionContext.run(rendererRoot as never, async () => {\n // Drain owned work while the transport still reaches a live main\n // root, end the pages, then drain the teardown's own deliveries.\n await frontend.settle().catch(() => {});\n await frontend.dispose();\n await frontend.settle().catch(() => {});\n await rendererRoot.dispose();\n });\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/testing/wood_env_renderer.ts"],"sourcesContent":["/**\n * The renderer tier of a `testWood(configPath)` environment: a fresh renderer\n * root (mirroring the production process split), an environment-local\n * Navigation, the generated bridge client over an in-process transport into\n * the main router, and the frontend application surface over the selected\n * page catalog. With a DOM mode, the page's real component is mounted by the\n * app's host module and driven through a selector-based surface.\n */\nimport type { IContainer } from '@noego/ioc';\nimport { ExecutionContext, LoadAs } from '@noego/ioc';\nimport type { TestIocBuilder } from '@noego/testing';\nimport type { WoodRouter } from '../router/wood_router.cjs';\nimport { InProcessRouterTransport } from '../transport/in_process_router_transport.js';\nimport { buildTransportBridge } from '../transport/build_transport_bridge.js';\nimport { RouterBridgeEvents } from './create_router_bridge.js';\nimport type { OperationDef, ViewDef } from '../types/index.cjs';\nimport { Navigation } from '../navigation/index.js';\nimport type { PageCatalog } from '../frontend/page_catalog.js';\nimport { WorkRegistry } from '../frontend/work_registry.js';\nimport { TrackedTransport } from '../frontend/tracked_transport.js';\nimport {\n createFrontendSurface,\n type FrontendApplicationSurface,\n type PageScopeContext,\n} from '../frontend/frontend_surface.js';\nimport type { FrontendPage } from '../frontend/frontend_page.js';\nimport { TestWoodError, WOOD_FRONTEND_BRIDGE } from './test_wood.cjs';\nimport { importDefaultExport, type ModuleImporter, type WoodConfigSource } from './wood_config_source.cjs';\nimport type { WoodDomMode } from './wood_test_selection.cjs';\nimport { leaseDomAnimation, type WoodAnimationLease } from './wood_dom_animation.cjs';\nimport {\n buildRecordedEnvironment,\n cleanupAll,\n RegistrationOnlyRoot,\n TestWoodCleanupError,\n} from './registration_only_root.cjs';\n\n/** What the app's `main.testing.compose` module may hand back for the renderer side. */\nexport interface WoodRendererHooks {\n /** Backend push emitter feeding renderer subscriptions (default: transport-owned). */\n events?: RouterBridgeEvents;\n /** Register app-owned renderer composition on the fresh renderer root. */\n configureRendererRoot?(root: IContainer): void | Promise<void>;\n /** Register per-scope renderer builtins on a new page/layout scope. */\n configurePageScope?(scope: IContainer, view: ViewDef, context: PageScopeContext): void | Promise<void>;\n}\n\n/** A page's real component mounted under a DOM driver (`renderer.testing.host`). */\nexport interface WoodMountedPage {\n readonly target: Element;\n flush?(): void | Promise<void>;\n unmount(): void;\n}\n\nexport type WoodPageHost = (identity: string, page: FrontendPage, target: Element) => WoodMountedPage;\n\nexport interface RendererTierOptions {\n source: WoodConfigSource;\n router: WoodRouter;\n catalog: PageCatalog;\n operations: Map<string, OperationDef>;\n importer: ModuleImporter;\n hooks: WoodRendererHooks | null;\n dom: WoodDomMode | null;\n ioc: TestIocBuilder;\n registrations: RegistrationOnlyRoot;\n controllerClasses: ReadonlyMap<string, ControllerClass>;\n}\n\nexport interface WoodDomDriver {\n readonly target: Element;\n click(selector: string): Promise<void>;\n type(selector: string, text: string): Promise<void>;\n submit(selector: string): Promise<void>;\n text(selector: string): Promise<string>;\n exists(selector: string): Promise<boolean>;\n}\n\nexport interface RendererTier {\n readonly frontend: FrontendApplicationSurface;\n readonly events: RouterBridgeEvents;\n readonly rendererRoot: IContainer;\n open(page: string, params?: Record<string, string>, query?: Record<string, string>): Promise<FrontendPage>;\n navigate(page: string, params?: Record<string, string>, query?: Record<string, string>): Promise<FrontendPage>;\n current(): FrontendPage | null;\n settle(): Promise<void>;\n pendingWork(): ReturnType<WorkRegistry['pending']>;\n verify(): Promise<void>;\n dom: WoodDomDriver | null;\n dispose(): Promise<void>;\n}\ntype ControllerClass = new (...args: never[]) => unknown;\n\nfunction controllerLoader(\n source: WoodConfigSource,\n importer: ModuleImporter,\n preloaded: ReadonlyMap<string, ControllerClass>,\n) {\n const cache = new Map<string, Promise<ControllerClass>>();\n for (const [identity, controller] of preloaded) cache.set(identity, Promise.resolve(controller));\n return (identity: string): Promise<ControllerClass> => {\n let pending = cache.get(identity);\n if (!pending) {\n pending = importDefaultExport<ControllerClass>(\n importer,\n source.rendererControllerFile(identity),\n `renderer controller \"${identity}\"`,\n );\n cache.set(identity, pending);\n }\n return pending;\n };\n}\n\nasync function loadPageHost(\n source: WoodConfigSource,\n importer: ModuleImporter,\n dom: WoodDomMode,\n): Promise<WoodPageHost> {\n if (dom === 'chromium') {\n throw new TestWoodError(\"select({ dom: 'chromium' }) is not available yet; use 'happy-dom'\");\n }\n if (typeof document === 'undefined') {\n throw new TestWoodError(\n \"select({ dom: 'happy-dom' }) requires a DOM environment (// @vitest-environment happy-dom)\",\n );\n }\n const hostPath = source.hookModule(source.config.renderer.testing?.host);\n if (!hostPath) {\n throw new TestWoodError(\n 'select({ dom }) requires renderer.testing.host in wood.config.yml (a module whose default export mounts a page)',\n );\n }\n return importDefaultExport<WoodPageHost>(importer, hostPath, 'renderer.testing.host');\n}\n\n/**\n * Keeps the mounted component in step with the surface's current page. While\n * a page is mounted it holds a Web Animations lease on the target's realm so\n * Svelte transitions work under happy-dom (see `wood_dom_animation.ts`); the\n * lease is released on unmount, on a failed host mount, and on dispose.\n */\nexport class MountKeeper {\n private mounted: WoodMountedPage | null = null;\n private mountedFor: FrontendPage | null = null;\n private animation: WoodAnimationLease | null = null;\n\n constructor(private readonly host: WoodPageHost) {}\n\n get target(): Element {\n if (!this.mounted) throw new TestWoodError('no page is mounted; open(page) first');\n return this.mounted.target;\n }\n\n /** The Web Animations lease held for the current mount (null when nothing is mounted). */\n get animationLease(): WoodAnimationLease | null {\n return this.animation;\n }\n\n async flush(): Promise<void> {\n await this.mounted?.flush?.();\n }\n\n sync(current: FrontendPage | null): void {\n if (current === this.mountedFor) return;\n this.unmount();\n if (!current) return;\n const target = document.createElement('div');\n document.body.appendChild(target);\n const lease = leaseDomAnimation(target);\n try {\n this.mounted = this.host(current.identity, current, target);\n } catch (error) {\n lease.release();\n target.remove();\n throw error;\n }\n this.animation = lease;\n this.mountedFor = current;\n }\n\n unmount(): void {\n const mounted = this.mounted;\n const lease = this.animation;\n this.mounted = null;\n this.mountedFor = null;\n this.animation = null;\n try {\n mounted?.unmount();\n } finally {\n mounted?.target.remove();\n lease?.release();\n }\n }\n}\n\nfunction domDriver(keeper: MountKeeper): WoodDomDriver {\n const find = (selector: string): Element => {\n const found = keeper.target.querySelector(selector);\n if (!found) throw new TestWoodError(`no element matches \"${selector}\"`);\n return found;\n };\n return {\n get target() {\n return keeper.target;\n },\n async click(selector) {\n (find(selector) as HTMLElement).click();\n await keeper.flush();\n },\n async type(selector, text) {\n const input = find(selector) as HTMLInputElement;\n input.focus();\n input.value = text;\n input.dispatchEvent(new Event('input', { bubbles: true }));\n await keeper.flush();\n },\n async submit(selector) {\n find(selector).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));\n await keeper.flush();\n },\n async text(selector) {\n return keeper.target.querySelector(selector)?.textContent ?? '';\n },\n async exists(selector) {\n return keeper.target.querySelector(selector) !== null;\n },\n };\n}\n\nexport async function buildRendererTier(options: RendererTierOptions): Promise<RendererTier> {\n const {\n source,\n router,\n catalog,\n operations,\n importer,\n hooks,\n dom,\n ioc,\n registrations,\n controllerClasses,\n } = options;\n const keeper = dom ? new MountKeeper(await loadPageHost(source, importer, dom)) : null;\n\n const registry = new WorkRegistry();\n const defaultNavigation = new Navigation();\n const innerTransport = new InProcessRouterTransport(router, hooks?.events ?? new RouterBridgeEvents());\n let currentOwner: () => string = () => 'application';\n const transport = new TrackedTransport(innerTransport, registry, () => currentOwner());\n const { bridge } = buildTransportBridge(operations, transport);\n\n registrations.root.registerFunction(Navigation, () => defaultNavigation, { loadAs: LoadAs.Singleton });\n registrations.root.registerFunction(WOOD_FRONTEND_BRIDGE, () => bridge, { loadAs: LoadAs.Singleton });\n\n const rendererEnv = await buildRecordedEnvironment(ioc, registrations, 'renderer');\n const rendererRoot = rendererEnv.root;\n let navigation: Navigation;\n const loadController = controllerLoader(source, importer, controllerClasses);\n let frontend: FrontendApplicationSurface;\n try {\n navigation = await ExecutionContext.run(rendererRoot as never, () => rendererRoot.get(Navigation)) as Navigation;\n frontend = createFrontendSurface({\n catalog,\n rendererRoot: rendererRoot as never,\n registry,\n navigation,\n resolveController: (identity) => loadController(identity),\n ...(hooks?.configurePageScope\n ? { configurePageScope: (scope, view, context) => hooks.configurePageScope!(scope as IContainer, view, context) }\n : {}),\n });\n } catch (error) {\n try {\n await cleanupAll('renderer build rollback failed', [\n ['renderer mount', () => keeper?.unmount()],\n ['renderer root', () => ExecutionContext.run(rendererRoot as never, () => rendererEnv.dispose()) as Promise<void>],\n ]);\n } catch (cleanupError) {\n const failures = cleanupError instanceof TestWoodCleanupError\n ? cleanupError.failures\n : [{ owner: 'renderer rollback', error: cleanupError }];\n throw new TestWoodCleanupError('renderer build failed and rollback failed', failures, error);\n }\n throw error;\n }\n\n currentOwner = () => {\n const current = frontend.current();\n return current ? `page ${current.identity}` : 'application';\n };\n\n const syncMount = async (): Promise<void> => {\n if (!keeper) return;\n keeper.sync(frontend.current());\n await keeper.flush();\n };\n const settle = async (): Promise<void> => {\n await frontend.settle();\n if (!keeper) return;\n await syncMount();\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\n await keeper.flush();\n };\n\n let closing: Promise<void> | null = null;\n const dispose = (): Promise<void> => closing ??= cleanupAll('renderer cleanup failed', [\n ['renderer mount', () => keeper?.unmount()],\n ['renderer work drain (before frontend)', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.settle()) as Promise<void>],\n ['renderer frontend', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.dispose()) as Promise<void>],\n ['renderer work drain (after frontend)', () =>\n ExecutionContext.run(rendererRoot as never, () => frontend.settle()) as Promise<void>],\n ['renderer root', () =>\n ExecutionContext.run(rendererRoot as never, () => rendererEnv.dispose()) as Promise<void>],\n ]);\n\n return {\n frontend,\n events: innerTransport.events,\n rendererRoot,\n async open(page, params, query) {\n const opened = await frontend.open({\n page,\n ...(params ? { params } : {}),\n ...(query ? { query } : {}),\n });\n await syncMount();\n return opened;\n },\n async navigate(page, params, query) {\n if (!frontend.current()) throw new TestWoodError('navigate() needs an open page; call open(page) first');\n await frontend.act(() => navigation.go(page, params, query));\n await syncMount();\n const current = frontend.current();\n if (!current) throw new TestWoodError(`navigate(\"${page}\") left no page open`);\n return current;\n },\n current: () => frontend.current(),\n settle,\n pendingWork: () => registry.pending(),\n verify: () => rendererEnv.verify(),\n dom: keeper ? domDriver(keeper) : null,\n dispose,\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,5 @@
1
1
  import { IContainer } from '@noego/ioc';
2
+ import { TestIocBuilder } from '@noego/testing';
2
3
  import { WoodRouter } from '../router/wood_router.cjs';
3
4
  import { RouterBridgeEvents } from './create_router_bridge.cjs';
4
5
  import { OperationDef } from '../types/operations.cjs';
@@ -9,11 +10,17 @@ import { PageScopeContext, FrontendApplicationSurface } from '../frontend/fronte
9
10
  import { FrontendPage } from '../frontend/frontend_page.cjs';
10
11
  import { WoodConfigSource, ModuleImporter } from './wood_config_source.cjs';
11
12
  import { WoodDomMode } from './wood_test_selection.cjs';
13
+ import { WoodAnimationLease } from './wood_dom_animation.cjs';
14
+ import { RegistrationOnlyRoot } from './registration_only_root.cjs';
12
15
  import '../types/context.cjs';
16
+ import '../validation/schema_validator.cjs';
17
+ import '../middleware/middleware_resolver.cjs';
18
+ import '../controller/controller_resolver.cjs';
13
19
  import '../navigation/index.cjs';
14
20
  import '../navigation/automation_introspection.cjs';
15
21
  import '../frontend/frontend_lifecycle.cjs';
16
22
  import '../types/config.cjs';
23
+ import './test_wood.cjs';
17
24
 
18
25
  /**
19
26
  * The renderer tier of a `testWood(configPath)` environment: a fresh renderer
@@ -48,6 +55,9 @@ interface RendererTierOptions {
48
55
  importer: ModuleImporter;
49
56
  hooks: WoodRendererHooks | null;
50
57
  dom: WoodDomMode | null;
58
+ ioc: TestIocBuilder;
59
+ registrations: RegistrationOnlyRoot;
60
+ controllerClasses: ReadonlyMap<string, ControllerClass>;
51
61
  }
52
62
  interface WoodDomDriver {
53
63
  readonly target: Element;
@@ -66,9 +76,30 @@ interface RendererTier {
66
76
  current(): FrontendPage | null;
67
77
  settle(): Promise<void>;
68
78
  pendingWork(): ReturnType<WorkRegistry['pending']>;
79
+ verify(): Promise<void>;
69
80
  dom: WoodDomDriver | null;
70
81
  dispose(): Promise<void>;
71
82
  }
83
+ type ControllerClass = new (...args: never[]) => unknown;
84
+ /**
85
+ * Keeps the mounted component in step with the surface's current page. While
86
+ * a page is mounted it holds a Web Animations lease on the target's realm so
87
+ * Svelte transitions work under happy-dom (see `wood_dom_animation.ts`); the
88
+ * lease is released on unmount, on a failed host mount, and on dispose.
89
+ */
90
+ declare class MountKeeper {
91
+ private readonly host;
92
+ private mounted;
93
+ private mountedFor;
94
+ private animation;
95
+ constructor(host: WoodPageHost);
96
+ get target(): Element;
97
+ /** The Web Animations lease held for the current mount (null when nothing is mounted). */
98
+ get animationLease(): WoodAnimationLease | null;
99
+ flush(): Promise<void>;
100
+ sync(current: FrontendPage | null): void;
101
+ unmount(): void;
102
+ }
72
103
  declare function buildRendererTier(options: RendererTierOptions): Promise<RendererTier>;
73
104
 
74
- export { type RendererTier, type RendererTierOptions, type WoodDomDriver, type WoodMountedPage, type WoodPageHost, type WoodRendererHooks, buildRendererTier };
105
+ export { MountKeeper, type RendererTier, type RendererTierOptions, type WoodDomDriver, type WoodMountedPage, type WoodPageHost, type WoodRendererHooks, buildRendererTier };
@@ -1,4 +1,5 @@
1
1
  import { IContainer } from '@noego/ioc';
2
+ import { TestIocBuilder } from '@noego/testing';
2
3
  import { WoodRouter } from '../router/wood_router.js';
3
4
  import { RouterBridgeEvents } from './create_router_bridge.js';
4
5
  import { OperationDef } from '../types/operations.js';
@@ -9,11 +10,17 @@ import { PageScopeContext, FrontendApplicationSurface } from '../frontend/fronte
9
10
  import { FrontendPage } from '../frontend/frontend_page.js';
10
11
  import { WoodConfigSource, ModuleImporter } from './wood_config_source.js';
11
12
  import { WoodDomMode } from './wood_test_selection.js';
13
+ import { WoodAnimationLease } from './wood_dom_animation.js';
14
+ import { RegistrationOnlyRoot } from './registration_only_root.js';
12
15
  import '../types/context.js';
16
+ import '../validation/schema_validator.js';
17
+ import '../middleware/middleware_resolver.js';
18
+ import '../controller/controller_resolver.js';
13
19
  import '../navigation/index.js';
14
20
  import '../navigation/automation_introspection.js';
15
21
  import '../frontend/frontend_lifecycle.js';
16
22
  import '../types/config.js';
23
+ import './test_wood.js';
17
24
 
18
25
  /**
19
26
  * The renderer tier of a `testWood(configPath)` environment: a fresh renderer
@@ -48,6 +55,9 @@ interface RendererTierOptions {
48
55
  importer: ModuleImporter;
49
56
  hooks: WoodRendererHooks | null;
50
57
  dom: WoodDomMode | null;
58
+ ioc: TestIocBuilder;
59
+ registrations: RegistrationOnlyRoot;
60
+ controllerClasses: ReadonlyMap<string, ControllerClass>;
51
61
  }
52
62
  interface WoodDomDriver {
53
63
  readonly target: Element;
@@ -66,9 +76,30 @@ interface RendererTier {
66
76
  current(): FrontendPage | null;
67
77
  settle(): Promise<void>;
68
78
  pendingWork(): ReturnType<WorkRegistry['pending']>;
79
+ verify(): Promise<void>;
69
80
  dom: WoodDomDriver | null;
70
81
  dispose(): Promise<void>;
71
82
  }
83
+ type ControllerClass = new (...args: never[]) => unknown;
84
+ /**
85
+ * Keeps the mounted component in step with the surface's current page. While
86
+ * a page is mounted it holds a Web Animations lease on the target's realm so
87
+ * Svelte transitions work under happy-dom (see `wood_dom_animation.ts`); the
88
+ * lease is released on unmount, on a failed host mount, and on dispose.
89
+ */
90
+ declare class MountKeeper {
91
+ private readonly host;
92
+ private mounted;
93
+ private mountedFor;
94
+ private animation;
95
+ constructor(host: WoodPageHost);
96
+ get target(): Element;
97
+ /** The Web Animations lease held for the current mount (null when nothing is mounted). */
98
+ get animationLease(): WoodAnimationLease | null;
99
+ flush(): Promise<void>;
100
+ sync(current: FrontendPage | null): void;
101
+ unmount(): void;
102
+ }
72
103
  declare function buildRendererTier(options: RendererTierOptions): Promise<RendererTier>;
73
104
 
74
- export { type RendererTier, type RendererTierOptions, type WoodDomDriver, type WoodMountedPage, type WoodPageHost, type WoodRendererHooks, buildRendererTier };
105
+ export { MountKeeper, type RendererTier, type RendererTierOptions, type WoodDomDriver, type WoodMountedPage, type WoodPageHost, type WoodRendererHooks, buildRendererTier };