@noego/wood 0.2.0 → 0.3.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 (42) hide show
  1. package/dist/client/index.cjs +1 -1
  2. package/dist/client/index.cjs.map +1 -1
  3. package/dist/client/index.js +1 -1
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/codegen/renderer_app_generator.cjs +17 -1
  6. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  7. package/dist/codegen/renderer_app_generator.js +17 -1
  8. package/dist/codegen/renderer_app_generator.js.map +1 -1
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/navigation/index.cjs +17 -12
  14. package/dist/navigation/index.cjs.map +1 -1
  15. package/dist/navigation/index.d.cts +8 -4
  16. package/dist/navigation/index.d.ts +8 -4
  17. package/dist/navigation/index.js +17 -12
  18. package/dist/navigation/index.js.map +1 -1
  19. package/dist/parser/views_parser.cjs +24 -3
  20. package/dist/parser/views_parser.cjs.map +1 -1
  21. package/dist/parser/views_parser.js +24 -3
  22. package/dist/parser/views_parser.js.map +1 -1
  23. package/dist/testing/create_controller_harness.cjs +2 -2
  24. package/dist/testing/create_controller_harness.cjs.map +1 -1
  25. package/dist/testing/create_test_router.cjs +1 -1
  26. package/dist/testing/create_test_router.cjs.map +1 -1
  27. package/dist/testing/index.cjs +6 -6
  28. package/dist/testing/index.cjs.map +1 -1
  29. package/dist/testing/index.js +3 -3
  30. package/dist/testing/index.js.map +1 -1
  31. package/dist/testing/mock_electron.cjs +59 -39
  32. package/dist/testing/mock_electron.cjs.map +1 -1
  33. package/dist/testing/mock_electron.d.cts +2 -2
  34. package/dist/testing/mock_electron.d.ts +2 -2
  35. package/dist/testing/mock_electron.js +59 -39
  36. package/dist/testing/mock_electron.js.map +1 -1
  37. package/dist/types/views.cjs.map +1 -1
  38. package/dist/types/views.d.cts +3 -0
  39. package/dist/types/views.d.ts +3 -0
  40. package/package.json +1 -1
  41. package/src/components/NavigationShell.svelte +154 -27
  42. package/src/components/WoodRecursiveRender.svelte +49 -36
@@ -9,14 +9,20 @@
9
9
  key: string;
10
10
  windowName: string;
11
11
  pageName: string;
12
+ layoutPaths: string[];
13
+ layoutControllers: Array<string | undefined>;
14
+ layoutPreserve?: boolean[];
12
15
  view: any;
13
16
  layouts: any[];
14
17
  [k: string]: unknown;
15
18
  };
16
19
 
17
20
  type CreateViewControllerFn = (entry: ViewManifestEntry) => Promise<unknown>;
18
- type CreateLayoutControllersFn = (entry: ViewManifestEntry) => Promise<Array<unknown>>;
21
+ type CreateLayoutControllersFn = (entry: ViewManifestEntry, startIndex?: number) => Promise<Array<unknown>>;
19
22
  type GetViewManifestEntryFn = (viewKey: string) => ViewManifestEntry | null;
23
+ type NavigationOptions = {
24
+ preserveLayouts?: boolean;
25
+ };
20
26
  type WoodBridgeLike = {
21
27
  __window?: {
22
28
  open?: (windowId: string, options?: Record<string, unknown>) => Promise<unknown>;
@@ -45,12 +51,29 @@
45
51
  getViewManifestEntryFn: GetViewManifestEntryFn;
46
52
  } = $props();
47
53
 
48
- let activeEntry = $state(initialEntry);
49
- let activeController = $state(initialController);
50
- let activeLayoutControllers: Array<unknown> = $state(initialLayoutControllers);
54
+ function getInitialEntry(): ViewManifestEntry {
55
+ return initialEntry;
56
+ }
57
+
58
+ function getInitialController(): unknown {
59
+ return initialController;
60
+ }
61
+
62
+ function getInitialLayoutControllers(): Array<unknown> {
63
+ return initialLayoutControllers;
64
+ }
65
+
66
+ let activeEntry = $state(getInitialEntry());
67
+ let activeController = $state(getInitialController());
68
+ let activeLayoutControllers: Array<unknown> = $state(getInitialLayoutControllers());
51
69
  let activeParams: Record<string, unknown> = $state({});
52
70
  let activeQuery: Record<string, unknown> = $state({});
53
- let activeMountKey = $state(createNavigationMountKey(initialEntry.key, {}));
71
+ let activePageKey = $state(createNavigationMountKey(getInitialEntry().key, {}));
72
+ let activeLayoutKeys: string[] = $state(
73
+ getInitialEntry().layoutPaths.map((_, index) =>
74
+ createLayoutMountKey(getInitialEntry(), index, activePageKey),
75
+ ),
76
+ );
54
77
 
55
78
  let activePage = $derived({
56
79
  key: activeEntry.key,
@@ -88,7 +111,7 @@
88
111
  async function navigateWithinCurrentWindow(
89
112
  viewKey: string,
90
113
  params?: Record<string, unknown>,
91
- options?: { replace?: boolean },
114
+ options?: { replace?: boolean; preserveLayouts?: boolean },
92
115
  ): Promise<void> {
93
116
  const normalizedViewKey = viewKey.replace(',', '.');
94
117
  const entry = getViewManifestEntryFn(normalizedViewKey);
@@ -102,13 +125,83 @@
102
125
  }
103
126
 
104
127
  const p = (params ?? {}) as Record<string, string>;
128
+ const navigationOptions: NavigationOptions = {
129
+ ...(options?.preserveLayouts === undefined
130
+ ? {}
131
+ : { preserveLayouts: options.preserveLayouts }),
132
+ };
105
133
  if (options?.replace) {
106
- navigation.replace(normalizedViewKey, p);
134
+ navigation.replace(normalizedViewKey, p, {}, navigationOptions);
107
135
  } else {
108
- navigation.go(normalizedViewKey, p);
136
+ navigation.go(normalizedViewKey, p, {}, navigationOptions);
109
137
  }
110
138
  }
111
139
 
140
+ function createLayoutMountKey(
141
+ entry: ViewManifestEntry,
142
+ index: number,
143
+ pageKey: string,
144
+ ): string {
145
+ const layoutPath = entry.layoutPaths[index] ?? '';
146
+ const controllerPath = entry.layoutControllers[index] ?? '';
147
+ return `${pageKey}:layout:${index}:${layoutPath}:${controllerPath}`;
148
+ }
149
+
150
+ function createLayoutMountKeys(
151
+ entry: ViewManifestEntry,
152
+ pageKey: string,
153
+ preservedKeys: string[] = [],
154
+ ): string[] {
155
+ return entry.layoutPaths.map((_, index) =>
156
+ preservedKeys[index] ?? createLayoutMountKey(entry, index, pageKey),
157
+ );
158
+ }
159
+
160
+ function layoutIdentityMatches(
161
+ left: ViewManifestEntry,
162
+ right: ViewManifestEntry,
163
+ index: number,
164
+ ): boolean {
165
+ return (
166
+ left.layoutPaths[index] === right.layoutPaths[index] &&
167
+ left.layoutControllers[index] === right.layoutControllers[index]
168
+ );
169
+ }
170
+
171
+ function shouldPreserveLayout(
172
+ from: ViewManifestEntry,
173
+ to: ViewManifestEntry,
174
+ index: number,
175
+ options: NavigationOptions,
176
+ ): boolean {
177
+ if (options.preserveLayouts === false) {
178
+ return false;
179
+ }
180
+ if (options.preserveLayouts === true) {
181
+ return true;
182
+ }
183
+ return from.layoutPreserve?.[index] === true && to.layoutPreserve?.[index] === true;
184
+ }
185
+
186
+ function computePreservedLayoutDepth(
187
+ from: ViewManifestEntry,
188
+ to: ViewManifestEntry,
189
+ options: NavigationOptions,
190
+ ): number {
191
+ const maxDepth = Math.min(from.layoutPaths.length, to.layoutPaths.length);
192
+ let depth = 0;
193
+ for (let index = 0; index < maxDepth; index += 1) {
194
+ if (!layoutIdentityMatches(from, to, index)) {
195
+ break;
196
+ }
197
+ if (!shouldPreserveLayout(from, to, index, options)) {
198
+ break;
199
+ }
200
+ depth += 1;
201
+ }
202
+ return depth;
203
+ }
204
+
112
205
  async function disposeActiveControllers(): Promise<void> {
113
206
  await disposeControllerLifecycle(activeController);
114
207
  for (const layoutController of activeLayoutControllers) {
@@ -116,10 +209,31 @@
116
209
  }
117
210
  }
118
211
 
212
+ async function disposeChangedControllers(fromLayoutIndex: number): Promise<void> {
213
+ await disposeControllerLifecycle(activeController);
214
+ for (let index = fromLayoutIndex; index < activeLayoutControllers.length; index += 1) {
215
+ await disposeControllerLifecycle(activeLayoutControllers[index]);
216
+ }
217
+ }
218
+
219
+ function mergeLayoutControllers(
220
+ preservedDepth: number,
221
+ createdControllers: Array<unknown>,
222
+ nextLayoutCount: number,
223
+ ): Array<unknown> {
224
+ const merged: Array<unknown> = [];
225
+ for (let index = 0; index < nextLayoutCount; index += 1) {
226
+ merged[index] = index < preservedDepth
227
+ ? activeLayoutControllers[index]
228
+ : createdControllers[index];
229
+ }
230
+ return merged;
231
+ }
232
+
119
233
  setContext('wood:navigate', (
120
234
  viewKey: string,
121
235
  params?: Record<string, unknown>,
122
- options?: { replace?: boolean },
236
+ options?: { replace?: boolean; preserveLayouts?: boolean },
123
237
  ) => {
124
238
  void navigateWithinCurrentWindow(viewKey, params, options).catch((error) => {
125
239
  const message = error instanceof Error ? error.message : String(error);
@@ -148,7 +262,8 @@
148
262
  if (navEntry) {
149
263
  activeParams = { ...navEntry.params };
150
264
  activeQuery = { ...navEntry.query };
151
- activeMountKey = createNavigationMountKey(navEntry.page, navEntry.params);
265
+ activePageKey = createNavigationMountKey(navEntry.page, navEntry.params);
266
+ activeLayoutKeys = createLayoutMountKeys(activeEntry, activePageKey);
152
267
  }
153
268
  return;
154
269
  }
@@ -159,26 +274,38 @@
159
274
  const entry = getViewManifestEntryFn(newKey);
160
275
  if (!entry) return;
161
276
 
162
- const nextMountKey = createNavigationMountKey(newKey, navEntry.params);
277
+ const nextPageKey = createNavigationMountKey(newKey, navEntry.params);
163
278
 
164
- if (nextMountKey === activeMountKey) {
279
+ if (nextPageKey === activePageKey) {
165
280
  activeParams = { ...navEntry.params };
166
281
  activeQuery = { ...navEntry.query };
167
282
  return;
168
283
  }
169
284
 
170
- await disposeActiveControllers();
285
+ const preservedDepth = computePreservedLayoutDepth(
286
+ activeEntry,
287
+ entry,
288
+ navEntry.options ?? {},
289
+ );
290
+ const preservedLayoutKeys = activeLayoutKeys.slice(0, preservedDepth);
291
+
292
+ await disposeChangedControllers(preservedDepth);
171
293
  const [controller, layoutControllers] = await Promise.all([
172
294
  createViewControllerFn(entry),
173
- createLayoutControllersFn(entry),
295
+ createLayoutControllersFn(entry, preservedDepth),
174
296
  ]);
175
297
 
176
298
  activeEntry = entry;
177
299
  activeController = controller;
178
- activeLayoutControllers = layoutControllers;
300
+ activeLayoutControllers = mergeLayoutControllers(
301
+ preservedDepth,
302
+ layoutControllers,
303
+ entry.layoutPaths.length,
304
+ );
179
305
  activeParams = { ...navEntry.params };
180
306
  activeQuery = { ...navEntry.query };
181
- activeMountKey = nextMountKey;
307
+ activePageKey = nextPageKey;
308
+ activeLayoutKeys = createLayoutMountKeys(entry, nextPageKey, preservedLayoutKeys);
182
309
  });
183
310
 
184
311
  const bridge = getWoodBridge();
@@ -207,14 +334,14 @@
207
334
  });
208
335
  </script>
209
336
 
210
- {#key activeMountKey}
211
- <WoodRecursiveRender
212
- layouts={activeEntry.layouts}
213
- layoutControllers={activeLayoutControllers}
214
- view={activeEntry.view}
215
- controller={activeController}
216
- params={activeParams}
217
- query={activeQuery}
218
- page={activePage}
219
- />
220
- {/key}
337
+ <WoodRecursiveRender
338
+ layouts={activeEntry.layouts}
339
+ layoutControllers={activeLayoutControllers}
340
+ layoutKeys={activeLayoutKeys}
341
+ view={activeEntry.view}
342
+ controller={activeController}
343
+ pageKey={activePageKey}
344
+ params={activeParams}
345
+ query={activeQuery}
346
+ page={activePage}
347
+ />
@@ -1,28 +1,35 @@
1
1
  <script lang="ts">
2
2
  import type { PageController } from '../client/controller_contract';
3
+ import WoodRecursiveRender from './WoodRecursiveRender.svelte';
3
4
 
4
5
  let {
5
6
  layouts = [],
6
7
  layoutControllers = [],
8
+ layoutKeys = [],
7
9
  view = null,
8
10
  controller = undefined,
11
+ pageKey = '',
9
12
  params = {},
10
13
  query = {},
11
14
  page = {},
12
15
  } = $props<{
13
16
  layouts?: any[];
14
17
  layoutControllers?: Array<PageController<any, any, any> | undefined>;
18
+ layoutKeys?: string[];
15
19
  view?: any;
16
20
  controller?: PageController<any, any, any>;
21
+ pageKey?: string;
17
22
  params?: Record<string, unknown>;
18
23
  query?: Record<string, unknown>;
19
24
  page?: Record<string, unknown>;
20
25
  }>();
21
26
 
22
- let topLayout = $derived(layouts[0]);
27
+ let TopLayout = $derived(layouts[0]);
23
28
  let childLayouts = $derived(layouts.slice(1));
24
29
  let topLayoutController = $derived(layoutControllers[0]);
25
30
  let childLayoutControllers = $derived(layoutControllers.slice(1));
31
+ let topLayoutKey = $derived(layoutKeys[0] ?? '');
32
+ let childLayoutKeys = $derived(layoutKeys.slice(1));
26
33
  let ViewComponent = $derived(view);
27
34
 
28
35
  let topData = $derived(topLayoutController?.data);
@@ -36,30 +43,56 @@
36
43
 
37
44
  {#if childLayouts.length > 0}
38
45
  {#snippet nestedChild()}
39
- <svelte:self
46
+ <WoodRecursiveRender
40
47
  layouts={childLayouts}
41
48
  layoutControllers={childLayoutControllers}
49
+ layoutKeys={childLayoutKeys}
42
50
  {view}
43
51
  {controller}
52
+ {pageKey}
44
53
  {params}
45
54
  {query}
46
55
  {page}
47
56
  />
48
57
  {/snippet}
49
- <svelte:component
50
- this={topLayout}
51
- data={topData}
52
- input={topInput}
53
- events={topEvents}
54
- {params}
55
- {query}
56
- {page}
57
- children={nestedChild}
58
- />
59
- {:else if topLayout}
58
+ {#key topLayoutKey}
59
+ <TopLayout
60
+ data={topData}
61
+ input={topInput}
62
+ events={topEvents}
63
+ {params}
64
+ {query}
65
+ {page}
66
+ children={nestedChild}
67
+ />
68
+ {/key}
69
+ {:else if TopLayout}
60
70
  {#snippet viewChild()}
61
- <svelte:component
62
- this={ViewComponent}
71
+ {#key pageKey}
72
+ <ViewComponent
73
+ data={viewData}
74
+ input={viewInput}
75
+ events={viewEvents}
76
+ {params}
77
+ {query}
78
+ {page}
79
+ />
80
+ {/key}
81
+ {/snippet}
82
+ {#key topLayoutKey}
83
+ <TopLayout
84
+ data={topData}
85
+ input={topInput}
86
+ events={topEvents}
87
+ {params}
88
+ {query}
89
+ {page}
90
+ children={viewChild}
91
+ />
92
+ {/key}
93
+ {:else}
94
+ {#key pageKey}
95
+ <ViewComponent
63
96
  data={viewData}
64
97
  input={viewInput}
65
98
  events={viewEvents}
@@ -67,25 +100,5 @@
67
100
  {query}
68
101
  {page}
69
102
  />
70
- {/snippet}
71
- <svelte:component
72
- this={topLayout}
73
- data={topData}
74
- input={topInput}
75
- events={topEvents}
76
- {params}
77
- {query}
78
- {page}
79
- children={viewChild}
80
- />
81
- {:else}
82
- <svelte:component
83
- this={ViewComponent}
84
- data={viewData}
85
- input={viewInput}
86
- events={viewEvents}
87
- {params}
88
- {query}
89
- {page}
90
- />
103
+ {/key}
91
104
  {/if}