@noego/wood 0.6.0 → 0.6.2
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.
- package/dist/client/index.cjs +12 -0
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +14 -1
- package/dist/client/index.d.ts +14 -1
- package/dist/client/index.js +11 -0
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/preload_generator.cjs +44 -5
- package/dist/codegen/preload_generator.cjs.map +1 -1
- package/dist/codegen/preload_generator.js +44 -5
- package/dist/codegen/preload_generator.js.map +1 -1
- package/dist/frontend/frontend_surface.d.cts +1 -0
- package/dist/frontend/frontend_surface.d.ts +1 -0
- package/dist/frontend/index.d.cts +1 -0
- package/dist/frontend/index.d.ts +1 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/main/index.cjs +43 -58
- package/dist/main/index.cjs.map +1 -1
- package/dist/main/index.d.cts +2 -2
- package/dist/main/index.d.ts +2 -2
- package/dist/main/index.js +48 -58
- package/dist/main/index.js.map +1 -1
- package/dist/main/shutdown_coordinator.cjs +153 -0
- package/dist/main/shutdown_coordinator.cjs.map +1 -0
- package/dist/main/shutdown_coordinator.d.cts +81 -0
- package/dist/main/shutdown_coordinator.d.ts +81 -0
- package/dist/main/shutdown_coordinator.js +127 -0
- package/dist/main/shutdown_coordinator.js.map +1 -0
- package/dist/navigation/index.cjs.map +1 -1
- package/dist/navigation/index.d.cts +4 -28
- package/dist/navigation/index.d.ts +4 -28
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/layout_preservation.cjs.map +1 -1
- package/dist/navigation/layout_preservation.d.cts +27 -2
- package/dist/navigation/layout_preservation.d.ts +27 -2
- package/dist/navigation/layout_preservation.js.map +1 -1
- package/dist/testing/create_controller_harness.d.cts +1 -0
- package/dist/testing/create_controller_harness.d.ts +1 -0
- package/dist/testing/index.d.cts +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/test_wood_config.d.cts +1 -0
- package/dist/testing/test_wood_config.d.ts +1 -0
- package/dist/testing/wood_env_renderer.d.cts +1 -0
- package/dist/testing/wood_env_renderer.d.ts +1 -0
- package/package.json +1 -1
- package/src/navigation/layout_preservation.ts +3 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const RENDERER_SHUTDOWN_DRAIN_TIMEOUT_MS = 1e3;
|
|
2
|
+
function shouldDeferLastWindowCloseForCleanup(trackedWindowCount, cleanupComplete) {
|
|
3
|
+
return trackedWindowCount === 1 && !cleanupComplete;
|
|
4
|
+
}
|
|
5
|
+
class ShutdownCoordinator {
|
|
6
|
+
constructor(deps) {
|
|
7
|
+
this.deps = deps;
|
|
8
|
+
this.currentPhase = "idle";
|
|
9
|
+
this.shutdown = null;
|
|
10
|
+
}
|
|
11
|
+
get phase() {
|
|
12
|
+
return this.currentPhase;
|
|
13
|
+
}
|
|
14
|
+
get isActive() {
|
|
15
|
+
return this.shutdown !== null;
|
|
16
|
+
}
|
|
17
|
+
get isComplete() {
|
|
18
|
+
return this.currentPhase === "complete";
|
|
19
|
+
}
|
|
20
|
+
/** Resolves when the in-flight (or already finished) shutdown sequence settles. */
|
|
21
|
+
whenSettled() {
|
|
22
|
+
return this.shutdown ?? Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `close` handler for a tracked BrowserWindow. Returns true when the close
|
|
26
|
+
* was intercepted (last window, or a shutdown is already running); false
|
|
27
|
+
* means the window may close normally.
|
|
28
|
+
*/
|
|
29
|
+
handleTrackedWindowClose(event, trackedWindowCount) {
|
|
30
|
+
if (this.isComplete) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (this.shutdown) {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (!shouldDeferLastWindowCloseForCleanup(trackedWindowCount, false)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
this.begin("last-window-close");
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/** `before-quit` handler. Prevents every quit until the sequence completes. */
|
|
45
|
+
handleBeforeQuit(event) {
|
|
46
|
+
if (this.isComplete) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
if (this.shutdown) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.begin("quit");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `window-all-closed` / last `closed` handler. While a shutdown is in flight
|
|
57
|
+
* the coordinator issues the final quit itself, so the cascade caused by
|
|
58
|
+
* destroying our own windows must not request another one.
|
|
59
|
+
*/
|
|
60
|
+
handleAllWindowsClosed() {
|
|
61
|
+
if (this.shutdown) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.deps.requestQuit();
|
|
65
|
+
}
|
|
66
|
+
begin(reason) {
|
|
67
|
+
this.deps.logger.info("shutdown started", { reason });
|
|
68
|
+
this.shutdown = this.run();
|
|
69
|
+
}
|
|
70
|
+
async run() {
|
|
71
|
+
this.hideAllWindows();
|
|
72
|
+
this.currentPhase = "draining";
|
|
73
|
+
try {
|
|
74
|
+
await this.deps.drainRenderers();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
this.deps.logger.warn("renderer shutdown drain failed", { error: toErrorMessage(error) });
|
|
77
|
+
}
|
|
78
|
+
this.destroyAllWindows();
|
|
79
|
+
this.currentPhase = "windows-destroyed";
|
|
80
|
+
this.currentPhase = "cleaning-backend";
|
|
81
|
+
try {
|
|
82
|
+
await this.deps.cleanupBackend();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.deps.logger.warn("backend shutdown cleanup failed", { error: toErrorMessage(error) });
|
|
85
|
+
}
|
|
86
|
+
this.currentPhase = "complete";
|
|
87
|
+
this.deps.logger.info("shutdown complete");
|
|
88
|
+
this.deps.requestQuit();
|
|
89
|
+
}
|
|
90
|
+
hideAllWindows() {
|
|
91
|
+
for (const [id, window] of [...this.deps.trackedWindows()]) {
|
|
92
|
+
this.hideWindow(id, window);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
hideWindow(id, window) {
|
|
96
|
+
try {
|
|
97
|
+
if (!window.isDestroyed()) {
|
|
98
|
+
window.hide();
|
|
99
|
+
this.deps.logger.debug("hid window for shutdown", { windowId: id });
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
this.deps.logger.warn("hiding window for shutdown failed", { windowId: id, error: toErrorMessage(error) });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
destroyAllWindows() {
|
|
106
|
+
for (const [id, window] of [...this.deps.trackedWindows()]) {
|
|
107
|
+
try {
|
|
108
|
+
if (!window.isDestroyed()) {
|
|
109
|
+
window.destroy();
|
|
110
|
+
this.deps.logger.debug("destroyed window on shutdown", { windowId: id });
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.deps.logger.warn("destroying window on shutdown failed", { windowId: id, error: toErrorMessage(error) });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
this.deps.clearTrackedWindows();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function toErrorMessage(error) {
|
|
120
|
+
return error instanceof Error ? error.message : String(error);
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
RENDERER_SHUTDOWN_DRAIN_TIMEOUT_MS,
|
|
124
|
+
ShutdownCoordinator,
|
|
125
|
+
shouldDeferLastWindowCloseForCleanup
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=shutdown_coordinator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/main/shutdown_coordinator.ts"],"sourcesContent":["import type { Logger } from '@noego/logger';\n\n/**\n * Bounded time the last renderers get to acknowledge the shutdown drain\n * (trace transport close + app-registered persistence hooks) before their\n * windows are destroyed.\n */\nexport const RENDERER_SHUTDOWN_DRAIN_TIMEOUT_MS = 1_000;\n\nexport type ShutdownWindow = Readonly<{\n isDestroyed(): boolean;\n hide(): void;\n destroy(): void;\n}>;\n\nexport type ShutdownPreventableEvent = Readonly<{\n preventDefault(): void;\n}>;\n\nexport type ShutdownReason = 'last-window-close' | 'quit';\n\nexport type ShutdownPhase =\n | 'idle'\n | 'draining'\n | 'windows-destroyed'\n | 'cleaning-backend'\n | 'complete';\n\nexport type ShutdownCoordinatorDependencies = Readonly<{\n /** Live view of the tracked windows; iterated as a snapshot on each use. */\n trackedWindows(): Iterable<readonly [string, ShutdownWindow]>;\n /** Forget every tracked window after destruction. */\n clearTrackedWindows(): void;\n /** Bounded renderer drain (traces + persistence acknowledgements). */\n drainRenderers(): Promise<void>;\n /** Backend runtime/service teardown; runs only after windows are gone. */\n cleanupBackend(): Promise<void>;\n /** Ask Electron to quit; the coordinator calls this exactly once, at the end. */\n requestQuit(): void;\n logger: Pick<Logger, 'debug' | 'info' | 'warn'>;\n}>;\n\nexport function shouldDeferLastWindowCloseForCleanup(\n trackedWindowCount: number,\n cleanupComplete: boolean,\n): boolean {\n return trackedWindowCount === 1 && !cleanupComplete;\n}\n\n/**\n * Owns the Desktop shutdown sequence so windows disappear first and the\n * backend is torn down last:\n *\n * accepted last-window Close or Quit\n * -> hide every tracked window immediately\n * -> bounded renderer drain (trace close + persistence acknowledgements)\n * -> destroy every tracked window\n * -> backend runtime/service cleanup\n * -> app.quit()\n *\n * The sequence runs exactly once. Every Electron quit attempt that arrives\n * while it is in flight (repeated Cmd+Q, the `closed` -> `window-all-closed`\n * cascade caused by our own window destruction, a second Close) is prevented\n * so the process cannot exit before cleanup finishes. Once complete, the final\n * `requestQuit()` is allowed through untouched.\n */\nexport class ShutdownCoordinator {\n private currentPhase: ShutdownPhase = 'idle';\n private shutdown: Promise<void> | null = null;\n\n constructor(private readonly deps: ShutdownCoordinatorDependencies) {}\n\n get phase(): ShutdownPhase {\n return this.currentPhase;\n }\n\n get isActive(): boolean {\n return this.shutdown !== null;\n }\n\n get isComplete(): boolean {\n return this.currentPhase === 'complete';\n }\n\n /** Resolves when the in-flight (or already finished) shutdown sequence settles. */\n whenSettled(): Promise<void> {\n return this.shutdown ?? Promise.resolve();\n }\n\n /**\n * `close` handler for a tracked BrowserWindow. Returns true when the close\n * was intercepted (last window, or a shutdown is already running); false\n * means the window may close normally.\n */\n handleTrackedWindowClose(\n event: ShutdownPreventableEvent,\n trackedWindowCount: number,\n ): boolean {\n if (this.isComplete) {\n return false;\n }\n if (this.shutdown) {\n // Shutdown owns window destruction; do not let a competing close race it.\n event.preventDefault();\n return true;\n }\n if (!shouldDeferLastWindowCloseForCleanup(trackedWindowCount, false)) {\n return false;\n }\n event.preventDefault();\n // begin() hides every tracked window synchronously, including this one.\n this.begin('last-window-close');\n return true;\n }\n\n /** `before-quit` handler. Prevents every quit until the sequence completes. */\n handleBeforeQuit(event: ShutdownPreventableEvent): void {\n if (this.isComplete) {\n return;\n }\n event.preventDefault();\n if (this.shutdown) {\n return;\n }\n this.begin('quit');\n }\n\n /**\n * `window-all-closed` / last `closed` handler. While a shutdown is in flight\n * the coordinator issues the final quit itself, so the cascade caused by\n * destroying our own windows must not request another one.\n */\n handleAllWindowsClosed(): void {\n if (this.shutdown) {\n return;\n }\n this.deps.requestQuit();\n }\n\n private begin(reason: ShutdownReason): void {\n this.deps.logger.info('shutdown started', { reason });\n this.shutdown = this.run();\n }\n\n private async run(): Promise<void> {\n this.hideAllWindows();\n\n this.currentPhase = 'draining';\n try {\n await this.deps.drainRenderers();\n } catch (error) {\n this.deps.logger.warn('renderer shutdown drain failed', { error: toErrorMessage(error) });\n }\n\n this.destroyAllWindows();\n this.currentPhase = 'windows-destroyed';\n\n this.currentPhase = 'cleaning-backend';\n try {\n await this.deps.cleanupBackend();\n } catch (error) {\n this.deps.logger.warn('backend shutdown cleanup failed', { error: toErrorMessage(error) });\n }\n\n this.currentPhase = 'complete';\n this.deps.logger.info('shutdown complete');\n this.deps.requestQuit();\n }\n\n private hideAllWindows(): void {\n for (const [id, window] of [...this.deps.trackedWindows()]) {\n this.hideWindow(id, window);\n }\n }\n\n private hideWindow(id: string, window: ShutdownWindow): void {\n try {\n if (!window.isDestroyed()) {\n window.hide();\n this.deps.logger.debug('hid window for shutdown', { windowId: id });\n }\n } catch (error) {\n this.deps.logger.warn('hiding window for shutdown failed', { windowId: id, error: toErrorMessage(error) });\n }\n }\n\n private destroyAllWindows(): void {\n for (const [id, window] of [...this.deps.trackedWindows()]) {\n try {\n if (!window.isDestroyed()) {\n window.destroy();\n this.deps.logger.debug('destroyed window on shutdown', { windowId: id });\n }\n } catch (error) {\n this.deps.logger.warn('destroying window on shutdown failed', { windowId: id, error: toErrorMessage(error) });\n }\n }\n this.deps.clearTrackedWindows();\n }\n}\n\nfunction toErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/navigation/index.ts"],"sourcesContent":["import { emitRendererTrace } from '../tracing/index.js';\n\nconst NAVIGATION_SYMBOL = Symbol.for('@noego/wood.navigation');\nconst HISTORY_STATE_MARKER = '__wood_navigation_state__';\n\nexport interface NavigationEntry {\n page: string;\n params: Record<string, string>;\n query: Record<string, string>;\n options: NavigationOptions;\n}\n\nexport interface NavigationOptions {\n preserveLayouts?: boolean;\n}\n\ntype NavigationHistoryState = NavigationEntry & {\n [HISTORY_STATE_MARKER]: true;\n};\n\ntype NavigationListener = (entry: NavigationEntry | null) => void;\n\nexport class Navigation {\n private entries: NavigationEntry[] = [];\n private index = -1;\n private listeners = new Set<NavigationListener>();\n\n getCurrent(): NavigationEntry | null {\n if (this.index < 0 || this.index >= this.entries.length) {\n return null;\n }\n return this.cloneEntry(this.entries[this.index]);\n }\n\n getPages(): NavigationEntry[] {\n return this.entries.map((entry) => this.cloneEntry(entry));\n }\n\n getParams(): Record<string, string> {\n const current = this.getCurrent();\n if (!current) {\n return {};\n }\n return { ...current.params };\n }\n\n replace(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n\n if (this.index < 0) {\n this.entries.push(entry);\n this.index = 0;\n } else {\n this.entries[this.index] = entry;\n }\n\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.replace',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n if (this.index < this.entries.length - 1) {\n this.entries = this.entries.slice(0, this.index + 1);\n }\n this.entries.push(entry);\n this.index = this.entries.length - 1;\n\n this.writeWindowHistory('pushState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.go',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n goto(delta: number): NavigationEntry | null {\n return this.moveCursor(delta);\n }\n\n goBack(): NavigationEntry | null {\n return this.goto(-1);\n }\n\n goForward(): NavigationEntry | null {\n return this.goto(1);\n }\n\n subscribe(listener: NavigationListener): () => void {\n this.listeners.add(listener);\n listener(this.getCurrent());\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private moveCursor(delta: number): NavigationEntry | null {\n if (delta === 0) {\n return this.getCurrent();\n }\n\n const nextIndex = this.index + delta;\n if (nextIndex < 0 || nextIndex >= this.entries.length) {\n return this.getCurrent();\n }\n\n this.index = nextIndex;\n const entry = this.entries[this.index];\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.goto',\n payload: {\n delta,\n page: entry.page,\n },\n });\n return this.cloneEntry(entry);\n }\n\n private notify(): void {\n const current = this.getCurrent();\n for (const listener of this.listeners) {\n listener(current);\n }\n }\n\n private createEntry(\n page: string,\n params: Record<string, string>,\n query: Record<string, string>,\n options: NavigationOptions,\n ): NavigationEntry {\n return {\n page,\n params: this.normalizeRecord(params),\n query: this.normalizeRecord(query),\n options: { ...options },\n };\n }\n\n private cloneEntry(entry: NavigationEntry): NavigationEntry {\n return {\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n }\n\n private normalizeRecord(input: Record<string, string>): Record<string, string> {\n const output: Record<string, string> = {};\n for (const [key, value] of Object.entries(input ?? {})) {\n output[String(key)] = String(value);\n }\n return output;\n }\n\n private writeWindowHistory(\n mode: 'pushState' | 'replaceState',\n entry: NavigationEntry,\n ): void {\n if (typeof window === 'undefined') {\n return;\n }\n\n const state: NavigationHistoryState = {\n [HISTORY_STATE_MARKER]: true,\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n\n window.history[mode](state, '');\n }\n}\n\ntype GlobalWithNavigation = typeof globalThis & {\n [NAVIGATION_SYMBOL]?: Navigation;\n};\n\nfunction createNavigationInstance(): Navigation {\n return new Navigation();\n}\n\nexport function getNavigation(): Navigation {\n const globalObj = globalThis as GlobalWithNavigation;\n if (!globalObj[NAVIGATION_SYMBOL]) {\n globalObj[NAVIGATION_SYMBOL] = createNavigationInstance();\n }\n return globalObj[NAVIGATION_SYMBOL];\n}\n\nexport function goto(\n delta: number,\n): NavigationEntry | null {\n return getNavigation().goto(delta);\n}\n\nexport function go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n): NavigationEntry {\n return getNavigation().go(page, params, query, options);\n}\n\nexport function goback(): NavigationEntry | null {\n return goto(-1);\n}\n\n/** Test-only helper to clear the navigation singleton. */\nexport function clearNavigationForTests(): void {\n const globalObj = globalThis as GlobalWithNavigation;\n delete globalObj[NAVIGATION_SYMBOL];\n}\n\n// Dev/test-only automation introspection surface (Axe AXE-WP-02).\n// Benign import cycle: automation_introspection uses getNavigation lazily.\n// NOTE: keep these re-exports single-line — the tsup extension-rewrite plugin\n// only rewrites single-line import/export specifiers.\nexport { installAutomationIntrospection, WOOD_AUTOMATION_GLOBAL_KEY } from './automation_introspection.cjs';\nexport type { WoodAutomationSnapshot, WoodAutomationEvent, WoodAutomationIntrospection, InstallAutomationIntrospectionOptions } from './automation_introspection.cjs';\nexport { computePreservedLayoutDepth } from './layout_preservation.cjs';\nexport type { LayoutStack } from './layout_preservation.cjs';\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/navigation/index.ts"],"sourcesContent":["import { emitRendererTrace } from '../tracing/index.js';\n\nconst NAVIGATION_SYMBOL = Symbol.for('@noego/wood.navigation');\nconst HISTORY_STATE_MARKER = '__wood_navigation_state__';\n\nexport interface NavigationEntry {\n page: string;\n params: Record<string, string>;\n query: Record<string, string>;\n options: NavigationOptions;\n}\n\nimport type { NavigationOptions } from './layout_preservation.js';\nexport type { NavigationOptions } from './layout_preservation.js';\n\ntype NavigationHistoryState = NavigationEntry & {\n [HISTORY_STATE_MARKER]: true;\n};\n\ntype NavigationListener = (entry: NavigationEntry | null) => void;\n\nexport class Navigation {\n private entries: NavigationEntry[] = [];\n private index = -1;\n private listeners = new Set<NavigationListener>();\n\n getCurrent(): NavigationEntry | null {\n if (this.index < 0 || this.index >= this.entries.length) {\n return null;\n }\n return this.cloneEntry(this.entries[this.index]);\n }\n\n getPages(): NavigationEntry[] {\n return this.entries.map((entry) => this.cloneEntry(entry));\n }\n\n getParams(): Record<string, string> {\n const current = this.getCurrent();\n if (!current) {\n return {};\n }\n return { ...current.params };\n }\n\n replace(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n\n if (this.index < 0) {\n this.entries.push(entry);\n this.index = 0;\n } else {\n this.entries[this.index] = entry;\n }\n\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.replace',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n if (this.index < this.entries.length - 1) {\n this.entries = this.entries.slice(0, this.index + 1);\n }\n this.entries.push(entry);\n this.index = this.entries.length - 1;\n\n this.writeWindowHistory('pushState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.go',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n goto(delta: number): NavigationEntry | null {\n return this.moveCursor(delta);\n }\n\n goBack(): NavigationEntry | null {\n return this.goto(-1);\n }\n\n goForward(): NavigationEntry | null {\n return this.goto(1);\n }\n\n subscribe(listener: NavigationListener): () => void {\n this.listeners.add(listener);\n listener(this.getCurrent());\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private moveCursor(delta: number): NavigationEntry | null {\n if (delta === 0) {\n return this.getCurrent();\n }\n\n const nextIndex = this.index + delta;\n if (nextIndex < 0 || nextIndex >= this.entries.length) {\n return this.getCurrent();\n }\n\n this.index = nextIndex;\n const entry = this.entries[this.index];\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.goto',\n payload: {\n delta,\n page: entry.page,\n },\n });\n return this.cloneEntry(entry);\n }\n\n private notify(): void {\n const current = this.getCurrent();\n for (const listener of this.listeners) {\n listener(current);\n }\n }\n\n private createEntry(\n page: string,\n params: Record<string, string>,\n query: Record<string, string>,\n options: NavigationOptions,\n ): NavigationEntry {\n return {\n page,\n params: this.normalizeRecord(params),\n query: this.normalizeRecord(query),\n options: { ...options },\n };\n }\n\n private cloneEntry(entry: NavigationEntry): NavigationEntry {\n return {\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n }\n\n private normalizeRecord(input: Record<string, string>): Record<string, string> {\n const output: Record<string, string> = {};\n for (const [key, value] of Object.entries(input ?? {})) {\n output[String(key)] = String(value);\n }\n return output;\n }\n\n private writeWindowHistory(\n mode: 'pushState' | 'replaceState',\n entry: NavigationEntry,\n ): void {\n if (typeof window === 'undefined') {\n return;\n }\n\n const state: NavigationHistoryState = {\n [HISTORY_STATE_MARKER]: true,\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n\n window.history[mode](state, '');\n }\n}\n\ntype GlobalWithNavigation = typeof globalThis & {\n [NAVIGATION_SYMBOL]?: Navigation;\n};\n\nfunction createNavigationInstance(): Navigation {\n return new Navigation();\n}\n\nexport function getNavigation(): Navigation {\n const globalObj = globalThis as GlobalWithNavigation;\n if (!globalObj[NAVIGATION_SYMBOL]) {\n globalObj[NAVIGATION_SYMBOL] = createNavigationInstance();\n }\n return globalObj[NAVIGATION_SYMBOL];\n}\n\nexport function goto(\n delta: number,\n): NavigationEntry | null {\n return getNavigation().goto(delta);\n}\n\nexport function go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n): NavigationEntry {\n return getNavigation().go(page, params, query, options);\n}\n\nexport function goback(): NavigationEntry | null {\n return goto(-1);\n}\n\n/** Test-only helper to clear the navigation singleton. */\nexport function clearNavigationForTests(): void {\n const globalObj = globalThis as GlobalWithNavigation;\n delete globalObj[NAVIGATION_SYMBOL];\n}\n\n// Dev/test-only automation introspection surface (Axe AXE-WP-02).\n// Benign import cycle: automation_introspection uses getNavigation lazily.\n// NOTE: keep these re-exports single-line — the tsup extension-rewrite plugin\n// only rewrites single-line import/export specifiers.\nexport { installAutomationIntrospection, WOOD_AUTOMATION_GLOBAL_KEY } from './automation_introspection.cjs';\nexport type { WoodAutomationSnapshot, WoodAutomationEvent, WoodAutomationIntrospection, InstallAutomationIntrospectionOptions } from './automation_introspection.cjs';\nexport { computePreservedLayoutDepth } from './layout_preservation.cjs';\nexport type { LayoutStack } from './layout_preservation.cjs';\n"],"mappings":"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","names":[]}
|
|
@@ -1,38 +1,14 @@
|
|
|
1
|
+
import { NavigationOptions } from './layout_preservation.cjs';
|
|
2
|
+
export { LayoutStack, computePreservedLayoutDepth } from './layout_preservation.cjs';
|
|
1
3
|
export { InstallAutomationIntrospectionOptions, WOOD_AUTOMATION_GLOBAL_KEY, WoodAutomationEvent, WoodAutomationIntrospection, WoodAutomationSnapshot, installAutomationIntrospection } from './automation_introspection.cjs';
|
|
2
4
|
|
|
3
|
-
/**
|
|
4
|
-
* Layout preservation depth — the single production rule for how many
|
|
5
|
-
* leading layouts a navigation transition keeps mounted.
|
|
6
|
-
*
|
|
7
|
-
* One implementation, two callers: the renderer's NavigationShell (real
|
|
8
|
-
* component transitions) and the Node frontend application slice
|
|
9
|
-
* (`createFrontendSurface`). They must agree — a slice test that preserved
|
|
10
|
-
* a layout the renderer rebuilds, or vice versa, would be evidence about a
|
|
11
|
-
* rule that does not exist in production.
|
|
12
|
-
*
|
|
13
|
-
* The rule: walk the layout stacks in parallel and keep a layout only while
|
|
14
|
-
* BOTH sides declare the same layout path, the same layout controller, and
|
|
15
|
-
* both mark it `preserve: true`. `NavigationOptions.preserveLayouts`
|
|
16
|
-
* overrides the declaration in both directions.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/** The layout stack of one view, in the shape both callers can supply. */
|
|
20
|
-
interface LayoutStack {
|
|
21
|
-
layoutPaths: readonly string[];
|
|
22
|
-
layoutControllers?: ReadonlyArray<string | undefined>;
|
|
23
|
-
layoutPreserve?: readonly boolean[];
|
|
24
|
-
}
|
|
25
|
-
declare function computePreservedLayoutDepth(from: LayoutStack, to: LayoutStack, options?: NavigationOptions): number;
|
|
26
|
-
|
|
27
5
|
interface NavigationEntry {
|
|
28
6
|
page: string;
|
|
29
7
|
params: Record<string, string>;
|
|
30
8
|
query: Record<string, string>;
|
|
31
9
|
options: NavigationOptions;
|
|
32
10
|
}
|
|
33
|
-
|
|
34
|
-
preserveLayouts?: boolean;
|
|
35
|
-
}
|
|
11
|
+
|
|
36
12
|
type NavigationListener = (entry: NavigationEntry | null) => void;
|
|
37
13
|
declare class Navigation {
|
|
38
14
|
private entries;
|
|
@@ -61,4 +37,4 @@ declare function goback(): NavigationEntry | null;
|
|
|
61
37
|
/** Test-only helper to clear the navigation singleton. */
|
|
62
38
|
declare function clearNavigationForTests(): void;
|
|
63
39
|
|
|
64
|
-
export {
|
|
40
|
+
export { Navigation, type NavigationEntry, NavigationOptions, clearNavigationForTests, getNavigation, go, goback, goto };
|
|
@@ -1,38 +1,14 @@
|
|
|
1
|
+
import { NavigationOptions } from './layout_preservation.js';
|
|
2
|
+
export { LayoutStack, computePreservedLayoutDepth } from './layout_preservation.js';
|
|
1
3
|
export { InstallAutomationIntrospectionOptions, WOOD_AUTOMATION_GLOBAL_KEY, WoodAutomationEvent, WoodAutomationIntrospection, WoodAutomationSnapshot, installAutomationIntrospection } from './automation_introspection.js';
|
|
2
4
|
|
|
3
|
-
/**
|
|
4
|
-
* Layout preservation depth — the single production rule for how many
|
|
5
|
-
* leading layouts a navigation transition keeps mounted.
|
|
6
|
-
*
|
|
7
|
-
* One implementation, two callers: the renderer's NavigationShell (real
|
|
8
|
-
* component transitions) and the Node frontend application slice
|
|
9
|
-
* (`createFrontendSurface`). They must agree — a slice test that preserved
|
|
10
|
-
* a layout the renderer rebuilds, or vice versa, would be evidence about a
|
|
11
|
-
* rule that does not exist in production.
|
|
12
|
-
*
|
|
13
|
-
* The rule: walk the layout stacks in parallel and keep a layout only while
|
|
14
|
-
* BOTH sides declare the same layout path, the same layout controller, and
|
|
15
|
-
* both mark it `preserve: true`. `NavigationOptions.preserveLayouts`
|
|
16
|
-
* overrides the declaration in both directions.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/** The layout stack of one view, in the shape both callers can supply. */
|
|
20
|
-
interface LayoutStack {
|
|
21
|
-
layoutPaths: readonly string[];
|
|
22
|
-
layoutControllers?: ReadonlyArray<string | undefined>;
|
|
23
|
-
layoutPreserve?: readonly boolean[];
|
|
24
|
-
}
|
|
25
|
-
declare function computePreservedLayoutDepth(from: LayoutStack, to: LayoutStack, options?: NavigationOptions): number;
|
|
26
|
-
|
|
27
5
|
interface NavigationEntry {
|
|
28
6
|
page: string;
|
|
29
7
|
params: Record<string, string>;
|
|
30
8
|
query: Record<string, string>;
|
|
31
9
|
options: NavigationOptions;
|
|
32
10
|
}
|
|
33
|
-
|
|
34
|
-
preserveLayouts?: boolean;
|
|
35
|
-
}
|
|
11
|
+
|
|
36
12
|
type NavigationListener = (entry: NavigationEntry | null) => void;
|
|
37
13
|
declare class Navigation {
|
|
38
14
|
private entries;
|
|
@@ -61,4 +37,4 @@ declare function goback(): NavigationEntry | null;
|
|
|
61
37
|
/** Test-only helper to clear the navigation singleton. */
|
|
62
38
|
declare function clearNavigationForTests(): void;
|
|
63
39
|
|
|
64
|
-
export {
|
|
40
|
+
export { Navigation, type NavigationEntry, NavigationOptions, clearNavigationForTests, getNavigation, go, goback, goto };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/navigation/index.ts"],"sourcesContent":["import { emitRendererTrace } from '../tracing/index.js';\n\nconst NAVIGATION_SYMBOL = Symbol.for('@noego/wood.navigation');\nconst HISTORY_STATE_MARKER = '__wood_navigation_state__';\n\nexport interface NavigationEntry {\n page: string;\n params: Record<string, string>;\n query: Record<string, string>;\n options: NavigationOptions;\n}\n\nexport interface NavigationOptions {\n preserveLayouts?: boolean;\n}\n\ntype NavigationHistoryState = NavigationEntry & {\n [HISTORY_STATE_MARKER]: true;\n};\n\ntype NavigationListener = (entry: NavigationEntry | null) => void;\n\nexport class Navigation {\n private entries: NavigationEntry[] = [];\n private index = -1;\n private listeners = new Set<NavigationListener>();\n\n getCurrent(): NavigationEntry | null {\n if (this.index < 0 || this.index >= this.entries.length) {\n return null;\n }\n return this.cloneEntry(this.entries[this.index]);\n }\n\n getPages(): NavigationEntry[] {\n return this.entries.map((entry) => this.cloneEntry(entry));\n }\n\n getParams(): Record<string, string> {\n const current = this.getCurrent();\n if (!current) {\n return {};\n }\n return { ...current.params };\n }\n\n replace(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n\n if (this.index < 0) {\n this.entries.push(entry);\n this.index = 0;\n } else {\n this.entries[this.index] = entry;\n }\n\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.replace',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n if (this.index < this.entries.length - 1) {\n this.entries = this.entries.slice(0, this.index + 1);\n }\n this.entries.push(entry);\n this.index = this.entries.length - 1;\n\n this.writeWindowHistory('pushState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.go',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n goto(delta: number): NavigationEntry | null {\n return this.moveCursor(delta);\n }\n\n goBack(): NavigationEntry | null {\n return this.goto(-1);\n }\n\n goForward(): NavigationEntry | null {\n return this.goto(1);\n }\n\n subscribe(listener: NavigationListener): () => void {\n this.listeners.add(listener);\n listener(this.getCurrent());\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private moveCursor(delta: number): NavigationEntry | null {\n if (delta === 0) {\n return this.getCurrent();\n }\n\n const nextIndex = this.index + delta;\n if (nextIndex < 0 || nextIndex >= this.entries.length) {\n return this.getCurrent();\n }\n\n this.index = nextIndex;\n const entry = this.entries[this.index];\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.goto',\n payload: {\n delta,\n page: entry.page,\n },\n });\n return this.cloneEntry(entry);\n }\n\n private notify(): void {\n const current = this.getCurrent();\n for (const listener of this.listeners) {\n listener(current);\n }\n }\n\n private createEntry(\n page: string,\n params: Record<string, string>,\n query: Record<string, string>,\n options: NavigationOptions,\n ): NavigationEntry {\n return {\n page,\n params: this.normalizeRecord(params),\n query: this.normalizeRecord(query),\n options: { ...options },\n };\n }\n\n private cloneEntry(entry: NavigationEntry): NavigationEntry {\n return {\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n }\n\n private normalizeRecord(input: Record<string, string>): Record<string, string> {\n const output: Record<string, string> = {};\n for (const [key, value] of Object.entries(input ?? {})) {\n output[String(key)] = String(value);\n }\n return output;\n }\n\n private writeWindowHistory(\n mode: 'pushState' | 'replaceState',\n entry: NavigationEntry,\n ): void {\n if (typeof window === 'undefined') {\n return;\n }\n\n const state: NavigationHistoryState = {\n [HISTORY_STATE_MARKER]: true,\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n\n window.history[mode](state, '');\n }\n}\n\ntype GlobalWithNavigation = typeof globalThis & {\n [NAVIGATION_SYMBOL]?: Navigation;\n};\n\nfunction createNavigationInstance(): Navigation {\n return new Navigation();\n}\n\nexport function getNavigation(): Navigation {\n const globalObj = globalThis as GlobalWithNavigation;\n if (!globalObj[NAVIGATION_SYMBOL]) {\n globalObj[NAVIGATION_SYMBOL] = createNavigationInstance();\n }\n return globalObj[NAVIGATION_SYMBOL];\n}\n\nexport function goto(\n delta: number,\n): NavigationEntry | null {\n return getNavigation().goto(delta);\n}\n\nexport function go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n): NavigationEntry {\n return getNavigation().go(page, params, query, options);\n}\n\nexport function goback(): NavigationEntry | null {\n return goto(-1);\n}\n\n/** Test-only helper to clear the navigation singleton. */\nexport function clearNavigationForTests(): void {\n const globalObj = globalThis as GlobalWithNavigation;\n delete globalObj[NAVIGATION_SYMBOL];\n}\n\n// Dev/test-only automation introspection surface (Axe AXE-WP-02).\n// Benign import cycle: automation_introspection uses getNavigation lazily.\n// NOTE: keep these re-exports single-line — the tsup extension-rewrite plugin\n// only rewrites single-line import/export specifiers.\nexport { installAutomationIntrospection, WOOD_AUTOMATION_GLOBAL_KEY } from './automation_introspection.js';\nexport type { WoodAutomationSnapshot, WoodAutomationEvent, WoodAutomationIntrospection, InstallAutomationIntrospectionOptions } from './automation_introspection.js';\nexport { computePreservedLayoutDepth } from './layout_preservation.js';\nexport type { LayoutStack } from './layout_preservation.js';\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/navigation/index.ts"],"sourcesContent":["import { emitRendererTrace } from '../tracing/index.js';\n\nconst NAVIGATION_SYMBOL = Symbol.for('@noego/wood.navigation');\nconst HISTORY_STATE_MARKER = '__wood_navigation_state__';\n\nexport interface NavigationEntry {\n page: string;\n params: Record<string, string>;\n query: Record<string, string>;\n options: NavigationOptions;\n}\n\nimport type { NavigationOptions } from './layout_preservation.js';\nexport type { NavigationOptions } from './layout_preservation.js';\n\ntype NavigationHistoryState = NavigationEntry & {\n [HISTORY_STATE_MARKER]: true;\n};\n\ntype NavigationListener = (entry: NavigationEntry | null) => void;\n\nexport class Navigation {\n private entries: NavigationEntry[] = [];\n private index = -1;\n private listeners = new Set<NavigationListener>();\n\n getCurrent(): NavigationEntry | null {\n if (this.index < 0 || this.index >= this.entries.length) {\n return null;\n }\n return this.cloneEntry(this.entries[this.index]);\n }\n\n getPages(): NavigationEntry[] {\n return this.entries.map((entry) => this.cloneEntry(entry));\n }\n\n getParams(): Record<string, string> {\n const current = this.getCurrent();\n if (!current) {\n return {};\n }\n return { ...current.params };\n }\n\n replace(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n\n if (this.index < 0) {\n this.entries.push(entry);\n this.index = 0;\n } else {\n this.entries[this.index] = entry;\n }\n\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.replace',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n if (this.index < this.entries.length - 1) {\n this.entries = this.entries.slice(0, this.index + 1);\n }\n this.entries.push(entry);\n this.index = this.entries.length - 1;\n\n this.writeWindowHistory('pushState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.go',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n goto(delta: number): NavigationEntry | null {\n return this.moveCursor(delta);\n }\n\n goBack(): NavigationEntry | null {\n return this.goto(-1);\n }\n\n goForward(): NavigationEntry | null {\n return this.goto(1);\n }\n\n subscribe(listener: NavigationListener): () => void {\n this.listeners.add(listener);\n listener(this.getCurrent());\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private moveCursor(delta: number): NavigationEntry | null {\n if (delta === 0) {\n return this.getCurrent();\n }\n\n const nextIndex = this.index + delta;\n if (nextIndex < 0 || nextIndex >= this.entries.length) {\n return this.getCurrent();\n }\n\n this.index = nextIndex;\n const entry = this.entries[this.index];\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.goto',\n payload: {\n delta,\n page: entry.page,\n },\n });\n return this.cloneEntry(entry);\n }\n\n private notify(): void {\n const current = this.getCurrent();\n for (const listener of this.listeners) {\n listener(current);\n }\n }\n\n private createEntry(\n page: string,\n params: Record<string, string>,\n query: Record<string, string>,\n options: NavigationOptions,\n ): NavigationEntry {\n return {\n page,\n params: this.normalizeRecord(params),\n query: this.normalizeRecord(query),\n options: { ...options },\n };\n }\n\n private cloneEntry(entry: NavigationEntry): NavigationEntry {\n return {\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n }\n\n private normalizeRecord(input: Record<string, string>): Record<string, string> {\n const output: Record<string, string> = {};\n for (const [key, value] of Object.entries(input ?? {})) {\n output[String(key)] = String(value);\n }\n return output;\n }\n\n private writeWindowHistory(\n mode: 'pushState' | 'replaceState',\n entry: NavigationEntry,\n ): void {\n if (typeof window === 'undefined') {\n return;\n }\n\n const state: NavigationHistoryState = {\n [HISTORY_STATE_MARKER]: true,\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n\n window.history[mode](state, '');\n }\n}\n\ntype GlobalWithNavigation = typeof globalThis & {\n [NAVIGATION_SYMBOL]?: Navigation;\n};\n\nfunction createNavigationInstance(): Navigation {\n return new Navigation();\n}\n\nexport function getNavigation(): Navigation {\n const globalObj = globalThis as GlobalWithNavigation;\n if (!globalObj[NAVIGATION_SYMBOL]) {\n globalObj[NAVIGATION_SYMBOL] = createNavigationInstance();\n }\n return globalObj[NAVIGATION_SYMBOL];\n}\n\nexport function goto(\n delta: number,\n): NavigationEntry | null {\n return getNavigation().goto(delta);\n}\n\nexport function go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n): NavigationEntry {\n return getNavigation().go(page, params, query, options);\n}\n\nexport function goback(): NavigationEntry | null {\n return goto(-1);\n}\n\n/** Test-only helper to clear the navigation singleton. */\nexport function clearNavigationForTests(): void {\n const globalObj = globalThis as GlobalWithNavigation;\n delete globalObj[NAVIGATION_SYMBOL];\n}\n\n// Dev/test-only automation introspection surface (Axe AXE-WP-02).\n// Benign import cycle: automation_introspection uses getNavigation lazily.\n// NOTE: keep these re-exports single-line — the tsup extension-rewrite plugin\n// only rewrites single-line import/export specifiers.\nexport { installAutomationIntrospection, WOOD_AUTOMATION_GLOBAL_KEY } from './automation_introspection.js';\nexport type { WoodAutomationSnapshot, WoodAutomationEvent, WoodAutomationIntrospection, InstallAutomationIntrospectionOptions } from './automation_introspection.js';\nexport { computePreservedLayoutDepth } from './layout_preservation.js';\nexport type { LayoutStack } from './layout_preservation.js';\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/navigation/layout_preservation.ts"],"sourcesContent":["/**\n * Layout preservation depth — the single production rule for how many\n * leading layouts a navigation transition keeps mounted.\n *\n * One implementation, two callers: the renderer's NavigationShell (real\n * component transitions) and the Node frontend application slice\n * (`createFrontendSurface`). They must agree — a slice test that preserved\n * a layout the renderer rebuilds, or vice versa, would be evidence about a\n * rule that does not exist in production.\n *\n * The rule: walk the layout stacks in parallel and keep a layout only while\n * BOTH sides declare the same layout path, the same layout controller, and\n * both mark it `preserve: true`. `NavigationOptions.preserveLayouts`\n * overrides the declaration in both directions.\n */\
|
|
1
|
+
{"version":3,"sources":["../../src/navigation/layout_preservation.ts"],"sourcesContent":["/**\n * Layout preservation depth — the single production rule for how many\n * leading layouts a navigation transition keeps mounted.\n *\n * One implementation, two callers: the renderer's NavigationShell (real\n * component transitions) and the Node frontend application slice\n * (`createFrontendSurface`). They must agree — a slice test that preserved\n * a layout the renderer rebuilds, or vice versa, would be evidence about a\n * rule that does not exist in production.\n *\n * The rule: walk the layout stacks in parallel and keep a layout only while\n * BOTH sides declare the same layout path, the same layout controller, and\n * both mark it `preserve: true`. `NavigationOptions.preserveLayouts`\n * overrides the declaration in both directions.\n */\nexport interface NavigationOptions {\n preserveLayouts?: boolean;\n}\n\n/** The layout stack of one view, in the shape both callers can supply. */\nexport interface LayoutStack {\n layoutPaths: readonly string[];\n layoutControllers?: ReadonlyArray<string | undefined>;\n layoutPreserve?: readonly boolean[];\n}\n\nexport function computePreservedLayoutDepth(\n from: LayoutStack,\n to: LayoutStack,\n options: NavigationOptions = {},\n): number {\n const maxDepth = Math.min(from.layoutPaths.length, to.layoutPaths.length);\n let depth = 0;\n for (let index = 0; index < maxDepth; index += 1) {\n if (from.layoutPaths[index] !== to.layoutPaths[index]) break;\n if (from.layoutControllers?.[index] !== to.layoutControllers?.[index]) break;\n if (options.preserveLayouts === false) break;\n if (\n options.preserveLayouts !== true &&\n (from.layoutPreserve?.[index] !== true || to.layoutPreserve?.[index] !== true)\n ) {\n break;\n }\n depth += 1;\n }\n return depth;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA0BO,SAAS,4BACd,MACA,IACA,UAA6B,CAAC,GACtB;AACR,QAAM,WAAW,KAAK,IAAI,KAAK,YAAY,QAAQ,GAAG,YAAY,MAAM;AACxE,MAAI,QAAQ;AACZ,WAAS,QAAQ,GAAG,QAAQ,UAAU,SAAS,GAAG;AAChD,QAAI,KAAK,YAAY,KAAK,MAAM,GAAG,YAAY,KAAK,EAAG;AACvD,QAAI,KAAK,oBAAoB,KAAK,MAAM,GAAG,oBAAoB,KAAK,EAAG;AACvE,QAAI,QAAQ,oBAAoB,MAAO;AACvC,QACE,QAAQ,oBAAoB,SAC3B,KAAK,iBAAiB,KAAK,MAAM,QAAQ,GAAG,iBAAiB,KAAK,MAAM,OACzE;AACA;AAAA,IACF;AACA,aAAS;AAAA,EACX;AACA,SAAO;AACT;","names":[]}
|
|
@@ -1,2 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Layout preservation depth — the single production rule for how many
|
|
3
|
+
* leading layouts a navigation transition keeps mounted.
|
|
4
|
+
*
|
|
5
|
+
* One implementation, two callers: the renderer's NavigationShell (real
|
|
6
|
+
* component transitions) and the Node frontend application slice
|
|
7
|
+
* (`createFrontendSurface`). They must agree — a slice test that preserved
|
|
8
|
+
* a layout the renderer rebuilds, or vice versa, would be evidence about a
|
|
9
|
+
* rule that does not exist in production.
|
|
10
|
+
*
|
|
11
|
+
* The rule: walk the layout stacks in parallel and keep a layout only while
|
|
12
|
+
* BOTH sides declare the same layout path, the same layout controller, and
|
|
13
|
+
* both mark it `preserve: true`. `NavigationOptions.preserveLayouts`
|
|
14
|
+
* overrides the declaration in both directions.
|
|
15
|
+
*/
|
|
16
|
+
interface NavigationOptions {
|
|
17
|
+
preserveLayouts?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** The layout stack of one view, in the shape both callers can supply. */
|
|
20
|
+
interface LayoutStack {
|
|
21
|
+
layoutPaths: readonly string[];
|
|
22
|
+
layoutControllers?: ReadonlyArray<string | undefined>;
|
|
23
|
+
layoutPreserve?: readonly boolean[];
|
|
24
|
+
}
|
|
25
|
+
declare function computePreservedLayoutDepth(from: LayoutStack, to: LayoutStack, options?: NavigationOptions): number;
|
|
26
|
+
|
|
27
|
+
export { type LayoutStack, type NavigationOptions, computePreservedLayoutDepth };
|
|
@@ -1,2 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Layout preservation depth — the single production rule for how many
|
|
3
|
+
* leading layouts a navigation transition keeps mounted.
|
|
4
|
+
*
|
|
5
|
+
* One implementation, two callers: the renderer's NavigationShell (real
|
|
6
|
+
* component transitions) and the Node frontend application slice
|
|
7
|
+
* (`createFrontendSurface`). They must agree — a slice test that preserved
|
|
8
|
+
* a layout the renderer rebuilds, or vice versa, would be evidence about a
|
|
9
|
+
* rule that does not exist in production.
|
|
10
|
+
*
|
|
11
|
+
* The rule: walk the layout stacks in parallel and keep a layout only while
|
|
12
|
+
* BOTH sides declare the same layout path, the same layout controller, and
|
|
13
|
+
* both mark it `preserve: true`. `NavigationOptions.preserveLayouts`
|
|
14
|
+
* overrides the declaration in both directions.
|
|
15
|
+
*/
|
|
16
|
+
interface NavigationOptions {
|
|
17
|
+
preserveLayouts?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** The layout stack of one view, in the shape both callers can supply. */
|
|
20
|
+
interface LayoutStack {
|
|
21
|
+
layoutPaths: readonly string[];
|
|
22
|
+
layoutControllers?: ReadonlyArray<string | undefined>;
|
|
23
|
+
layoutPreserve?: readonly boolean[];
|
|
24
|
+
}
|
|
25
|
+
declare function computePreservedLayoutDepth(from: LayoutStack, to: LayoutStack, options?: NavigationOptions): number;
|
|
26
|
+
|
|
27
|
+
export { type LayoutStack, type NavigationOptions, computePreservedLayoutDepth };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/navigation/layout_preservation.ts"],"sourcesContent":["/**\n * Layout preservation depth — the single production rule for how many\n * leading layouts a navigation transition keeps mounted.\n *\n * One implementation, two callers: the renderer's NavigationShell (real\n * component transitions) and the Node frontend application slice\n * (`createFrontendSurface`). They must agree — a slice test that preserved\n * a layout the renderer rebuilds, or vice versa, would be evidence about a\n * rule that does not exist in production.\n *\n * The rule: walk the layout stacks in parallel and keep a layout only while\n * BOTH sides declare the same layout path, the same layout controller, and\n * both mark it `preserve: true`. `NavigationOptions.preserveLayouts`\n * overrides the declaration in both directions.\n */\
|
|
1
|
+
{"version":3,"sources":["../../src/navigation/layout_preservation.ts"],"sourcesContent":["/**\n * Layout preservation depth — the single production rule for how many\n * leading layouts a navigation transition keeps mounted.\n *\n * One implementation, two callers: the renderer's NavigationShell (real\n * component transitions) and the Node frontend application slice\n * (`createFrontendSurface`). They must agree — a slice test that preserved\n * a layout the renderer rebuilds, or vice versa, would be evidence about a\n * rule that does not exist in production.\n *\n * The rule: walk the layout stacks in parallel and keep a layout only while\n * BOTH sides declare the same layout path, the same layout controller, and\n * both mark it `preserve: true`. `NavigationOptions.preserveLayouts`\n * overrides the declaration in both directions.\n */\nexport interface NavigationOptions {\n preserveLayouts?: boolean;\n}\n\n/** The layout stack of one view, in the shape both callers can supply. */\nexport interface LayoutStack {\n layoutPaths: readonly string[];\n layoutControllers?: ReadonlyArray<string | undefined>;\n layoutPreserve?: readonly boolean[];\n}\n\nexport function computePreservedLayoutDepth(\n from: LayoutStack,\n to: LayoutStack,\n options: NavigationOptions = {},\n): number {\n const maxDepth = Math.min(from.layoutPaths.length, to.layoutPaths.length);\n let depth = 0;\n for (let index = 0; index < maxDepth; index += 1) {\n if (from.layoutPaths[index] !== to.layoutPaths[index]) break;\n if (from.layoutControllers?.[index] !== to.layoutControllers?.[index]) break;\n if (options.preserveLayouts === false) break;\n if (\n options.preserveLayouts !== true &&\n (from.layoutPreserve?.[index] !== true || to.layoutPreserve?.[index] !== true)\n ) {\n break;\n }\n depth += 1;\n }\n return depth;\n}\n"],"mappings":"AA0BO,SAAS,4BACd,MACA,IACA,UAA6B,CAAC,GACtB;AACR,QAAM,WAAW,KAAK,IAAI,KAAK,YAAY,QAAQ,GAAG,YAAY,MAAM;AACxE,MAAI,QAAQ;AACZ,WAAS,QAAQ,GAAG,QAAQ,UAAU,SAAS,GAAG;AAChD,QAAI,KAAK,YAAY,KAAK,MAAM,GAAG,YAAY,KAAK,EAAG;AACvD,QAAI,KAAK,oBAAoB,KAAK,MAAM,GAAG,oBAAoB,KAAK,EAAG;AACvE,QAAI,QAAQ,oBAAoB,MAAO;AACvC,QACE,QAAQ,oBAAoB,SAC3B,KAAK,iBAAiB,KAAK,MAAM,QAAQ,GAAG,iBAAiB,KAAK,MAAM,OACzE;AACA;AAAA,IACF;AACA,aAAS;AAAA,EACX;AACA,SAAO;AACT;","names":[]}
|
|
@@ -2,6 +2,7 @@ import { Navigation } from '../navigation/index.cjs';
|
|
|
2
2
|
import { TraceRecorder } from '../trace-testing/trace_recorder.cjs';
|
|
3
3
|
import { RouterBridgeResult } from './create_router_bridge.cjs';
|
|
4
4
|
import { createTestRouter } from './create_test_router.cjs';
|
|
5
|
+
import '../navigation/layout_preservation.cjs';
|
|
5
6
|
import '../navigation/automation_introspection.cjs';
|
|
6
7
|
import '@noego/trace/testing';
|
|
7
8
|
import '../trace_hub-Cpo_nVze.cjs';
|
|
@@ -2,6 +2,7 @@ import { Navigation } from '../navigation/index.js';
|
|
|
2
2
|
import { TraceRecorder } from '../trace-testing/trace_recorder.js';
|
|
3
3
|
import { RouterBridgeResult } from './create_router_bridge.js';
|
|
4
4
|
import { createTestRouter } from './create_test_router.js';
|
|
5
|
+
import '../navigation/layout_preservation.js';
|
|
5
6
|
import '../navigation/automation_introspection.js';
|
|
6
7
|
import '@noego/trace/testing';
|
|
7
8
|
import '../trace_hub-Cpo_nVze.js';
|
package/dist/testing/index.d.cts
CHANGED
|
@@ -37,6 +37,7 @@ import '../types/config.cjs';
|
|
|
37
37
|
import '../types/views.cjs';
|
|
38
38
|
import './wood_dom_animation.cjs';
|
|
39
39
|
import '../navigation/index.cjs';
|
|
40
|
+
import '../navigation/layout_preservation.cjs';
|
|
40
41
|
import '../navigation/automation_introspection.cjs';
|
|
41
42
|
import 'playwright';
|
|
42
43
|
import '@noego/trace/testing';
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ import '../types/config.js';
|
|
|
37
37
|
import '../types/views.js';
|
|
38
38
|
import './wood_dom_animation.js';
|
|
39
39
|
import '../navigation/index.js';
|
|
40
|
+
import '../navigation/layout_preservation.js';
|
|
40
41
|
import '../navigation/automation_introspection.js';
|
|
41
42
|
import 'playwright';
|
|
42
43
|
import '@noego/trace/testing';
|
|
@@ -14,6 +14,7 @@ import '../validation/schema_validator.cjs';
|
|
|
14
14
|
import '../middleware/middleware_resolver.cjs';
|
|
15
15
|
import '../controller/controller_resolver.cjs';
|
|
16
16
|
import '../navigation/index.cjs';
|
|
17
|
+
import '../navigation/layout_preservation.cjs';
|
|
17
18
|
import '../navigation/automation_introspection.cjs';
|
|
18
19
|
import '../types/views.cjs';
|
|
19
20
|
import '../frontend/page_catalog.cjs';
|
|
@@ -14,6 +14,7 @@ import '../validation/schema_validator.js';
|
|
|
14
14
|
import '../middleware/middleware_resolver.js';
|
|
15
15
|
import '../controller/controller_resolver.js';
|
|
16
16
|
import '../navigation/index.js';
|
|
17
|
+
import '../navigation/layout_preservation.js';
|
|
17
18
|
import '../navigation/automation_introspection.js';
|
|
18
19
|
import '../types/views.js';
|
|
19
20
|
import '../frontend/page_catalog.js';
|
|
@@ -17,6 +17,7 @@ import '../validation/schema_validator.cjs';
|
|
|
17
17
|
import '../middleware/middleware_resolver.cjs';
|
|
18
18
|
import '../controller/controller_resolver.cjs';
|
|
19
19
|
import '../navigation/index.cjs';
|
|
20
|
+
import '../navigation/layout_preservation.cjs';
|
|
20
21
|
import '../navigation/automation_introspection.cjs';
|
|
21
22
|
import '../frontend/frontend_lifecycle.cjs';
|
|
22
23
|
import '../types/config.cjs';
|
|
@@ -17,6 +17,7 @@ import '../validation/schema_validator.js';
|
|
|
17
17
|
import '../middleware/middleware_resolver.js';
|
|
18
18
|
import '../controller/controller_resolver.js';
|
|
19
19
|
import '../navigation/index.js';
|
|
20
|
+
import '../navigation/layout_preservation.js';
|
|
20
21
|
import '../navigation/automation_introspection.js';
|
|
21
22
|
import '../frontend/frontend_lifecycle.js';
|
|
22
23
|
import '../types/config.js';
|
package/package.json
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* both mark it `preserve: true`. `NavigationOptions.preserveLayouts`
|
|
14
14
|
* overrides the declaration in both directions.
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
export interface NavigationOptions {
|
|
17
|
+
preserveLayouts?: boolean;
|
|
18
|
+
}
|
|
17
19
|
|
|
18
20
|
/** The layout stack of one view, in the shape both callers can supply. */
|
|
19
21
|
export interface LayoutStack {
|