@noego/wood 0.7.2 → 0.7.4

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 (37) hide show
  1. package/bin/wood.js +9 -1
  2. package/dist/codegen/renderer_app_generator.cjs +14 -13
  3. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  4. package/dist/codegen/renderer_app_generator.js +14 -13
  5. package/dist/codegen/renderer_app_generator.js.map +1 -1
  6. package/dist/navigation/index.cjs +14 -0
  7. package/dist/navigation/index.cjs.map +1 -1
  8. package/dist/navigation/index.d.cts +3 -1
  9. package/dist/navigation/index.d.ts +3 -1
  10. package/dist/navigation/index.js +13 -0
  11. package/dist/navigation/index.js.map +1 -1
  12. package/dist/testing/browser.cjs +11 -1
  13. package/dist/testing/browser.cjs.map +1 -1
  14. package/dist/testing/browser.d.cts +7 -1
  15. package/dist/testing/browser.d.ts +7 -1
  16. package/dist/testing/browser.js +10 -1
  17. package/dist/testing/browser.js.map +1 -1
  18. package/dist/testing/native_browser/happy_dom_tab.cjs +3 -2
  19. package/dist/testing/native_browser/happy_dom_tab.cjs.map +1 -1
  20. package/dist/testing/native_browser/happy_dom_tab.js +3 -2
  21. package/dist/testing/native_browser/happy_dom_tab.js.map +1 -1
  22. package/dist/testing/native_browser/host_transport.cjs +18 -8
  23. package/dist/testing/native_browser/host_transport.cjs.map +1 -1
  24. package/dist/testing/native_browser/host_transport.d.cts +3 -3
  25. package/dist/testing/native_browser/host_transport.d.ts +3 -3
  26. package/dist/testing/native_browser/host_transport.js +18 -8
  27. package/dist/testing/native_browser/host_transport.js.map +1 -1
  28. package/dist/testing/native_browser/playwright_tab.cjs +4 -6
  29. package/dist/testing/native_browser/playwright_tab.cjs.map +1 -1
  30. package/dist/testing/native_browser/playwright_tab.js +4 -6
  31. package/dist/testing/native_browser/playwright_tab.js.map +1 -1
  32. package/dist/testing/native_browser/renderer_entry.cjs +5 -2
  33. package/dist/testing/native_browser/renderer_entry.cjs.map +1 -1
  34. package/dist/testing/native_browser/renderer_entry.js +5 -2
  35. package/dist/testing/native_browser/renderer_entry.js.map +1 -1
  36. package/docs/native-browser-testing.md +11 -0
  37. package/package.json +2 -2
package/bin/wood.js CHANGED
@@ -27,6 +27,7 @@ const {
27
27
  generateTypes,
28
28
  generateRuntime,
29
29
  generateRendererApp,
30
+ resolveRendererTraceBatching,
30
31
  } = await import(distUrl('index.js'));
31
32
  const {
32
33
  resolveWoodRemoteDebuggingPort,
@@ -325,7 +326,14 @@ function buildCodegenArtifacts(allOperations, allViews) {
325
326
  const windowConfigSnippet = generateWindowConfigSnippet(config.windows);
326
327
  const runtimeWithWindowConfigCode = `${runtimeCode}\n\n${windowConfigSnippet}`;
327
328
 
328
- const preloadCode = generatePreload(allOperations);
329
+ // `observability.trace.rendererToMain.batching` must reach the generated
330
+ // preload here exactly as WoodDevServer passes it; without it the CLI
331
+ // codegen (dev/build/codegen) silently emitted the 256/32 defaults and the
332
+ // consumer's config was dead (demo-assistant, 2026-09-19: 28k renderer
333
+ // trace events dropped in one session).
334
+ const preloadCode = generatePreload(allOperations, {
335
+ traceBatching: resolveRendererTraceBatching(config),
336
+ });
329
337
  const typesCode = generateTypes(allOperations);
330
338
 
331
339
  const rendererComponentDir = path.resolve(projectRoot, rendererSection.componentDir);
@@ -240,7 +240,7 @@ function generateRendererApp(views, options) {
240
240
  return `${(0, import_generated_file_notice.generatedFileNotice)(RENDERER_APP_SOURCE_INSTRUCTIONS)}
241
241
  import { mount } from 'svelte';
242
242
  import { App, getApp, getContainer } from '@noego/wood/client';
243
- import { Navigation, getNavigation, installAutomationIntrospection } from '@noego/wood/navigation';
243
+ import { Navigation, getNavigation, readNavigationHistory, installAutomationIntrospection } from '@noego/wood/navigation';
244
244
  import { createLoaderRequest, initializeArgument, initializeController, runViewLoaders } from '@noego/wood/navigation';
245
245
  import { Ipc, getIpc } from '@noego/wood/ipc';
246
246
  import NavigationShell from '@noego/wood/navigation-shell';
@@ -459,7 +459,7 @@ function getWoodBridge(): WoodBootstrapBridgeLike | null {
459
459
  return bridge as WoodBootstrapBridgeLike;
460
460
  }
461
461
 
462
- async function resolveInitialEntry(): Promise<GeneratedAppViewEntry> {
462
+ async function resolveInitialEntry(route: RouteInput): Promise<{ entry: GeneratedAppViewEntry; route: RouteInput }> {
463
463
  if (!DEFAULT_VIEW_KEY) {
464
464
  throw new Error('No views available in renderer manifest. Define at least one view in renderer OpenAPI YAML.');
465
465
  }
@@ -468,19 +468,20 @@ async function resolveInitialEntry(): Promise<GeneratedAppViewEntry> {
468
468
  const bootstrapWindow = await bridge?.__window?.current?.();
469
469
  const requestedRoute = bootstrapWindow?.defaultRoute;
470
470
 
471
- if (requestedRoute) {
472
- const routeEntry = getViewManifestEntry(requestedRoute);
473
- if (routeEntry) {
474
- return routeEntry;
475
- }
476
- }
477
-
478
- const entry = getViewManifestEntry(DEFAULT_VIEW_KEY);
471
+ const routeEntry = requestedRoute ? getViewManifestEntry(requestedRoute) : null;
472
+ const entry = routeEntry ?? getViewManifestEntry(DEFAULT_VIEW_KEY);
479
473
  if (!entry) {
480
474
  throw new Error(\`Default view "\${DEFAULT_VIEW_KEY}" is missing from generated view manifest.\`);
481
475
  }
482
476
 
483
- return entry;
477
+ // Reload restores only a valid manifest route owned by this window. Invalid
478
+ // or foreign history falls back to the host's original bootstrap unchanged.
479
+ const saved = readNavigationHistory(typeof window === 'undefined' ? null : window.history.state);
480
+ const savedEntry = saved ? getViewManifestEntry(saved.page) : null;
481
+ if (saved && savedEntry && savedEntry.windowName === (bootstrapWindow?.windowId ?? entry.windowName)) {
482
+ return {entry: savedEntry, route: {params: saved.params, query: saved.query}};
483
+ }
484
+ return {entry, route};
484
485
  }
485
486
 
486
487
  function isAutomationBuild(): boolean {
@@ -493,8 +494,8 @@ function isAutomationBuild(): boolean {
493
494
  }
494
495
 
495
496
  export const supportsInitialRoute = true;
496
- export async function app(target: Element, route: RouteInput = { params: {}, query: {} }) {
497
- const entry = await resolveInitialEntry();
497
+ export async function app(target: Element, initialRoute: RouteInput = { params: {}, query: {} }) {
498
+ const {entry, route} = await resolveInitialEntry(initialRoute);
498
499
 
499
500
  getNavigation().replace(entry.key, route.params, route.query);
500
501
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/codegen/renderer_app_generator.ts"],"sourcesContent":["import { existsSync } from 'fs';\nimport path from 'path';\nimport type { ViewDef } from '../types/index.cjs';\nimport { loaderCompanionCandidates } from '../loader/loader_orchestration.cjs';\nimport { generatedFileNotice } from './generated_file_notice.cjs';\n\nconst RENDERER_APP_SOURCE_INSTRUCTIONS = [\n 'To change routes, page components, layouts, or renderer controllers, edit the views YAML',\n 'configured by wood.config.yml renderer.openapi and the matching UI source files.',\n] as const;\n\nexport interface RendererAppGeneratorOptions {\n /** Absolute path to renderer component root (e.g. /app/ui) */\n componentDir: string;\n /** Absolute output directory where generated file will be written */\n outDir: string;\n /** Optional explicit default view key (e.g. \"main.chat\") */\n entryViewKey?: string;\n}\n\nfunction computeImportPath(fromDir: string, toFile: string): string {\n let rel = path.relative(fromDir, toFile);\n\n if (!rel.startsWith('.')) {\n rel = './' + rel;\n }\n\n return rel.replace(/\\\\/g, '/');\n}\n\nfunction resolveFromComponentDir(componentDir: string, fileRef: string): string {\n if (path.isAbsolute(fileRef)) {\n return fileRef;\n }\n\n const normalizedRef = fileRef.replace(/\\\\/g, '/');\n const componentDirName = path.basename(componentDir).replace(/\\\\/g, '/');\n\n if (normalizedRef.startsWith(componentDirName + '/')) {\n const stripped = normalizedRef.slice(componentDirName.length + 1);\n return path.resolve(componentDir, stripped);\n }\n\n return path.resolve(componentDir, normalizedRef);\n}\n\nfunction viewKey(view: ViewDef): string {\n return `${view.windowName}.${view.pageName}`;\n}\n\nfunction toIdentifier(prefix: string, value: string): string {\n const core = value\n .replace(/[^a-zA-Z0-9]+/g, ' ')\n .trim()\n .split(/\\s+/)\n .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))\n .join('');\n\n return `${prefix}${core || 'Component'}`;\n}\n\nfunction escapeSingleQuoted(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\nconst CONTROLLER_FILE_SUFFIXES = [\n '.ts',\n '.js',\n '.controller.ts',\n '.controller.js',\n] as const;\n\nconst CONTROLLER_INDEX_CANDIDATES = [\n 'index.ts',\n 'index.js',\n] as const;\n\nfunction resolveControllerFile(controllerRef: string, componentDir: string): string | null {\n const normalized = controllerRef.trim();\n const basePath = resolveFromComponentDir(componentDir, normalized);\n\n if (existsSync(basePath)) {\n return basePath;\n }\n\n if (path.extname(basePath)) {\n return null;\n }\n\n for (const suffix of CONTROLLER_FILE_SUFFIXES) {\n const candidate = basePath + suffix;\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n for (const indexFile of CONTROLLER_INDEX_CANDIDATES) {\n const candidate = path.resolve(basePath, indexFile);\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n/**\n * The companion data loader of a view/layout (`pages/chat.svelte` →\n * `pages/chat.load.ts`), discovered at build. None is fine (`null`); more\n * than one companion is ambiguous and fails the build.\n */\nfunction resolveLoaderFile(viewPath: string, componentDir: string): string | null {\n const present = loaderCompanionCandidates(viewPath)\n .map((candidate) => resolveFromComponentDir(componentDir, candidate))\n .filter((candidate) => existsSync(candidate));\n if (present.length > 1) {\n throw new Error(\n `Renderer app codegen failed: ambiguous data loader for \"${viewPath}\": ${present.join(', ')}. ` +\n 'Keep exactly one .load companion.',\n );\n }\n return present[0] ?? null;\n}\n\nfunction resolveDefaultViewKey(sortedViews: ViewDef[], entryViewKey?: string): string {\n if (sortedViews.length === 0) return '';\n\n const configuredEntry = entryViewKey?.trim();\n if (!configuredEntry) {\n return viewKey(sortedViews[0]);\n }\n\n const availableKeys = sortedViews.map((view) => viewKey(view));\n if (availableKeys.includes(configuredEntry)) {\n return configuredEntry;\n }\n\n throw new Error(\n `Renderer entry \"${configuredEntry}\" was not found in parsed views. Available view keys: ${availableKeys.join(', ')}`,\n );\n}\n\nexport function generateRendererApp(\n views: ViewDef[],\n options: RendererAppGeneratorOptions,\n): string {\n const sortedViews = [...views].sort((a, b) => viewKey(a).localeCompare(viewKey(b)));\n const defaultViewKey = resolveDefaultViewKey(sortedViews, options.entryViewKey);\n\n const importLines: string[] = [];\n const viewIdentifiers = new Map<string, string>();\n const layoutIdentifiers = new Map<string, string>();\n const controllerIdentifiers = new Map<string, { identifier: string; importPath: string }>();\n /** view/layout path → generated loader identifier (only when a companion exists). */\n const loaderIdentifiers = new Map<string, string>();\n const usedNames = new Set<string>();\n\n const allocateName = (prefix: string, filePath: string): string => {\n const base = toIdentifier(prefix, filePath);\n let name = base;\n let suffix = 2;\n while (usedNames.has(name)) {\n name = `${base}${suffix}`;\n suffix += 1;\n }\n usedNames.add(name);\n return name;\n };\n\n const ensureControllerImported = (controllerRef: string, owningViewKey: string): void => {\n if (controllerIdentifiers.has(controllerRef)) return;\n\n const resolvedControllerFile = resolveControllerFile(controllerRef, options.componentDir);\n if (!resolvedControllerFile) {\n throw new Error(\n `Renderer app codegen failed: controller \"${controllerRef}\" for view \"${owningViewKey}\" was not found. ` +\n 'Controller must point to an existing file with a default exported class.',\n );\n }\n\n const identifier = allocateName('PageController', controllerRef);\n const importPath = computeImportPath(options.outDir, resolvedControllerFile);\n controllerIdentifiers.set(controllerRef, { identifier, importPath });\n importLines.push(`import ${identifier} from '${importPath}';`);\n };\n\n // Build discovers and IMPORTS companion loaders; none runs until a page\n // opens (createControllerSet executes them lazily per navigation).\n const ensureLoaderImported = (viewPath: string): void => {\n if (loaderIdentifiers.has(viewPath)) return;\n const loaderFile = resolveLoaderFile(viewPath, options.componentDir);\n if (!loaderFile) return;\n const identifier = allocateName('Loader', viewPath);\n loaderIdentifiers.set(viewPath, identifier);\n importLines.push(`import ${identifier} from '${computeImportPath(options.outDir, loaderFile)}';`);\n };\n\n for (const view of sortedViews) {\n if (!viewIdentifiers.has(view.viewPath)) {\n const identifier = allocateName('View', view.viewPath);\n viewIdentifiers.set(view.viewPath, identifier);\n const abs = resolveFromComponentDir(options.componentDir, view.viewPath);\n const importPath = computeImportPath(options.outDir, abs);\n importLines.push(`import ${identifier} from '${importPath}';`);\n }\n ensureLoaderImported(view.viewPath);\n\n for (const layoutPath of view.layouts) {\n ensureLoaderImported(layoutPath);\n if (layoutIdentifiers.has(layoutPath)) continue;\n const identifier = allocateName('Layout', layoutPath);\n layoutIdentifiers.set(layoutPath, identifier);\n const abs = resolveFromComponentDir(options.componentDir, layoutPath);\n const importPath = computeImportPath(options.outDir, abs);\n importLines.push(`import ${identifier} from '${importPath}';`);\n }\n\n if (view.controller) {\n ensureControllerImported(view.controller, viewKey(view));\n }\n\n for (const layoutController of view.layoutControllers ?? []) {\n if (!layoutController) continue;\n ensureControllerImported(layoutController, viewKey(view));\n }\n }\n\n const manifestEntries = sortedViews.map((view) => {\n const key = viewKey(view);\n const viewIdentifier = viewIdentifiers.get(view.viewPath)!;\n const layoutList = view.layouts.map((layoutPath) => layoutIdentifiers.get(layoutPath)!);\n const layoutLoaders = view.layouts.map((layoutPath) => loaderIdentifiers.get(layoutPath) ?? 'null');\n const viewLoader = loaderIdentifiers.get(view.viewPath) ?? 'null';\n const controllerPath = view.controller ? `'${escapeSingleQuoted(view.controller)}'` : 'undefined';\n const controllerClass = view.controller\n ? controllerIdentifiers.get(view.controller)!.identifier\n : 'undefined';\n const layoutControllerRefs = view.layoutControllers;\n if (layoutControllerRefs && layoutControllerRefs.length !== view.layouts.length) {\n throw new Error(\n `Renderer app codegen failed: layout controller count for view \"${key}\" does not match layout count.`,\n );\n }\n const normalizedLayoutControllerRefs = layoutControllerRefs ?? Array.from(\n { length: view.layouts.length },\n () => undefined,\n );\n const layoutControllerPaths = `[${normalizedLayoutControllerRefs\n .map((controllerRef) => (controllerRef ? `'${escapeSingleQuoted(controllerRef)}'` : 'undefined'))\n .join(', ')}]`;\n const layoutControllerClasses = `[${normalizedLayoutControllerRefs\n .map((controllerRef) => (controllerRef ? controllerIdentifiers.get(controllerRef)!.identifier : 'undefined'))\n .join(', ')}]`;\n const layoutPreserve = view.layoutPreserve ?? Array.from(\n { length: view.layouts.length },\n () => false,\n );\n if (layoutPreserve.length !== view.layouts.length) {\n throw new Error(\n `Renderer app codegen failed: layout preserve count for view \"${key}\" does not match layout count.`,\n );\n }\n\n return ` '${escapeSingleQuoted(key)}': {\n key: '${escapeSingleQuoted(key)}',\n windowName: '${escapeSingleQuoted(view.windowName)}',\n pageName: '${escapeSingleQuoted(view.pageName)}',\n viewPath: '${escapeSingleQuoted(view.viewPath)}',\n layoutPaths: ${JSON.stringify(view.layouts)},\n layoutControllers: ${layoutControllerPaths},\n layoutPreserve: ${JSON.stringify(layoutPreserve)},\n controller: ${controllerPath},\n layoutControllerClasses: ${layoutControllerClasses},\n controllerClass: ${controllerClass},\n view: ${viewIdentifier},\n layouts: [${layoutList.join(', ')}],\n viewLoader: ${viewLoader},\n layoutLoaders: [${layoutLoaders.join(', ')}],\n },`;\n });\n\n return `${generatedFileNotice(RENDERER_APP_SOURCE_INSTRUCTIONS)}\nimport { mount } from 'svelte';\nimport { App, getApp, getContainer } from '@noego/wood/client';\nimport { Navigation, getNavigation, installAutomationIntrospection } from '@noego/wood/navigation';\nimport { createLoaderRequest, initializeArgument, initializeController, runViewLoaders } from '@noego/wood/navigation';\nimport { Ipc, getIpc } from '@noego/wood/ipc';\nimport NavigationShell from '@noego/wood/navigation-shell';\n${importLines.join('\\n')}\n\ninterface GeneratedAppViewEntry {\n key: string;\n windowName: string;\n pageName: string;\n viewPath: string;\n layoutPaths: string[];\n layoutControllers: Array<string | undefined>;\n layoutPreserve: boolean[];\n controller?: string;\n layoutControllerClasses: Array<any | undefined>;\n controllerClass?: any;\n view: any;\n layouts: any[];\n /** Companion data loader of the view (\\`pages/x.svelte\\` → \\`pages/x.load.ts\\`), discovered at build. */\n viewLoader: ((request: any) => unknown) | null;\n /** Index-aligned companion loaders of the layout stack. */\n layoutLoaders: Array<((request: any) => unknown) | null>;\n}\n\ntype RouteInput = { params: Record<string, string>; query: Record<string, string> };\n\nconst VIEW_MANIFEST: Record<string, GeneratedAppViewEntry> = {\n${manifestEntries.join('\\n')}\n};\n\nfunction getViewManifestEntry(viewKey: string): GeneratedAppViewEntry | null {\n return VIEW_MANIFEST[viewKey] ?? null;\n}\n\ninterface WoodScopedContainer {\n get(token: any): unknown | Promise<unknown>;\n registerClass?(token: any): void;\n registerFunction?(\n token: any,\n resolver: (...args: any[]) => unknown,\n options?: Record<string, unknown>,\n ): void;\n}\n\ninterface WoodContainer extends WoodScopedContainer {\n extend?(): WoodScopedContainer;\n}\n\nfunction createScopedContainer(): WoodScopedContainer {\n const container = getContainer() as WoodContainer;\n if (typeof container.extend !== 'function') {\n throw new Error('Wood client container does not support extend().');\n }\n\n const scopedContainer = container.extend();\n\n if (!scopedContainer || typeof scopedContainer.get !== 'function') {\n throw new Error('Wood client container does not support get().');\n }\n\n // Renderer builtins belong to the root created by getContainer().\n // Page scopes inherit them; re-registering resolved ancestor singletons is invalid.\n\n return scopedContainer as WoodScopedContainer;\n}\n\nasync function resolveControllerInScope(\n scope: WoodScopedContainer,\n entry: GeneratedAppViewEntry,\n ControllerClass: any,\n contextLabel: string,\n): Promise<unknown> {\n if (typeof ControllerClass !== 'function') {\n throw new Error(\n \\`Controller for \\${contextLabel} in view \"\\${entry.key}\" is invalid. Expected a default exported class.\\`,\n );\n }\n\n if (typeof scope.registerClass === 'function') {\n scope.registerClass(ControllerClass);\n }\n\n return await scope.get(ControllerClass);\n}\n\ntype RollbackStep = () => unknown | Promise<unknown>;\n\nasync function disposeScope(scope: { dispose?: () => Promise<void> } | null): Promise<void> {\n if (!scope || typeof scope.dispose !== 'function') return;\n await scope.dispose();\n}\n\n/**\n * Roll back after \\`primary\\` failed: every step is attempted exactly once and\n * every cleanup failure is retained as its original error object next to the\n * primary failure (\\`errors\\`, with \\`cause\\` = primary). Never swallows.\n */\nasync function failWithRollback(primary: unknown, steps: RollbackStep[], message: string): Promise<never> {\n const errors: unknown[] = [primary];\n for (const step of steps) {\n try { await step(); } catch (error) { errors.push(error); }\n }\n if (errors.length === 1) throw primary;\n throw Object.assign(new Error(message), { errors, cause: primary });\n}\n\n/**\n * One page scope per view lifecycle: the view controller and every\n * non-preservable layout controller resolve from the SAME scope, so Scoped\n * services/state are shared within the page (the NoEgo renderer model).\n * Layouts marked preservable in the manifest own dedicated scopes that\n * survive page transitions and dispose when the layout finally leaves.\n *\n * Data loaders (shared orchestration) run first for this navigation —\n * non-preserved layouts in stack order, then the view — and each\n * controller's optional \\`initialize({ params, query[, data] })\\` receives\n * its loader's data. A loader failure disposes the scopes created here.\n */\nasync function createControllerSet(\n entry: GeneratedAppViewEntry,\n preservedDepth = 0,\n route: RouteInput = { params: {}, query: {} },\n): Promise<{\n controller: unknown;\n layoutControllers: Array<unknown>;\n pageScope: WoodScopedContainer;\n layoutScopes: Array<WoodScopedContainer | null>;\n}> {\n const pageScope = createScopedContainer();\n const layoutControllers: unknown[] = [];\n const layoutScopes: Array<WoodScopedContainer | null> = [];\n const createdLayoutScopes: WoodScopedContainer[] = [];\n\n try {\n const loaded = await runViewLoaders(\n { layouts: entry.layoutLoaders, view: entry.viewLoader },\n createLoaderRequest(entry.key, route),\n preservedDepth,\n );\n\n for (let i = 0; i < preservedDepth; i += 1) {\n layoutControllers.push(undefined);\n layoutScopes.push(null);\n }\n\n for (let i = preservedDepth; i < entry.layoutControllerClasses.length; i += 1) {\n const LayoutController = entry.layoutControllerClasses[i];\n if (!LayoutController) {\n layoutControllers.push(undefined);\n layoutScopes.push(null);\n continue;\n }\n const preservable = entry.layoutPreserve?.[i] === true;\n let scope = pageScope;\n if (preservable) {\n scope = createScopedContainer();\n createdLayoutScopes.push(scope);\n }\n const controller = await resolveControllerInScope(\n scope,\n entry,\n LayoutController,\n \\`layout[\\${i}]\\`,\n );\n await initializeController(controller, initializeArgument(route, loaded.layouts[i] ?? null));\n layoutControllers.push(controller);\n layoutScopes.push(preservable ? scope : null);\n }\n\n const controller = entry.controllerClass\n ? await resolveControllerInScope(pageScope, entry, entry.controllerClass, 'page')\n : undefined;\n if (controller !== undefined) {\n await initializeController(controller, initializeArgument(route, loaded.view));\n }\n\n return { controller, layoutControllers, pageScope, layoutScopes };\n } catch (error) {\n // Failed transitions must not orphan scopes: every scope this set created\n // is disposed once, and every disposal failure is retained with the\n // original initialization failure instead of being swallowed.\n return failWithRollback(error, [\n ...createdLayoutScopes.map((scope) => () => disposeScope(scope as any)),\n () => disposeScope(pageScope as any),\n ], 'Wood controller-set rollback failed');\n }\n}\n\nconst DEFAULT_VIEW_KEY = '${escapeSingleQuoted(defaultViewKey)}';\n\ntype WoodWindowBootstrapInfo = {\n ok?: boolean;\n windowId?: string;\n defaultRoute?: string;\n};\n\ntype WoodBootstrapBridgeLike = {\n __window?: {\n current?: () => Promise<WoodWindowBootstrapInfo>;\n };\n};\n\nfunction getWoodBridge(): WoodBootstrapBridgeLike | null {\n const globalObj = globalThis as typeof globalThis & {\n __NOEGO_WOOD_BRIDGE__?: unknown;\n window?: {\n __NOEGO_WOOD_BRIDGE__?: unknown;\n };\n };\n\n const bridge = globalObj.__NOEGO_WOOD_BRIDGE__ ?? globalObj.window?.__NOEGO_WOOD_BRIDGE__;\n if (!bridge || typeof bridge !== 'object') {\n return null;\n }\n\n return bridge as WoodBootstrapBridgeLike;\n}\n\nasync function resolveInitialEntry(): Promise<GeneratedAppViewEntry> {\n if (!DEFAULT_VIEW_KEY) {\n throw new Error('No views available in renderer manifest. Define at least one view in renderer OpenAPI YAML.');\n }\n\n const bridge = getWoodBridge();\n const bootstrapWindow = await bridge?.__window?.current?.();\n const requestedRoute = bootstrapWindow?.defaultRoute;\n\n if (requestedRoute) {\n const routeEntry = getViewManifestEntry(requestedRoute);\n if (routeEntry) {\n return routeEntry;\n }\n }\n\n const entry = getViewManifestEntry(DEFAULT_VIEW_KEY);\n if (!entry) {\n throw new Error(\\`Default view \"\\${DEFAULT_VIEW_KEY}\" is missing from generated view manifest.\\`);\n }\n\n return entry;\n}\n\nfunction isAutomationBuild(): boolean {\n try {\n const env = (import.meta as { env?: { DEV?: boolean; MODE?: string } }).env;\n return Boolean(env?.DEV) || env?.MODE === 'test';\n } catch {\n return false;\n }\n}\n\nexport const supportsInitialRoute = true;\nexport async function app(target: Element, route: RouteInput = { params: {}, query: {} }) {\n const entry = await resolveInitialEntry();\n\n getNavigation().replace(entry.key, route.params, route.query);\n\n // Dev/test-only Axe introspection surface; never present in production builds.\n if (isAutomationBuild()) {\n const bootstrapWindow = await getWoodBridge()?.__window?.current?.();\n installAutomationIntrospection({ windowId: bootstrapWindow?.windowId ?? null });\n }\n const initialSet = await createControllerSet(entry, 0, route);\n\n try {\n return mount(NavigationShell, {\n target,\n props: {\n manifest: VIEW_MANIFEST,\n initialEntry: entry,\n initialRoute: route,\n initialControllerSet: initialSet,\n createControllerSetFn: createControllerSet,\n getViewManifestEntryFn: getViewManifestEntry,\n },\n });\n } catch (error) {\n // A failed initial mount must not orphan the initial controller set: the\n // shell never took ownership, so its scopes are disposed here once.\n return failWithRollback(error, [\n ...initialSet.layoutScopes.map((scope) => () => disposeScope(scope as any)),\n () => disposeScope(initialSet.pageScope as any),\n ], 'Wood initial mount rollback failed');\n }\n}\n`;\n}\n"],"mappings":"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AA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAiKA,mBAAmB,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkG9D;","names":["path"]}
1
+ {"version":3,"sources":["../../src/codegen/renderer_app_generator.ts"],"sourcesContent":["import { existsSync } from 'fs';\nimport path from 'path';\nimport type { ViewDef } from '../types/index.cjs';\nimport { loaderCompanionCandidates } from '../loader/loader_orchestration.cjs';\nimport { generatedFileNotice } from './generated_file_notice.cjs';\n\nconst RENDERER_APP_SOURCE_INSTRUCTIONS = [\n 'To change routes, page components, layouts, or renderer controllers, edit the views YAML',\n 'configured by wood.config.yml renderer.openapi and the matching UI source files.',\n] as const;\n\nexport interface RendererAppGeneratorOptions {\n /** Absolute path to renderer component root (e.g. /app/ui) */\n componentDir: string;\n /** Absolute output directory where generated file will be written */\n outDir: string;\n /** Optional explicit default view key (e.g. \"main.chat\") */\n entryViewKey?: string;\n}\n\nfunction computeImportPath(fromDir: string, toFile: string): string {\n let rel = path.relative(fromDir, toFile);\n\n if (!rel.startsWith('.')) {\n rel = './' + rel;\n }\n\n return rel.replace(/\\\\/g, '/');\n}\n\nfunction resolveFromComponentDir(componentDir: string, fileRef: string): string {\n if (path.isAbsolute(fileRef)) {\n return fileRef;\n }\n\n const normalizedRef = fileRef.replace(/\\\\/g, '/');\n const componentDirName = path.basename(componentDir).replace(/\\\\/g, '/');\n\n if (normalizedRef.startsWith(componentDirName + '/')) {\n const stripped = normalizedRef.slice(componentDirName.length + 1);\n return path.resolve(componentDir, stripped);\n }\n\n return path.resolve(componentDir, normalizedRef);\n}\n\nfunction viewKey(view: ViewDef): string {\n return `${view.windowName}.${view.pageName}`;\n}\n\nfunction toIdentifier(prefix: string, value: string): string {\n const core = value\n .replace(/[^a-zA-Z0-9]+/g, ' ')\n .trim()\n .split(/\\s+/)\n .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))\n .join('');\n\n return `${prefix}${core || 'Component'}`;\n}\n\nfunction escapeSingleQuoted(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\nconst CONTROLLER_FILE_SUFFIXES = [\n '.ts',\n '.js',\n '.controller.ts',\n '.controller.js',\n] as const;\n\nconst CONTROLLER_INDEX_CANDIDATES = [\n 'index.ts',\n 'index.js',\n] as const;\n\nfunction resolveControllerFile(controllerRef: string, componentDir: string): string | null {\n const normalized = controllerRef.trim();\n const basePath = resolveFromComponentDir(componentDir, normalized);\n\n if (existsSync(basePath)) {\n return basePath;\n }\n\n if (path.extname(basePath)) {\n return null;\n }\n\n for (const suffix of CONTROLLER_FILE_SUFFIXES) {\n const candidate = basePath + suffix;\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n for (const indexFile of CONTROLLER_INDEX_CANDIDATES) {\n const candidate = path.resolve(basePath, indexFile);\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n/**\n * The companion data loader of a view/layout (`pages/chat.svelte` →\n * `pages/chat.load.ts`), discovered at build. None is fine (`null`); more\n * than one companion is ambiguous and fails the build.\n */\nfunction resolveLoaderFile(viewPath: string, componentDir: string): string | null {\n const present = loaderCompanionCandidates(viewPath)\n .map((candidate) => resolveFromComponentDir(componentDir, candidate))\n .filter((candidate) => existsSync(candidate));\n if (present.length > 1) {\n throw new Error(\n `Renderer app codegen failed: ambiguous data loader for \"${viewPath}\": ${present.join(', ')}. ` +\n 'Keep exactly one .load companion.',\n );\n }\n return present[0] ?? null;\n}\n\nfunction resolveDefaultViewKey(sortedViews: ViewDef[], entryViewKey?: string): string {\n if (sortedViews.length === 0) return '';\n\n const configuredEntry = entryViewKey?.trim();\n if (!configuredEntry) {\n return viewKey(sortedViews[0]);\n }\n\n const availableKeys = sortedViews.map((view) => viewKey(view));\n if (availableKeys.includes(configuredEntry)) {\n return configuredEntry;\n }\n\n throw new Error(\n `Renderer entry \"${configuredEntry}\" was not found in parsed views. Available view keys: ${availableKeys.join(', ')}`,\n );\n}\n\nexport function generateRendererApp(\n views: ViewDef[],\n options: RendererAppGeneratorOptions,\n): string {\n const sortedViews = [...views].sort((a, b) => viewKey(a).localeCompare(viewKey(b)));\n const defaultViewKey = resolveDefaultViewKey(sortedViews, options.entryViewKey);\n\n const importLines: string[] = [];\n const viewIdentifiers = new Map<string, string>();\n const layoutIdentifiers = new Map<string, string>();\n const controllerIdentifiers = new Map<string, { identifier: string; importPath: string }>();\n /** view/layout path → generated loader identifier (only when a companion exists). */\n const loaderIdentifiers = new Map<string, string>();\n const usedNames = new Set<string>();\n\n const allocateName = (prefix: string, filePath: string): string => {\n const base = toIdentifier(prefix, filePath);\n let name = base;\n let suffix = 2;\n while (usedNames.has(name)) {\n name = `${base}${suffix}`;\n suffix += 1;\n }\n usedNames.add(name);\n return name;\n };\n\n const ensureControllerImported = (controllerRef: string, owningViewKey: string): void => {\n if (controllerIdentifiers.has(controllerRef)) return;\n\n const resolvedControllerFile = resolveControllerFile(controllerRef, options.componentDir);\n if (!resolvedControllerFile) {\n throw new Error(\n `Renderer app codegen failed: controller \"${controllerRef}\" for view \"${owningViewKey}\" was not found. ` +\n 'Controller must point to an existing file with a default exported class.',\n );\n }\n\n const identifier = allocateName('PageController', controllerRef);\n const importPath = computeImportPath(options.outDir, resolvedControllerFile);\n controllerIdentifiers.set(controllerRef, { identifier, importPath });\n importLines.push(`import ${identifier} from '${importPath}';`);\n };\n\n // Build discovers and IMPORTS companion loaders; none runs until a page\n // opens (createControllerSet executes them lazily per navigation).\n const ensureLoaderImported = (viewPath: string): void => {\n if (loaderIdentifiers.has(viewPath)) return;\n const loaderFile = resolveLoaderFile(viewPath, options.componentDir);\n if (!loaderFile) return;\n const identifier = allocateName('Loader', viewPath);\n loaderIdentifiers.set(viewPath, identifier);\n importLines.push(`import ${identifier} from '${computeImportPath(options.outDir, loaderFile)}';`);\n };\n\n for (const view of sortedViews) {\n if (!viewIdentifiers.has(view.viewPath)) {\n const identifier = allocateName('View', view.viewPath);\n viewIdentifiers.set(view.viewPath, identifier);\n const abs = resolveFromComponentDir(options.componentDir, view.viewPath);\n const importPath = computeImportPath(options.outDir, abs);\n importLines.push(`import ${identifier} from '${importPath}';`);\n }\n ensureLoaderImported(view.viewPath);\n\n for (const layoutPath of view.layouts) {\n ensureLoaderImported(layoutPath);\n if (layoutIdentifiers.has(layoutPath)) continue;\n const identifier = allocateName('Layout', layoutPath);\n layoutIdentifiers.set(layoutPath, identifier);\n const abs = resolveFromComponentDir(options.componentDir, layoutPath);\n const importPath = computeImportPath(options.outDir, abs);\n importLines.push(`import ${identifier} from '${importPath}';`);\n }\n\n if (view.controller) {\n ensureControllerImported(view.controller, viewKey(view));\n }\n\n for (const layoutController of view.layoutControllers ?? []) {\n if (!layoutController) continue;\n ensureControllerImported(layoutController, viewKey(view));\n }\n }\n\n const manifestEntries = sortedViews.map((view) => {\n const key = viewKey(view);\n const viewIdentifier = viewIdentifiers.get(view.viewPath)!;\n const layoutList = view.layouts.map((layoutPath) => layoutIdentifiers.get(layoutPath)!);\n const layoutLoaders = view.layouts.map((layoutPath) => loaderIdentifiers.get(layoutPath) ?? 'null');\n const viewLoader = loaderIdentifiers.get(view.viewPath) ?? 'null';\n const controllerPath = view.controller ? `'${escapeSingleQuoted(view.controller)}'` : 'undefined';\n const controllerClass = view.controller\n ? controllerIdentifiers.get(view.controller)!.identifier\n : 'undefined';\n const layoutControllerRefs = view.layoutControllers;\n if (layoutControllerRefs && layoutControllerRefs.length !== view.layouts.length) {\n throw new Error(\n `Renderer app codegen failed: layout controller count for view \"${key}\" does not match layout count.`,\n );\n }\n const normalizedLayoutControllerRefs = layoutControllerRefs ?? Array.from(\n { length: view.layouts.length },\n () => undefined,\n );\n const layoutControllerPaths = `[${normalizedLayoutControllerRefs\n .map((controllerRef) => (controllerRef ? `'${escapeSingleQuoted(controllerRef)}'` : 'undefined'))\n .join(', ')}]`;\n const layoutControllerClasses = `[${normalizedLayoutControllerRefs\n .map((controllerRef) => (controllerRef ? controllerIdentifiers.get(controllerRef)!.identifier : 'undefined'))\n .join(', ')}]`;\n const layoutPreserve = view.layoutPreserve ?? Array.from(\n { length: view.layouts.length },\n () => false,\n );\n if (layoutPreserve.length !== view.layouts.length) {\n throw new Error(\n `Renderer app codegen failed: layout preserve count for view \"${key}\" does not match layout count.`,\n );\n }\n\n return ` '${escapeSingleQuoted(key)}': {\n key: '${escapeSingleQuoted(key)}',\n windowName: '${escapeSingleQuoted(view.windowName)}',\n pageName: '${escapeSingleQuoted(view.pageName)}',\n viewPath: '${escapeSingleQuoted(view.viewPath)}',\n layoutPaths: ${JSON.stringify(view.layouts)},\n layoutControllers: ${layoutControllerPaths},\n layoutPreserve: ${JSON.stringify(layoutPreserve)},\n controller: ${controllerPath},\n layoutControllerClasses: ${layoutControllerClasses},\n controllerClass: ${controllerClass},\n view: ${viewIdentifier},\n layouts: [${layoutList.join(', ')}],\n viewLoader: ${viewLoader},\n layoutLoaders: [${layoutLoaders.join(', ')}],\n },`;\n });\n\n return `${generatedFileNotice(RENDERER_APP_SOURCE_INSTRUCTIONS)}\nimport { mount } from 'svelte';\nimport { App, getApp, getContainer } from '@noego/wood/client';\nimport { Navigation, getNavigation, readNavigationHistory, installAutomationIntrospection } from '@noego/wood/navigation';\nimport { createLoaderRequest, initializeArgument, initializeController, runViewLoaders } from '@noego/wood/navigation';\nimport { Ipc, getIpc } from '@noego/wood/ipc';\nimport NavigationShell from '@noego/wood/navigation-shell';\n${importLines.join('\\n')}\n\ninterface GeneratedAppViewEntry {\n key: string;\n windowName: string;\n pageName: string;\n viewPath: string;\n layoutPaths: string[];\n layoutControllers: Array<string | undefined>;\n layoutPreserve: boolean[];\n controller?: string;\n layoutControllerClasses: Array<any | undefined>;\n controllerClass?: any;\n view: any;\n layouts: any[];\n /** Companion data loader of the view (\\`pages/x.svelte\\` → \\`pages/x.load.ts\\`), discovered at build. */\n viewLoader: ((request: any) => unknown) | null;\n /** Index-aligned companion loaders of the layout stack. */\n layoutLoaders: Array<((request: any) => unknown) | null>;\n}\n\ntype RouteInput = { params: Record<string, string>; query: Record<string, string> };\n\nconst VIEW_MANIFEST: Record<string, GeneratedAppViewEntry> = {\n${manifestEntries.join('\\n')}\n};\n\nfunction getViewManifestEntry(viewKey: string): GeneratedAppViewEntry | null {\n return VIEW_MANIFEST[viewKey] ?? null;\n}\n\ninterface WoodScopedContainer {\n get(token: any): unknown | Promise<unknown>;\n registerClass?(token: any): void;\n registerFunction?(\n token: any,\n resolver: (...args: any[]) => unknown,\n options?: Record<string, unknown>,\n ): void;\n}\n\ninterface WoodContainer extends WoodScopedContainer {\n extend?(): WoodScopedContainer;\n}\n\nfunction createScopedContainer(): WoodScopedContainer {\n const container = getContainer() as WoodContainer;\n if (typeof container.extend !== 'function') {\n throw new Error('Wood client container does not support extend().');\n }\n\n const scopedContainer = container.extend();\n\n if (!scopedContainer || typeof scopedContainer.get !== 'function') {\n throw new Error('Wood client container does not support get().');\n }\n\n // Renderer builtins belong to the root created by getContainer().\n // Page scopes inherit them; re-registering resolved ancestor singletons is invalid.\n\n return scopedContainer as WoodScopedContainer;\n}\n\nasync function resolveControllerInScope(\n scope: WoodScopedContainer,\n entry: GeneratedAppViewEntry,\n ControllerClass: any,\n contextLabel: string,\n): Promise<unknown> {\n if (typeof ControllerClass !== 'function') {\n throw new Error(\n \\`Controller for \\${contextLabel} in view \"\\${entry.key}\" is invalid. Expected a default exported class.\\`,\n );\n }\n\n if (typeof scope.registerClass === 'function') {\n scope.registerClass(ControllerClass);\n }\n\n return await scope.get(ControllerClass);\n}\n\ntype RollbackStep = () => unknown | Promise<unknown>;\n\nasync function disposeScope(scope: { dispose?: () => Promise<void> } | null): Promise<void> {\n if (!scope || typeof scope.dispose !== 'function') return;\n await scope.dispose();\n}\n\n/**\n * Roll back after \\`primary\\` failed: every step is attempted exactly once and\n * every cleanup failure is retained as its original error object next to the\n * primary failure (\\`errors\\`, with \\`cause\\` = primary). Never swallows.\n */\nasync function failWithRollback(primary: unknown, steps: RollbackStep[], message: string): Promise<never> {\n const errors: unknown[] = [primary];\n for (const step of steps) {\n try { await step(); } catch (error) { errors.push(error); }\n }\n if (errors.length === 1) throw primary;\n throw Object.assign(new Error(message), { errors, cause: primary });\n}\n\n/**\n * One page scope per view lifecycle: the view controller and every\n * non-preservable layout controller resolve from the SAME scope, so Scoped\n * services/state are shared within the page (the NoEgo renderer model).\n * Layouts marked preservable in the manifest own dedicated scopes that\n * survive page transitions and dispose when the layout finally leaves.\n *\n * Data loaders (shared orchestration) run first for this navigation —\n * non-preserved layouts in stack order, then the view — and each\n * controller's optional \\`initialize({ params, query[, data] })\\` receives\n * its loader's data. A loader failure disposes the scopes created here.\n */\nasync function createControllerSet(\n entry: GeneratedAppViewEntry,\n preservedDepth = 0,\n route: RouteInput = { params: {}, query: {} },\n): Promise<{\n controller: unknown;\n layoutControllers: Array<unknown>;\n pageScope: WoodScopedContainer;\n layoutScopes: Array<WoodScopedContainer | null>;\n}> {\n const pageScope = createScopedContainer();\n const layoutControllers: unknown[] = [];\n const layoutScopes: Array<WoodScopedContainer | null> = [];\n const createdLayoutScopes: WoodScopedContainer[] = [];\n\n try {\n const loaded = await runViewLoaders(\n { layouts: entry.layoutLoaders, view: entry.viewLoader },\n createLoaderRequest(entry.key, route),\n preservedDepth,\n );\n\n for (let i = 0; i < preservedDepth; i += 1) {\n layoutControllers.push(undefined);\n layoutScopes.push(null);\n }\n\n for (let i = preservedDepth; i < entry.layoutControllerClasses.length; i += 1) {\n const LayoutController = entry.layoutControllerClasses[i];\n if (!LayoutController) {\n layoutControllers.push(undefined);\n layoutScopes.push(null);\n continue;\n }\n const preservable = entry.layoutPreserve?.[i] === true;\n let scope = pageScope;\n if (preservable) {\n scope = createScopedContainer();\n createdLayoutScopes.push(scope);\n }\n const controller = await resolveControllerInScope(\n scope,\n entry,\n LayoutController,\n \\`layout[\\${i}]\\`,\n );\n await initializeController(controller, initializeArgument(route, loaded.layouts[i] ?? null));\n layoutControllers.push(controller);\n layoutScopes.push(preservable ? scope : null);\n }\n\n const controller = entry.controllerClass\n ? await resolveControllerInScope(pageScope, entry, entry.controllerClass, 'page')\n : undefined;\n if (controller !== undefined) {\n await initializeController(controller, initializeArgument(route, loaded.view));\n }\n\n return { controller, layoutControllers, pageScope, layoutScopes };\n } catch (error) {\n // Failed transitions must not orphan scopes: every scope this set created\n // is disposed once, and every disposal failure is retained with the\n // original initialization failure instead of being swallowed.\n return failWithRollback(error, [\n ...createdLayoutScopes.map((scope) => () => disposeScope(scope as any)),\n () => disposeScope(pageScope as any),\n ], 'Wood controller-set rollback failed');\n }\n}\n\nconst DEFAULT_VIEW_KEY = '${escapeSingleQuoted(defaultViewKey)}';\n\ntype WoodWindowBootstrapInfo = {\n ok?: boolean;\n windowId?: string;\n defaultRoute?: string;\n};\n\ntype WoodBootstrapBridgeLike = {\n __window?: {\n current?: () => Promise<WoodWindowBootstrapInfo>;\n };\n};\n\nfunction getWoodBridge(): WoodBootstrapBridgeLike | null {\n const globalObj = globalThis as typeof globalThis & {\n __NOEGO_WOOD_BRIDGE__?: unknown;\n window?: {\n __NOEGO_WOOD_BRIDGE__?: unknown;\n };\n };\n\n const bridge = globalObj.__NOEGO_WOOD_BRIDGE__ ?? globalObj.window?.__NOEGO_WOOD_BRIDGE__;\n if (!bridge || typeof bridge !== 'object') {\n return null;\n }\n\n return bridge as WoodBootstrapBridgeLike;\n}\n\nasync function resolveInitialEntry(route: RouteInput): Promise<{ entry: GeneratedAppViewEntry; route: RouteInput }> {\n if (!DEFAULT_VIEW_KEY) {\n throw new Error('No views available in renderer manifest. Define at least one view in renderer OpenAPI YAML.');\n }\n\n const bridge = getWoodBridge();\n const bootstrapWindow = await bridge?.__window?.current?.();\n const requestedRoute = bootstrapWindow?.defaultRoute;\n\n const routeEntry = requestedRoute ? getViewManifestEntry(requestedRoute) : null;\n const entry = routeEntry ?? getViewManifestEntry(DEFAULT_VIEW_KEY);\n if (!entry) {\n throw new Error(\\`Default view \"\\${DEFAULT_VIEW_KEY}\" is missing from generated view manifest.\\`);\n }\n\n // Reload restores only a valid manifest route owned by this window. Invalid\n // or foreign history falls back to the host's original bootstrap unchanged.\n const saved = readNavigationHistory(typeof window === 'undefined' ? null : window.history.state);\n const savedEntry = saved ? getViewManifestEntry(saved.page) : null;\n if (saved && savedEntry && savedEntry.windowName === (bootstrapWindow?.windowId ?? entry.windowName)) {\n return {entry: savedEntry, route: {params: saved.params, query: saved.query}};\n }\n return {entry, route};\n}\n\nfunction isAutomationBuild(): boolean {\n try {\n const env = (import.meta as { env?: { DEV?: boolean; MODE?: string } }).env;\n return Boolean(env?.DEV) || env?.MODE === 'test';\n } catch {\n return false;\n }\n}\n\nexport const supportsInitialRoute = true;\nexport async function app(target: Element, initialRoute: RouteInput = { params: {}, query: {} }) {\n const {entry, route} = await resolveInitialEntry(initialRoute);\n\n getNavigation().replace(entry.key, route.params, route.query);\n\n // Dev/test-only Axe introspection surface; never present in production builds.\n if (isAutomationBuild()) {\n const bootstrapWindow = await getWoodBridge()?.__window?.current?.();\n installAutomationIntrospection({ windowId: bootstrapWindow?.windowId ?? null });\n }\n const initialSet = await createControllerSet(entry, 0, route);\n\n try {\n return mount(NavigationShell, {\n target,\n props: {\n manifest: VIEW_MANIFEST,\n initialEntry: entry,\n initialRoute: route,\n initialControllerSet: initialSet,\n createControllerSetFn: createControllerSet,\n getViewManifestEntryFn: getViewManifestEntry,\n },\n });\n } catch (error) {\n // A failed initial mount must not orphan the initial controller set: the\n // shell never took ownership, so its scopes are disposed here once.\n return failWithRollback(error, [\n ...initialSet.layoutScopes.map((scope) => () => disposeScope(scope as any)),\n () => disposeScope(initialSet.pageScope as any),\n ], 'Wood initial mount rollback failed');\n }\n}\n`;\n}\n"],"mappings":"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AA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAiKA,mBAAmB,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmG9D;","names":["path"]}
@@ -207,7 +207,7 @@ function generateRendererApp(views, options) {
207
207
  return `${generatedFileNotice(RENDERER_APP_SOURCE_INSTRUCTIONS)}
208
208
  import { mount } from 'svelte';
209
209
  import { App, getApp, getContainer } from '@noego/wood/client';
210
- import { Navigation, getNavigation, installAutomationIntrospection } from '@noego/wood/navigation';
210
+ import { Navigation, getNavigation, readNavigationHistory, installAutomationIntrospection } from '@noego/wood/navigation';
211
211
  import { createLoaderRequest, initializeArgument, initializeController, runViewLoaders } from '@noego/wood/navigation';
212
212
  import { Ipc, getIpc } from '@noego/wood/ipc';
213
213
  import NavigationShell from '@noego/wood/navigation-shell';
@@ -426,7 +426,7 @@ function getWoodBridge(): WoodBootstrapBridgeLike | null {
426
426
  return bridge as WoodBootstrapBridgeLike;
427
427
  }
428
428
 
429
- async function resolveInitialEntry(): Promise<GeneratedAppViewEntry> {
429
+ async function resolveInitialEntry(route: RouteInput): Promise<{ entry: GeneratedAppViewEntry; route: RouteInput }> {
430
430
  if (!DEFAULT_VIEW_KEY) {
431
431
  throw new Error('No views available in renderer manifest. Define at least one view in renderer OpenAPI YAML.');
432
432
  }
@@ -435,19 +435,20 @@ async function resolveInitialEntry(): Promise<GeneratedAppViewEntry> {
435
435
  const bootstrapWindow = await bridge?.__window?.current?.();
436
436
  const requestedRoute = bootstrapWindow?.defaultRoute;
437
437
 
438
- if (requestedRoute) {
439
- const routeEntry = getViewManifestEntry(requestedRoute);
440
- if (routeEntry) {
441
- return routeEntry;
442
- }
443
- }
444
-
445
- const entry = getViewManifestEntry(DEFAULT_VIEW_KEY);
438
+ const routeEntry = requestedRoute ? getViewManifestEntry(requestedRoute) : null;
439
+ const entry = routeEntry ?? getViewManifestEntry(DEFAULT_VIEW_KEY);
446
440
  if (!entry) {
447
441
  throw new Error(\`Default view "\${DEFAULT_VIEW_KEY}" is missing from generated view manifest.\`);
448
442
  }
449
443
 
450
- return entry;
444
+ // Reload restores only a valid manifest route owned by this window. Invalid
445
+ // or foreign history falls back to the host's original bootstrap unchanged.
446
+ const saved = readNavigationHistory(typeof window === 'undefined' ? null : window.history.state);
447
+ const savedEntry = saved ? getViewManifestEntry(saved.page) : null;
448
+ if (saved && savedEntry && savedEntry.windowName === (bootstrapWindow?.windowId ?? entry.windowName)) {
449
+ return {entry: savedEntry, route: {params: saved.params, query: saved.query}};
450
+ }
451
+ return {entry, route};
451
452
  }
452
453
 
453
454
  function isAutomationBuild(): boolean {
@@ -460,8 +461,8 @@ function isAutomationBuild(): boolean {
460
461
  }
461
462
 
462
463
  export const supportsInitialRoute = true;
463
- export async function app(target: Element, route: RouteInput = { params: {}, query: {} }) {
464
- const entry = await resolveInitialEntry();
464
+ export async function app(target: Element, initialRoute: RouteInput = { params: {}, query: {} }) {
465
+ const {entry, route} = await resolveInitialEntry(initialRoute);
465
466
 
466
467
  getNavigation().replace(entry.key, route.params, route.query);
467
468
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/codegen/renderer_app_generator.ts"],"sourcesContent":["import { existsSync } from 'fs';\nimport path from 'path';\nimport type { ViewDef } from '../types/index.js';\nimport { loaderCompanionCandidates } from '../loader/loader_orchestration.js';\nimport { generatedFileNotice } from './generated_file_notice.js';\n\nconst RENDERER_APP_SOURCE_INSTRUCTIONS = [\n 'To change routes, page components, layouts, or renderer controllers, edit the views YAML',\n 'configured by wood.config.yml renderer.openapi and the matching UI source files.',\n] as const;\n\nexport interface RendererAppGeneratorOptions {\n /** Absolute path to renderer component root (e.g. /app/ui) */\n componentDir: string;\n /** Absolute output directory where generated file will be written */\n outDir: string;\n /** Optional explicit default view key (e.g. \"main.chat\") */\n entryViewKey?: string;\n}\n\nfunction computeImportPath(fromDir: string, toFile: string): string {\n let rel = path.relative(fromDir, toFile);\n\n if (!rel.startsWith('.')) {\n rel = './' + rel;\n }\n\n return rel.replace(/\\\\/g, '/');\n}\n\nfunction resolveFromComponentDir(componentDir: string, fileRef: string): string {\n if (path.isAbsolute(fileRef)) {\n return fileRef;\n }\n\n const normalizedRef = fileRef.replace(/\\\\/g, '/');\n const componentDirName = path.basename(componentDir).replace(/\\\\/g, '/');\n\n if (normalizedRef.startsWith(componentDirName + '/')) {\n const stripped = normalizedRef.slice(componentDirName.length + 1);\n return path.resolve(componentDir, stripped);\n }\n\n return path.resolve(componentDir, normalizedRef);\n}\n\nfunction viewKey(view: ViewDef): string {\n return `${view.windowName}.${view.pageName}`;\n}\n\nfunction toIdentifier(prefix: string, value: string): string {\n const core = value\n .replace(/[^a-zA-Z0-9]+/g, ' ')\n .trim()\n .split(/\\s+/)\n .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))\n .join('');\n\n return `${prefix}${core || 'Component'}`;\n}\n\nfunction escapeSingleQuoted(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\nconst CONTROLLER_FILE_SUFFIXES = [\n '.ts',\n '.js',\n '.controller.ts',\n '.controller.js',\n] as const;\n\nconst CONTROLLER_INDEX_CANDIDATES = [\n 'index.ts',\n 'index.js',\n] as const;\n\nfunction resolveControllerFile(controllerRef: string, componentDir: string): string | null {\n const normalized = controllerRef.trim();\n const basePath = resolveFromComponentDir(componentDir, normalized);\n\n if (existsSync(basePath)) {\n return basePath;\n }\n\n if (path.extname(basePath)) {\n return null;\n }\n\n for (const suffix of CONTROLLER_FILE_SUFFIXES) {\n const candidate = basePath + suffix;\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n for (const indexFile of CONTROLLER_INDEX_CANDIDATES) {\n const candidate = path.resolve(basePath, indexFile);\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n/**\n * The companion data loader of a view/layout (`pages/chat.svelte` →\n * `pages/chat.load.ts`), discovered at build. None is fine (`null`); more\n * than one companion is ambiguous and fails the build.\n */\nfunction resolveLoaderFile(viewPath: string, componentDir: string): string | null {\n const present = loaderCompanionCandidates(viewPath)\n .map((candidate) => resolveFromComponentDir(componentDir, candidate))\n .filter((candidate) => existsSync(candidate));\n if (present.length > 1) {\n throw new Error(\n `Renderer app codegen failed: ambiguous data loader for \"${viewPath}\": ${present.join(', ')}. ` +\n 'Keep exactly one .load companion.',\n );\n }\n return present[0] ?? null;\n}\n\nfunction resolveDefaultViewKey(sortedViews: ViewDef[], entryViewKey?: string): string {\n if (sortedViews.length === 0) return '';\n\n const configuredEntry = entryViewKey?.trim();\n if (!configuredEntry) {\n return viewKey(sortedViews[0]);\n }\n\n const availableKeys = sortedViews.map((view) => viewKey(view));\n if (availableKeys.includes(configuredEntry)) {\n return configuredEntry;\n }\n\n throw new Error(\n `Renderer entry \"${configuredEntry}\" was not found in parsed views. Available view keys: ${availableKeys.join(', ')}`,\n );\n}\n\nexport function generateRendererApp(\n views: ViewDef[],\n options: RendererAppGeneratorOptions,\n): string {\n const sortedViews = [...views].sort((a, b) => viewKey(a).localeCompare(viewKey(b)));\n const defaultViewKey = resolveDefaultViewKey(sortedViews, options.entryViewKey);\n\n const importLines: string[] = [];\n const viewIdentifiers = new Map<string, string>();\n const layoutIdentifiers = new Map<string, string>();\n const controllerIdentifiers = new Map<string, { identifier: string; importPath: string }>();\n /** view/layout path → generated loader identifier (only when a companion exists). */\n const loaderIdentifiers = new Map<string, string>();\n const usedNames = new Set<string>();\n\n const allocateName = (prefix: string, filePath: string): string => {\n const base = toIdentifier(prefix, filePath);\n let name = base;\n let suffix = 2;\n while (usedNames.has(name)) {\n name = `${base}${suffix}`;\n suffix += 1;\n }\n usedNames.add(name);\n return name;\n };\n\n const ensureControllerImported = (controllerRef: string, owningViewKey: string): void => {\n if (controllerIdentifiers.has(controllerRef)) return;\n\n const resolvedControllerFile = resolveControllerFile(controllerRef, options.componentDir);\n if (!resolvedControllerFile) {\n throw new Error(\n `Renderer app codegen failed: controller \"${controllerRef}\" for view \"${owningViewKey}\" was not found. ` +\n 'Controller must point to an existing file with a default exported class.',\n );\n }\n\n const identifier = allocateName('PageController', controllerRef);\n const importPath = computeImportPath(options.outDir, resolvedControllerFile);\n controllerIdentifiers.set(controllerRef, { identifier, importPath });\n importLines.push(`import ${identifier} from '${importPath}';`);\n };\n\n // Build discovers and IMPORTS companion loaders; none runs until a page\n // opens (createControllerSet executes them lazily per navigation).\n const ensureLoaderImported = (viewPath: string): void => {\n if (loaderIdentifiers.has(viewPath)) return;\n const loaderFile = resolveLoaderFile(viewPath, options.componentDir);\n if (!loaderFile) return;\n const identifier = allocateName('Loader', viewPath);\n loaderIdentifiers.set(viewPath, identifier);\n importLines.push(`import ${identifier} from '${computeImportPath(options.outDir, loaderFile)}';`);\n };\n\n for (const view of sortedViews) {\n if (!viewIdentifiers.has(view.viewPath)) {\n const identifier = allocateName('View', view.viewPath);\n viewIdentifiers.set(view.viewPath, identifier);\n const abs = resolveFromComponentDir(options.componentDir, view.viewPath);\n const importPath = computeImportPath(options.outDir, abs);\n importLines.push(`import ${identifier} from '${importPath}';`);\n }\n ensureLoaderImported(view.viewPath);\n\n for (const layoutPath of view.layouts) {\n ensureLoaderImported(layoutPath);\n if (layoutIdentifiers.has(layoutPath)) continue;\n const identifier = allocateName('Layout', layoutPath);\n layoutIdentifiers.set(layoutPath, identifier);\n const abs = resolveFromComponentDir(options.componentDir, layoutPath);\n const importPath = computeImportPath(options.outDir, abs);\n importLines.push(`import ${identifier} from '${importPath}';`);\n }\n\n if (view.controller) {\n ensureControllerImported(view.controller, viewKey(view));\n }\n\n for (const layoutController of view.layoutControllers ?? []) {\n if (!layoutController) continue;\n ensureControllerImported(layoutController, viewKey(view));\n }\n }\n\n const manifestEntries = sortedViews.map((view) => {\n const key = viewKey(view);\n const viewIdentifier = viewIdentifiers.get(view.viewPath)!;\n const layoutList = view.layouts.map((layoutPath) => layoutIdentifiers.get(layoutPath)!);\n const layoutLoaders = view.layouts.map((layoutPath) => loaderIdentifiers.get(layoutPath) ?? 'null');\n const viewLoader = loaderIdentifiers.get(view.viewPath) ?? 'null';\n const controllerPath = view.controller ? `'${escapeSingleQuoted(view.controller)}'` : 'undefined';\n const controllerClass = view.controller\n ? controllerIdentifiers.get(view.controller)!.identifier\n : 'undefined';\n const layoutControllerRefs = view.layoutControllers;\n if (layoutControllerRefs && layoutControllerRefs.length !== view.layouts.length) {\n throw new Error(\n `Renderer app codegen failed: layout controller count for view \"${key}\" does not match layout count.`,\n );\n }\n const normalizedLayoutControllerRefs = layoutControllerRefs ?? Array.from(\n { length: view.layouts.length },\n () => undefined,\n );\n const layoutControllerPaths = `[${normalizedLayoutControllerRefs\n .map((controllerRef) => (controllerRef ? `'${escapeSingleQuoted(controllerRef)}'` : 'undefined'))\n .join(', ')}]`;\n const layoutControllerClasses = `[${normalizedLayoutControllerRefs\n .map((controllerRef) => (controllerRef ? controllerIdentifiers.get(controllerRef)!.identifier : 'undefined'))\n .join(', ')}]`;\n const layoutPreserve = view.layoutPreserve ?? Array.from(\n { length: view.layouts.length },\n () => false,\n );\n if (layoutPreserve.length !== view.layouts.length) {\n throw new Error(\n `Renderer app codegen failed: layout preserve count for view \"${key}\" does not match layout count.`,\n );\n }\n\n return ` '${escapeSingleQuoted(key)}': {\n key: '${escapeSingleQuoted(key)}',\n windowName: '${escapeSingleQuoted(view.windowName)}',\n pageName: '${escapeSingleQuoted(view.pageName)}',\n viewPath: '${escapeSingleQuoted(view.viewPath)}',\n layoutPaths: ${JSON.stringify(view.layouts)},\n layoutControllers: ${layoutControllerPaths},\n layoutPreserve: ${JSON.stringify(layoutPreserve)},\n controller: ${controllerPath},\n layoutControllerClasses: ${layoutControllerClasses},\n controllerClass: ${controllerClass},\n view: ${viewIdentifier},\n layouts: [${layoutList.join(', ')}],\n viewLoader: ${viewLoader},\n layoutLoaders: [${layoutLoaders.join(', ')}],\n },`;\n });\n\n return `${generatedFileNotice(RENDERER_APP_SOURCE_INSTRUCTIONS)}\nimport { mount } from 'svelte';\nimport { App, getApp, getContainer } from '@noego/wood/client';\nimport { Navigation, getNavigation, installAutomationIntrospection } from '@noego/wood/navigation';\nimport { createLoaderRequest, initializeArgument, initializeController, runViewLoaders } from '@noego/wood/navigation';\nimport { Ipc, getIpc } from '@noego/wood/ipc';\nimport NavigationShell from '@noego/wood/navigation-shell';\n${importLines.join('\\n')}\n\ninterface GeneratedAppViewEntry {\n key: string;\n windowName: string;\n pageName: string;\n viewPath: string;\n layoutPaths: string[];\n layoutControllers: Array<string | undefined>;\n layoutPreserve: boolean[];\n controller?: string;\n layoutControllerClasses: Array<any | undefined>;\n controllerClass?: any;\n view: any;\n layouts: any[];\n /** Companion data loader of the view (\\`pages/x.svelte\\` → \\`pages/x.load.ts\\`), discovered at build. */\n viewLoader: ((request: any) => unknown) | null;\n /** Index-aligned companion loaders of the layout stack. */\n layoutLoaders: Array<((request: any) => unknown) | null>;\n}\n\ntype RouteInput = { params: Record<string, string>; query: Record<string, string> };\n\nconst VIEW_MANIFEST: Record<string, GeneratedAppViewEntry> = {\n${manifestEntries.join('\\n')}\n};\n\nfunction getViewManifestEntry(viewKey: string): GeneratedAppViewEntry | null {\n return VIEW_MANIFEST[viewKey] ?? null;\n}\n\ninterface WoodScopedContainer {\n get(token: any): unknown | Promise<unknown>;\n registerClass?(token: any): void;\n registerFunction?(\n token: any,\n resolver: (...args: any[]) => unknown,\n options?: Record<string, unknown>,\n ): void;\n}\n\ninterface WoodContainer extends WoodScopedContainer {\n extend?(): WoodScopedContainer;\n}\n\nfunction createScopedContainer(): WoodScopedContainer {\n const container = getContainer() as WoodContainer;\n if (typeof container.extend !== 'function') {\n throw new Error('Wood client container does not support extend().');\n }\n\n const scopedContainer = container.extend();\n\n if (!scopedContainer || typeof scopedContainer.get !== 'function') {\n throw new Error('Wood client container does not support get().');\n }\n\n // Renderer builtins belong to the root created by getContainer().\n // Page scopes inherit them; re-registering resolved ancestor singletons is invalid.\n\n return scopedContainer as WoodScopedContainer;\n}\n\nasync function resolveControllerInScope(\n scope: WoodScopedContainer,\n entry: GeneratedAppViewEntry,\n ControllerClass: any,\n contextLabel: string,\n): Promise<unknown> {\n if (typeof ControllerClass !== 'function') {\n throw new Error(\n \\`Controller for \\${contextLabel} in view \"\\${entry.key}\" is invalid. Expected a default exported class.\\`,\n );\n }\n\n if (typeof scope.registerClass === 'function') {\n scope.registerClass(ControllerClass);\n }\n\n return await scope.get(ControllerClass);\n}\n\ntype RollbackStep = () => unknown | Promise<unknown>;\n\nasync function disposeScope(scope: { dispose?: () => Promise<void> } | null): Promise<void> {\n if (!scope || typeof scope.dispose !== 'function') return;\n await scope.dispose();\n}\n\n/**\n * Roll back after \\`primary\\` failed: every step is attempted exactly once and\n * every cleanup failure is retained as its original error object next to the\n * primary failure (\\`errors\\`, with \\`cause\\` = primary). Never swallows.\n */\nasync function failWithRollback(primary: unknown, steps: RollbackStep[], message: string): Promise<never> {\n const errors: unknown[] = [primary];\n for (const step of steps) {\n try { await step(); } catch (error) { errors.push(error); }\n }\n if (errors.length === 1) throw primary;\n throw Object.assign(new Error(message), { errors, cause: primary });\n}\n\n/**\n * One page scope per view lifecycle: the view controller and every\n * non-preservable layout controller resolve from the SAME scope, so Scoped\n * services/state are shared within the page (the NoEgo renderer model).\n * Layouts marked preservable in the manifest own dedicated scopes that\n * survive page transitions and dispose when the layout finally leaves.\n *\n * Data loaders (shared orchestration) run first for this navigation —\n * non-preserved layouts in stack order, then the view — and each\n * controller's optional \\`initialize({ params, query[, data] })\\` receives\n * its loader's data. A loader failure disposes the scopes created here.\n */\nasync function createControllerSet(\n entry: GeneratedAppViewEntry,\n preservedDepth = 0,\n route: RouteInput = { params: {}, query: {} },\n): Promise<{\n controller: unknown;\n layoutControllers: Array<unknown>;\n pageScope: WoodScopedContainer;\n layoutScopes: Array<WoodScopedContainer | null>;\n}> {\n const pageScope = createScopedContainer();\n const layoutControllers: unknown[] = [];\n const layoutScopes: Array<WoodScopedContainer | null> = [];\n const createdLayoutScopes: WoodScopedContainer[] = [];\n\n try {\n const loaded = await runViewLoaders(\n { layouts: entry.layoutLoaders, view: entry.viewLoader },\n createLoaderRequest(entry.key, route),\n preservedDepth,\n );\n\n for (let i = 0; i < preservedDepth; i += 1) {\n layoutControllers.push(undefined);\n layoutScopes.push(null);\n }\n\n for (let i = preservedDepth; i < entry.layoutControllerClasses.length; i += 1) {\n const LayoutController = entry.layoutControllerClasses[i];\n if (!LayoutController) {\n layoutControllers.push(undefined);\n layoutScopes.push(null);\n continue;\n }\n const preservable = entry.layoutPreserve?.[i] === true;\n let scope = pageScope;\n if (preservable) {\n scope = createScopedContainer();\n createdLayoutScopes.push(scope);\n }\n const controller = await resolveControllerInScope(\n scope,\n entry,\n LayoutController,\n \\`layout[\\${i}]\\`,\n );\n await initializeController(controller, initializeArgument(route, loaded.layouts[i] ?? null));\n layoutControllers.push(controller);\n layoutScopes.push(preservable ? scope : null);\n }\n\n const controller = entry.controllerClass\n ? await resolveControllerInScope(pageScope, entry, entry.controllerClass, 'page')\n : undefined;\n if (controller !== undefined) {\n await initializeController(controller, initializeArgument(route, loaded.view));\n }\n\n return { controller, layoutControllers, pageScope, layoutScopes };\n } catch (error) {\n // Failed transitions must not orphan scopes: every scope this set created\n // is disposed once, and every disposal failure is retained with the\n // original initialization failure instead of being swallowed.\n return failWithRollback(error, [\n ...createdLayoutScopes.map((scope) => () => disposeScope(scope as any)),\n () => disposeScope(pageScope as any),\n ], 'Wood controller-set rollback failed');\n }\n}\n\nconst DEFAULT_VIEW_KEY = '${escapeSingleQuoted(defaultViewKey)}';\n\ntype WoodWindowBootstrapInfo = {\n ok?: boolean;\n windowId?: string;\n defaultRoute?: string;\n};\n\ntype WoodBootstrapBridgeLike = {\n __window?: {\n current?: () => Promise<WoodWindowBootstrapInfo>;\n };\n};\n\nfunction getWoodBridge(): WoodBootstrapBridgeLike | null {\n const globalObj = globalThis as typeof globalThis & {\n __NOEGO_WOOD_BRIDGE__?: unknown;\n window?: {\n __NOEGO_WOOD_BRIDGE__?: unknown;\n };\n };\n\n const bridge = globalObj.__NOEGO_WOOD_BRIDGE__ ?? globalObj.window?.__NOEGO_WOOD_BRIDGE__;\n if (!bridge || typeof bridge !== 'object') {\n return null;\n }\n\n return bridge as WoodBootstrapBridgeLike;\n}\n\nasync function resolveInitialEntry(): Promise<GeneratedAppViewEntry> {\n if (!DEFAULT_VIEW_KEY) {\n throw new Error('No views available in renderer manifest. Define at least one view in renderer OpenAPI YAML.');\n }\n\n const bridge = getWoodBridge();\n const bootstrapWindow = await bridge?.__window?.current?.();\n const requestedRoute = bootstrapWindow?.defaultRoute;\n\n if (requestedRoute) {\n const routeEntry = getViewManifestEntry(requestedRoute);\n if (routeEntry) {\n return routeEntry;\n }\n }\n\n const entry = getViewManifestEntry(DEFAULT_VIEW_KEY);\n if (!entry) {\n throw new Error(\\`Default view \"\\${DEFAULT_VIEW_KEY}\" is missing from generated view manifest.\\`);\n }\n\n return entry;\n}\n\nfunction isAutomationBuild(): boolean {\n try {\n const env = (import.meta as { env?: { DEV?: boolean; MODE?: string } }).env;\n return Boolean(env?.DEV) || env?.MODE === 'test';\n } catch {\n return false;\n }\n}\n\nexport const supportsInitialRoute = true;\nexport async function app(target: Element, route: RouteInput = { params: {}, query: {} }) {\n const entry = await resolveInitialEntry();\n\n getNavigation().replace(entry.key, route.params, route.query);\n\n // Dev/test-only Axe introspection surface; never present in production builds.\n if (isAutomationBuild()) {\n const bootstrapWindow = await getWoodBridge()?.__window?.current?.();\n installAutomationIntrospection({ windowId: bootstrapWindow?.windowId ?? null });\n }\n const initialSet = await createControllerSet(entry, 0, route);\n\n try {\n return mount(NavigationShell, {\n target,\n props: {\n manifest: VIEW_MANIFEST,\n initialEntry: entry,\n initialRoute: route,\n initialControllerSet: initialSet,\n createControllerSetFn: createControllerSet,\n getViewManifestEntryFn: getViewManifestEntry,\n },\n });\n } catch (error) {\n // A failed initial mount must not orphan the initial controller set: the\n // shell never took ownership, so its scopes are disposed here once.\n return failWithRollback(error, [\n ...initialSet.layoutScopes.map((scope) => () => disposeScope(scope as any)),\n () => disposeScope(initialSet.pageScope as any),\n ], 'Wood initial mount rollback failed');\n }\n}\n`;\n}\n"],"mappings":"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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAiKA,mBAAmB,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkG9D;","names":[]}
1
+ {"version":3,"sources":["../../src/codegen/renderer_app_generator.ts"],"sourcesContent":["import { existsSync } from 'fs';\nimport path from 'path';\nimport type { ViewDef } from '../types/index.js';\nimport { loaderCompanionCandidates } from '../loader/loader_orchestration.js';\nimport { generatedFileNotice } from './generated_file_notice.js';\n\nconst RENDERER_APP_SOURCE_INSTRUCTIONS = [\n 'To change routes, page components, layouts, or renderer controllers, edit the views YAML',\n 'configured by wood.config.yml renderer.openapi and the matching UI source files.',\n] as const;\n\nexport interface RendererAppGeneratorOptions {\n /** Absolute path to renderer component root (e.g. /app/ui) */\n componentDir: string;\n /** Absolute output directory where generated file will be written */\n outDir: string;\n /** Optional explicit default view key (e.g. \"main.chat\") */\n entryViewKey?: string;\n}\n\nfunction computeImportPath(fromDir: string, toFile: string): string {\n let rel = path.relative(fromDir, toFile);\n\n if (!rel.startsWith('.')) {\n rel = './' + rel;\n }\n\n return rel.replace(/\\\\/g, '/');\n}\n\nfunction resolveFromComponentDir(componentDir: string, fileRef: string): string {\n if (path.isAbsolute(fileRef)) {\n return fileRef;\n }\n\n const normalizedRef = fileRef.replace(/\\\\/g, '/');\n const componentDirName = path.basename(componentDir).replace(/\\\\/g, '/');\n\n if (normalizedRef.startsWith(componentDirName + '/')) {\n const stripped = normalizedRef.slice(componentDirName.length + 1);\n return path.resolve(componentDir, stripped);\n }\n\n return path.resolve(componentDir, normalizedRef);\n}\n\nfunction viewKey(view: ViewDef): string {\n return `${view.windowName}.${view.pageName}`;\n}\n\nfunction toIdentifier(prefix: string, value: string): string {\n const core = value\n .replace(/[^a-zA-Z0-9]+/g, ' ')\n .trim()\n .split(/\\s+/)\n .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))\n .join('');\n\n return `${prefix}${core || 'Component'}`;\n}\n\nfunction escapeSingleQuoted(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\nconst CONTROLLER_FILE_SUFFIXES = [\n '.ts',\n '.js',\n '.controller.ts',\n '.controller.js',\n] as const;\n\nconst CONTROLLER_INDEX_CANDIDATES = [\n 'index.ts',\n 'index.js',\n] as const;\n\nfunction resolveControllerFile(controllerRef: string, componentDir: string): string | null {\n const normalized = controllerRef.trim();\n const basePath = resolveFromComponentDir(componentDir, normalized);\n\n if (existsSync(basePath)) {\n return basePath;\n }\n\n if (path.extname(basePath)) {\n return null;\n }\n\n for (const suffix of CONTROLLER_FILE_SUFFIXES) {\n const candidate = basePath + suffix;\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n for (const indexFile of CONTROLLER_INDEX_CANDIDATES) {\n const candidate = path.resolve(basePath, indexFile);\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n/**\n * The companion data loader of a view/layout (`pages/chat.svelte` →\n * `pages/chat.load.ts`), discovered at build. None is fine (`null`); more\n * than one companion is ambiguous and fails the build.\n */\nfunction resolveLoaderFile(viewPath: string, componentDir: string): string | null {\n const present = loaderCompanionCandidates(viewPath)\n .map((candidate) => resolveFromComponentDir(componentDir, candidate))\n .filter((candidate) => existsSync(candidate));\n if (present.length > 1) {\n throw new Error(\n `Renderer app codegen failed: ambiguous data loader for \"${viewPath}\": ${present.join(', ')}. ` +\n 'Keep exactly one .load companion.',\n );\n }\n return present[0] ?? null;\n}\n\nfunction resolveDefaultViewKey(sortedViews: ViewDef[], entryViewKey?: string): string {\n if (sortedViews.length === 0) return '';\n\n const configuredEntry = entryViewKey?.trim();\n if (!configuredEntry) {\n return viewKey(sortedViews[0]);\n }\n\n const availableKeys = sortedViews.map((view) => viewKey(view));\n if (availableKeys.includes(configuredEntry)) {\n return configuredEntry;\n }\n\n throw new Error(\n `Renderer entry \"${configuredEntry}\" was not found in parsed views. Available view keys: ${availableKeys.join(', ')}`,\n );\n}\n\nexport function generateRendererApp(\n views: ViewDef[],\n options: RendererAppGeneratorOptions,\n): string {\n const sortedViews = [...views].sort((a, b) => viewKey(a).localeCompare(viewKey(b)));\n const defaultViewKey = resolveDefaultViewKey(sortedViews, options.entryViewKey);\n\n const importLines: string[] = [];\n const viewIdentifiers = new Map<string, string>();\n const layoutIdentifiers = new Map<string, string>();\n const controllerIdentifiers = new Map<string, { identifier: string; importPath: string }>();\n /** view/layout path → generated loader identifier (only when a companion exists). */\n const loaderIdentifiers = new Map<string, string>();\n const usedNames = new Set<string>();\n\n const allocateName = (prefix: string, filePath: string): string => {\n const base = toIdentifier(prefix, filePath);\n let name = base;\n let suffix = 2;\n while (usedNames.has(name)) {\n name = `${base}${suffix}`;\n suffix += 1;\n }\n usedNames.add(name);\n return name;\n };\n\n const ensureControllerImported = (controllerRef: string, owningViewKey: string): void => {\n if (controllerIdentifiers.has(controllerRef)) return;\n\n const resolvedControllerFile = resolveControllerFile(controllerRef, options.componentDir);\n if (!resolvedControllerFile) {\n throw new Error(\n `Renderer app codegen failed: controller \"${controllerRef}\" for view \"${owningViewKey}\" was not found. ` +\n 'Controller must point to an existing file with a default exported class.',\n );\n }\n\n const identifier = allocateName('PageController', controllerRef);\n const importPath = computeImportPath(options.outDir, resolvedControllerFile);\n controllerIdentifiers.set(controllerRef, { identifier, importPath });\n importLines.push(`import ${identifier} from '${importPath}';`);\n };\n\n // Build discovers and IMPORTS companion loaders; none runs until a page\n // opens (createControllerSet executes them lazily per navigation).\n const ensureLoaderImported = (viewPath: string): void => {\n if (loaderIdentifiers.has(viewPath)) return;\n const loaderFile = resolveLoaderFile(viewPath, options.componentDir);\n if (!loaderFile) return;\n const identifier = allocateName('Loader', viewPath);\n loaderIdentifiers.set(viewPath, identifier);\n importLines.push(`import ${identifier} from '${computeImportPath(options.outDir, loaderFile)}';`);\n };\n\n for (const view of sortedViews) {\n if (!viewIdentifiers.has(view.viewPath)) {\n const identifier = allocateName('View', view.viewPath);\n viewIdentifiers.set(view.viewPath, identifier);\n const abs = resolveFromComponentDir(options.componentDir, view.viewPath);\n const importPath = computeImportPath(options.outDir, abs);\n importLines.push(`import ${identifier} from '${importPath}';`);\n }\n ensureLoaderImported(view.viewPath);\n\n for (const layoutPath of view.layouts) {\n ensureLoaderImported(layoutPath);\n if (layoutIdentifiers.has(layoutPath)) continue;\n const identifier = allocateName('Layout', layoutPath);\n layoutIdentifiers.set(layoutPath, identifier);\n const abs = resolveFromComponentDir(options.componentDir, layoutPath);\n const importPath = computeImportPath(options.outDir, abs);\n importLines.push(`import ${identifier} from '${importPath}';`);\n }\n\n if (view.controller) {\n ensureControllerImported(view.controller, viewKey(view));\n }\n\n for (const layoutController of view.layoutControllers ?? []) {\n if (!layoutController) continue;\n ensureControllerImported(layoutController, viewKey(view));\n }\n }\n\n const manifestEntries = sortedViews.map((view) => {\n const key = viewKey(view);\n const viewIdentifier = viewIdentifiers.get(view.viewPath)!;\n const layoutList = view.layouts.map((layoutPath) => layoutIdentifiers.get(layoutPath)!);\n const layoutLoaders = view.layouts.map((layoutPath) => loaderIdentifiers.get(layoutPath) ?? 'null');\n const viewLoader = loaderIdentifiers.get(view.viewPath) ?? 'null';\n const controllerPath = view.controller ? `'${escapeSingleQuoted(view.controller)}'` : 'undefined';\n const controllerClass = view.controller\n ? controllerIdentifiers.get(view.controller)!.identifier\n : 'undefined';\n const layoutControllerRefs = view.layoutControllers;\n if (layoutControllerRefs && layoutControllerRefs.length !== view.layouts.length) {\n throw new Error(\n `Renderer app codegen failed: layout controller count for view \"${key}\" does not match layout count.`,\n );\n }\n const normalizedLayoutControllerRefs = layoutControllerRefs ?? Array.from(\n { length: view.layouts.length },\n () => undefined,\n );\n const layoutControllerPaths = `[${normalizedLayoutControllerRefs\n .map((controllerRef) => (controllerRef ? `'${escapeSingleQuoted(controllerRef)}'` : 'undefined'))\n .join(', ')}]`;\n const layoutControllerClasses = `[${normalizedLayoutControllerRefs\n .map((controllerRef) => (controllerRef ? controllerIdentifiers.get(controllerRef)!.identifier : 'undefined'))\n .join(', ')}]`;\n const layoutPreserve = view.layoutPreserve ?? Array.from(\n { length: view.layouts.length },\n () => false,\n );\n if (layoutPreserve.length !== view.layouts.length) {\n throw new Error(\n `Renderer app codegen failed: layout preserve count for view \"${key}\" does not match layout count.`,\n );\n }\n\n return ` '${escapeSingleQuoted(key)}': {\n key: '${escapeSingleQuoted(key)}',\n windowName: '${escapeSingleQuoted(view.windowName)}',\n pageName: '${escapeSingleQuoted(view.pageName)}',\n viewPath: '${escapeSingleQuoted(view.viewPath)}',\n layoutPaths: ${JSON.stringify(view.layouts)},\n layoutControllers: ${layoutControllerPaths},\n layoutPreserve: ${JSON.stringify(layoutPreserve)},\n controller: ${controllerPath},\n layoutControllerClasses: ${layoutControllerClasses},\n controllerClass: ${controllerClass},\n view: ${viewIdentifier},\n layouts: [${layoutList.join(', ')}],\n viewLoader: ${viewLoader},\n layoutLoaders: [${layoutLoaders.join(', ')}],\n },`;\n });\n\n return `${generatedFileNotice(RENDERER_APP_SOURCE_INSTRUCTIONS)}\nimport { mount } from 'svelte';\nimport { App, getApp, getContainer } from '@noego/wood/client';\nimport { Navigation, getNavigation, readNavigationHistory, installAutomationIntrospection } from '@noego/wood/navigation';\nimport { createLoaderRequest, initializeArgument, initializeController, runViewLoaders } from '@noego/wood/navigation';\nimport { Ipc, getIpc } from '@noego/wood/ipc';\nimport NavigationShell from '@noego/wood/navigation-shell';\n${importLines.join('\\n')}\n\ninterface GeneratedAppViewEntry {\n key: string;\n windowName: string;\n pageName: string;\n viewPath: string;\n layoutPaths: string[];\n layoutControllers: Array<string | undefined>;\n layoutPreserve: boolean[];\n controller?: string;\n layoutControllerClasses: Array<any | undefined>;\n controllerClass?: any;\n view: any;\n layouts: any[];\n /** Companion data loader of the view (\\`pages/x.svelte\\` → \\`pages/x.load.ts\\`), discovered at build. */\n viewLoader: ((request: any) => unknown) | null;\n /** Index-aligned companion loaders of the layout stack. */\n layoutLoaders: Array<((request: any) => unknown) | null>;\n}\n\ntype RouteInput = { params: Record<string, string>; query: Record<string, string> };\n\nconst VIEW_MANIFEST: Record<string, GeneratedAppViewEntry> = {\n${manifestEntries.join('\\n')}\n};\n\nfunction getViewManifestEntry(viewKey: string): GeneratedAppViewEntry | null {\n return VIEW_MANIFEST[viewKey] ?? null;\n}\n\ninterface WoodScopedContainer {\n get(token: any): unknown | Promise<unknown>;\n registerClass?(token: any): void;\n registerFunction?(\n token: any,\n resolver: (...args: any[]) => unknown,\n options?: Record<string, unknown>,\n ): void;\n}\n\ninterface WoodContainer extends WoodScopedContainer {\n extend?(): WoodScopedContainer;\n}\n\nfunction createScopedContainer(): WoodScopedContainer {\n const container = getContainer() as WoodContainer;\n if (typeof container.extend !== 'function') {\n throw new Error('Wood client container does not support extend().');\n }\n\n const scopedContainer = container.extend();\n\n if (!scopedContainer || typeof scopedContainer.get !== 'function') {\n throw new Error('Wood client container does not support get().');\n }\n\n // Renderer builtins belong to the root created by getContainer().\n // Page scopes inherit them; re-registering resolved ancestor singletons is invalid.\n\n return scopedContainer as WoodScopedContainer;\n}\n\nasync function resolveControllerInScope(\n scope: WoodScopedContainer,\n entry: GeneratedAppViewEntry,\n ControllerClass: any,\n contextLabel: string,\n): Promise<unknown> {\n if (typeof ControllerClass !== 'function') {\n throw new Error(\n \\`Controller for \\${contextLabel} in view \"\\${entry.key}\" is invalid. Expected a default exported class.\\`,\n );\n }\n\n if (typeof scope.registerClass === 'function') {\n scope.registerClass(ControllerClass);\n }\n\n return await scope.get(ControllerClass);\n}\n\ntype RollbackStep = () => unknown | Promise<unknown>;\n\nasync function disposeScope(scope: { dispose?: () => Promise<void> } | null): Promise<void> {\n if (!scope || typeof scope.dispose !== 'function') return;\n await scope.dispose();\n}\n\n/**\n * Roll back after \\`primary\\` failed: every step is attempted exactly once and\n * every cleanup failure is retained as its original error object next to the\n * primary failure (\\`errors\\`, with \\`cause\\` = primary). Never swallows.\n */\nasync function failWithRollback(primary: unknown, steps: RollbackStep[], message: string): Promise<never> {\n const errors: unknown[] = [primary];\n for (const step of steps) {\n try { await step(); } catch (error) { errors.push(error); }\n }\n if (errors.length === 1) throw primary;\n throw Object.assign(new Error(message), { errors, cause: primary });\n}\n\n/**\n * One page scope per view lifecycle: the view controller and every\n * non-preservable layout controller resolve from the SAME scope, so Scoped\n * services/state are shared within the page (the NoEgo renderer model).\n * Layouts marked preservable in the manifest own dedicated scopes that\n * survive page transitions and dispose when the layout finally leaves.\n *\n * Data loaders (shared orchestration) run first for this navigation —\n * non-preserved layouts in stack order, then the view — and each\n * controller's optional \\`initialize({ params, query[, data] })\\` receives\n * its loader's data. A loader failure disposes the scopes created here.\n */\nasync function createControllerSet(\n entry: GeneratedAppViewEntry,\n preservedDepth = 0,\n route: RouteInput = { params: {}, query: {} },\n): Promise<{\n controller: unknown;\n layoutControllers: Array<unknown>;\n pageScope: WoodScopedContainer;\n layoutScopes: Array<WoodScopedContainer | null>;\n}> {\n const pageScope = createScopedContainer();\n const layoutControllers: unknown[] = [];\n const layoutScopes: Array<WoodScopedContainer | null> = [];\n const createdLayoutScopes: WoodScopedContainer[] = [];\n\n try {\n const loaded = await runViewLoaders(\n { layouts: entry.layoutLoaders, view: entry.viewLoader },\n createLoaderRequest(entry.key, route),\n preservedDepth,\n );\n\n for (let i = 0; i < preservedDepth; i += 1) {\n layoutControllers.push(undefined);\n layoutScopes.push(null);\n }\n\n for (let i = preservedDepth; i < entry.layoutControllerClasses.length; i += 1) {\n const LayoutController = entry.layoutControllerClasses[i];\n if (!LayoutController) {\n layoutControllers.push(undefined);\n layoutScopes.push(null);\n continue;\n }\n const preservable = entry.layoutPreserve?.[i] === true;\n let scope = pageScope;\n if (preservable) {\n scope = createScopedContainer();\n createdLayoutScopes.push(scope);\n }\n const controller = await resolveControllerInScope(\n scope,\n entry,\n LayoutController,\n \\`layout[\\${i}]\\`,\n );\n await initializeController(controller, initializeArgument(route, loaded.layouts[i] ?? null));\n layoutControllers.push(controller);\n layoutScopes.push(preservable ? scope : null);\n }\n\n const controller = entry.controllerClass\n ? await resolveControllerInScope(pageScope, entry, entry.controllerClass, 'page')\n : undefined;\n if (controller !== undefined) {\n await initializeController(controller, initializeArgument(route, loaded.view));\n }\n\n return { controller, layoutControllers, pageScope, layoutScopes };\n } catch (error) {\n // Failed transitions must not orphan scopes: every scope this set created\n // is disposed once, and every disposal failure is retained with the\n // original initialization failure instead of being swallowed.\n return failWithRollback(error, [\n ...createdLayoutScopes.map((scope) => () => disposeScope(scope as any)),\n () => disposeScope(pageScope as any),\n ], 'Wood controller-set rollback failed');\n }\n}\n\nconst DEFAULT_VIEW_KEY = '${escapeSingleQuoted(defaultViewKey)}';\n\ntype WoodWindowBootstrapInfo = {\n ok?: boolean;\n windowId?: string;\n defaultRoute?: string;\n};\n\ntype WoodBootstrapBridgeLike = {\n __window?: {\n current?: () => Promise<WoodWindowBootstrapInfo>;\n };\n};\n\nfunction getWoodBridge(): WoodBootstrapBridgeLike | null {\n const globalObj = globalThis as typeof globalThis & {\n __NOEGO_WOOD_BRIDGE__?: unknown;\n window?: {\n __NOEGO_WOOD_BRIDGE__?: unknown;\n };\n };\n\n const bridge = globalObj.__NOEGO_WOOD_BRIDGE__ ?? globalObj.window?.__NOEGO_WOOD_BRIDGE__;\n if (!bridge || typeof bridge !== 'object') {\n return null;\n }\n\n return bridge as WoodBootstrapBridgeLike;\n}\n\nasync function resolveInitialEntry(route: RouteInput): Promise<{ entry: GeneratedAppViewEntry; route: RouteInput }> {\n if (!DEFAULT_VIEW_KEY) {\n throw new Error('No views available in renderer manifest. Define at least one view in renderer OpenAPI YAML.');\n }\n\n const bridge = getWoodBridge();\n const bootstrapWindow = await bridge?.__window?.current?.();\n const requestedRoute = bootstrapWindow?.defaultRoute;\n\n const routeEntry = requestedRoute ? getViewManifestEntry(requestedRoute) : null;\n const entry = routeEntry ?? getViewManifestEntry(DEFAULT_VIEW_KEY);\n if (!entry) {\n throw new Error(\\`Default view \"\\${DEFAULT_VIEW_KEY}\" is missing from generated view manifest.\\`);\n }\n\n // Reload restores only a valid manifest route owned by this window. Invalid\n // or foreign history falls back to the host's original bootstrap unchanged.\n const saved = readNavigationHistory(typeof window === 'undefined' ? null : window.history.state);\n const savedEntry = saved ? getViewManifestEntry(saved.page) : null;\n if (saved && savedEntry && savedEntry.windowName === (bootstrapWindow?.windowId ?? entry.windowName)) {\n return {entry: savedEntry, route: {params: saved.params, query: saved.query}};\n }\n return {entry, route};\n}\n\nfunction isAutomationBuild(): boolean {\n try {\n const env = (import.meta as { env?: { DEV?: boolean; MODE?: string } }).env;\n return Boolean(env?.DEV) || env?.MODE === 'test';\n } catch {\n return false;\n }\n}\n\nexport const supportsInitialRoute = true;\nexport async function app(target: Element, initialRoute: RouteInput = { params: {}, query: {} }) {\n const {entry, route} = await resolveInitialEntry(initialRoute);\n\n getNavigation().replace(entry.key, route.params, route.query);\n\n // Dev/test-only Axe introspection surface; never present in production builds.\n if (isAutomationBuild()) {\n const bootstrapWindow = await getWoodBridge()?.__window?.current?.();\n installAutomationIntrospection({ windowId: bootstrapWindow?.windowId ?? null });\n }\n const initialSet = await createControllerSet(entry, 0, route);\n\n try {\n return mount(NavigationShell, {\n target,\n props: {\n manifest: VIEW_MANIFEST,\n initialEntry: entry,\n initialRoute: route,\n initialControllerSet: initialSet,\n createControllerSetFn: createControllerSet,\n getViewManifestEntryFn: getViewManifestEntry,\n },\n });\n } catch (error) {\n // A failed initial mount must not orphan the initial controller set: the\n // shell never took ownership, so its scopes are disposed here once.\n return failWithRollback(error, [\n ...initialSet.layoutScopes.map((scope) => () => disposeScope(scope as any)),\n () => disposeScope(initialSet.pageScope as any),\n ], 'Wood initial mount rollback failed');\n }\n}\n`;\n}\n"],"mappings":"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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAiKA,mBAAmB,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmG9D;","names":[]}
@@ -32,6 +32,7 @@ __export(navigation_exports, {
32
32
  initializeArgument: () => import_loader_orchestration.initializeArgument,
33
33
  initializeController: () => import_loader_orchestration.initializeController,
34
34
  installAutomationIntrospection: () => import_automation_introspection.installAutomationIntrospection,
35
+ readNavigationHistory: () => readNavigationHistory,
35
36
  runViewLoaders: () => import_loader_orchestration.runViewLoaders
36
37
  });
37
38
  module.exports = __toCommonJS(navigation_exports);
@@ -41,6 +42,18 @@ var import_layout_preservation = require("./layout_preservation.cjs");
41
42
  var import_loader_orchestration = require("../loader/loader_orchestration.cjs");
42
43
  const NAVIGATION_SYMBOL = /* @__PURE__ */ Symbol.for("@noego/wood.navigation");
43
44
  const HISTORY_STATE_MARKER = "__wood_navigation_state__";
45
+ function readNavigationHistory(state) {
46
+ if (!state || typeof state !== "object" || Array.isArray(state)) return null;
47
+ const value = state;
48
+ if (value[HISTORY_STATE_MARKER] !== true || typeof value.page !== "string" || !value.page.trim()) return null;
49
+ const record = (input) => {
50
+ if (!input || typeof input !== "object" || Array.isArray(input)) return false;
51
+ const prototype = Object.getPrototypeOf(input);
52
+ return (prototype === Object.prototype || prototype === null) && Object.values(input).every((item) => typeof item === "string");
53
+ };
54
+ if (!record(value.params) || !record(value.query)) return null;
55
+ return { page: value.page, params: { ...value.params }, query: { ...value.query } };
56
+ }
44
57
  class Navigation {
45
58
  constructor() {
46
59
  this.entries = [];
@@ -226,6 +239,7 @@ function clearNavigationForTests() {
226
239
  initializeArgument,
227
240
  initializeController,
228
241
  installAutomationIntrospection,
242
+ readNavigationHistory,
229
243
  runViewLoaders
230
244
  });
231
245
  //# sourceMappingURL=index.cjs.map
@@ -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\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// Shared page/layout data-loader orchestration for the generated renderer app.\nexport { createLoaderRequest, initializeArgument, initializeController, runViewLoaders, assertDataLoader, NO_LOADERS } from '../loader/loader_orchestration.cjs';\nexport type { DataLoader, LoadedData, RouteInput, ViewLoaderResult, ViewLoaderSet } from '../loader/loader_orchestration.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\n/** Validate persisted route data before a new document uses it for bootstrap. */\nexport function readNavigationHistory(state: unknown): Pick<NavigationEntry, 'page' | 'params' | 'query'> | null {\n if (!state || typeof state !== 'object' || Array.isArray(state)) return null;\n const value = state as Record<string, unknown>;\n if (value[HISTORY_STATE_MARKER] !== true || typeof value.page !== 'string' || !value.page.trim()) return null;\n const record = (input: unknown): input is Record<string, string> => {\n if (!input || typeof input !== 'object' || Array.isArray(input)) return false;\n const prototype = Object.getPrototypeOf(input);\n return (prototype === Object.prototype || prototype === null) && Object.values(input).every(item => typeof item === 'string');\n };\n if (!record(value.params) || !record(value.query)) return null;\n return {page: value.page, params: {...value.params}, query: {...value.query}};\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// Shared page/layout data-loader orchestration for the generated renderer app.\nexport { createLoaderRequest, initializeArgument, initializeController, runViewLoaders, assertDataLoader, NO_LOADERS } from '../loader/loader_orchestration.cjs';\nexport type { DataLoader, LoadedData, RouteInput, ViewLoaderResult, ViewLoaderSet } from '../loader/loader_orchestration.cjs';\n"],"mappings":"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","names":[]}
@@ -11,6 +11,8 @@ interface NavigationEntry {
11
11
  options: NavigationOptions;
12
12
  }
13
13
 
14
+ /** Validate persisted route data before a new document uses it for bootstrap. */
15
+ declare function readNavigationHistory(state: unknown): Pick<NavigationEntry, 'page' | 'params' | 'query'> | null;
14
16
  type NavigationListener = (entry: NavigationEntry | null) => void;
15
17
  declare class Navigation {
16
18
  private entries;
@@ -39,4 +41,4 @@ declare function goback(): NavigationEntry | null;
39
41
  /** Test-only helper to clear the navigation singleton. */
40
42
  declare function clearNavigationForTests(): void;
41
43
 
42
- export { Navigation, type NavigationEntry, NavigationOptions, clearNavigationForTests, getNavigation, go, goback, goto };
44
+ export { Navigation, type NavigationEntry, NavigationOptions, clearNavigationForTests, getNavigation, go, goback, goto, readNavigationHistory };
@@ -11,6 +11,8 @@ interface NavigationEntry {
11
11
  options: NavigationOptions;
12
12
  }
13
13
 
14
+ /** Validate persisted route data before a new document uses it for bootstrap. */
15
+ declare function readNavigationHistory(state: unknown): Pick<NavigationEntry, 'page' | 'params' | 'query'> | null;
14
16
  type NavigationListener = (entry: NavigationEntry | null) => void;
15
17
  declare class Navigation {
16
18
  private entries;
@@ -39,4 +41,4 @@ declare function goback(): NavigationEntry | null;
39
41
  /** Test-only helper to clear the navigation singleton. */
40
42
  declare function clearNavigationForTests(): void;
41
43
 
42
- export { Navigation, type NavigationEntry, NavigationOptions, clearNavigationForTests, getNavigation, go, goback, goto };
44
+ export { Navigation, type NavigationEntry, NavigationOptions, clearNavigationForTests, getNavigation, go, goback, goto, readNavigationHistory };
@@ -1,6 +1,18 @@
1
1
  import { emitRendererTrace } from "../tracing/index.js";
2
2
  const NAVIGATION_SYMBOL = /* @__PURE__ */ Symbol.for("@noego/wood.navigation");
3
3
  const HISTORY_STATE_MARKER = "__wood_navigation_state__";
4
+ function readNavigationHistory(state) {
5
+ if (!state || typeof state !== "object" || Array.isArray(state)) return null;
6
+ const value = state;
7
+ if (value[HISTORY_STATE_MARKER] !== true || typeof value.page !== "string" || !value.page.trim()) return null;
8
+ const record = (input) => {
9
+ if (!input || typeof input !== "object" || Array.isArray(input)) return false;
10
+ const prototype = Object.getPrototypeOf(input);
11
+ return (prototype === Object.prototype || prototype === null) && Object.values(input).every((item) => typeof item === "string");
12
+ };
13
+ if (!record(value.params) || !record(value.query)) return null;
14
+ return { page: value.page, params: { ...value.params }, query: { ...value.query } };
15
+ }
4
16
  class Navigation {
5
17
  constructor() {
6
18
  this.entries = [];
@@ -188,6 +200,7 @@ export {
188
200
  initializeArgument,
189
201
  initializeController,
190
202
  installAutomationIntrospection,
203
+ readNavigationHistory,
191
204
  runViewLoaders
192
205
  };
193
206
  //# sourceMappingURL=index.js.map