From 3f44f2a1e126c068b322130e3f58b1fcbe4f45ab Mon Sep 17 00:00:00 2001 From: erKam <88717125+erkamyaman@users.noreply.github.com> Date: Sun, 11 Oct 2026 06:16:48 +0300 Subject: [PATCH 1/2] fix(analog): keep layout load data on the layout for an index child page Angular passes a parent's resolved data down to an empty-path child, so at /products the index page's data.load was the products layout's load() result. chainOf credited it to the child, and the server then dropped it because the child has no .server.ts. The page view lost the data and the analog-current-page tool told the agent no load data reached the route. The same object seen again is now treated as inherited. --- .../src/__tests__/analog-load-inherit.test.ts | 78 +++++++++++++++++++ packages/devtools/src/analog-runtime.ts | 4 +- 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 packages/devtools/src/__tests__/analog-load-inherit.test.ts diff --git a/packages/devtools/src/__tests__/analog-load-inherit.test.ts b/packages/devtools/src/__tests__/analog-load-inherit.test.ts new file mode 100644 index 00000000..67529e37 --- /dev/null +++ b/packages/devtools/src/__tests__/analog-load-inherit.test.ts @@ -0,0 +1,78 @@ +// @vitest-environment jsdom +import '@angular/compiler'; +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; +import { Router, RouterOutlet, provideRouter, type Route, type Routes } from '@angular/router'; +import { describe, expect, it } from 'vitest'; +import { ANALOG_META_DESCRIPTION, chainOf, collectAnalog } from '../analog-runtime.ts'; +import { scanAnalog } from '../rpc/analog-scan.ts'; +import { resolveAnalogReport } from '../rpc/analog-tools.ts'; +import { BASE_FILES, makeProject } from './analog-fixture.ts'; + +TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); + +const META = Symbol(ANALOG_META_DESCRIPTION); + +function analogRoute(endpoint: string, endpointKey: string, route: Route): Route { + return Object.assign(route, { [META]: { endpoint, endpointKey } }); +} + +class ProductsLayout {} +Component({ imports: [RouterOutlet], template: '' })(ProductsLayout); + +class ProductList {} +Component({ template: '

list

' })(ProductList); + +describe('Analog load() of a layout', () => { + it('stays with the layout when an empty-path child page inherits it', async () => { + const routes: Routes = [ + { + path: 'products', + children: [ + analogRoute('/pages/products', '/src/app/pages/products.server.ts', { + path: '', + component: ProductsLayout, + resolve: { load: () => ({ items: [1, 2] }) }, + children: [ + analogRoute('/pages/products/index', '/src/app/pages/products/index.server.ts', { + path: '', + component: ProductList, + }), + ], + }), + ], + }, + ]; + TestBed.configureTestingModule({ providers: [provideRouter(routes)] }); + const router = TestBed.inject(Router); + await router.navigateByUrl('/products'); + + const { chain, data, loadFrom } = chainOf( + router.routerState.snapshot.root as unknown as Record, + ); + expect(chain.map((c) => c.serverFile)).toEqual([ + '/src/app/pages/products.server.ts', + '/src/app/pages/products/index.server.ts', + ]); + expect(data).toEqual({ items: [1, 2] }); + // The list page has no .server.ts: its `data.load` is the layout's, + // inherited by Angular because its path is empty. + + document.body.innerHTML = ''; + const project = scanAnalog( + makeProject(BASE_FILES, { + 'src/app/pages/products.server.ts': 'export const load = async () => ({ items: [] });\n', + }), + ); + const report = collectAnalog( + { ɵgetRouterInstance: () => router, getInjector: () => null }, + 'p1', + [], + () => ({ hydrated: 0 }), + )!; + expect(report.load?.keys).toEqual(['items']); + expect(resolveAnalogReport(project, report).load?.keys).toEqual(['items']); + expect(loadFrom).toBe(0); + }); +}); diff --git a/packages/devtools/src/analog-runtime.ts b/packages/devtools/src/analog-runtime.ts index b3c44094..af4bb62f 100644 --- a/packages/devtools/src/analog-runtime.ts +++ b/packages/devtools/src/analog-runtime.ts @@ -108,7 +108,9 @@ export function chainOf(root: AnyRecord | null): { if (meta) { chain.push({ path: `/${segments.join('/')}`, ...fileOfEndpoint(meta.endpointKey) }); const load = read(() => node!['data']?.['load'], undefined); - if (load !== undefined && (loadFrom === undefined || !isEmptyObject(load))) { + // An empty-path child inherits its parent's resolved data, so the same + // object again is the parent's load(), not this page's. + if (load !== undefined && load !== data && (loadFrom === undefined || !isEmptyObject(load))) { data = load; loadFrom = chain.length - 1; } From 9d9808e4325b7f3fc80c9953b8b2950538d6811b Mon Sep 17 00:00:00 2001 From: erKam <88717125+erkamyaman@users.noreply.github.com> Date: Sun, 11 Oct 2026 06:32:23 +0300 Subject: [PATCH 2/2] fix(analog): credit a child that resolves its own load to the same object A route that defines its own resolve.load keeps the credit even when it returns the very object its parent resolved, so only a route without its own resolver counts as inheriting. --- .../src/__tests__/analog-load-inherit.test.ts | 31 +++++++++++++++++++ packages/devtools/src/analog-runtime.ts | 6 ++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/devtools/src/__tests__/analog-load-inherit.test.ts b/packages/devtools/src/__tests__/analog-load-inherit.test.ts index 67529e37..4ef2f292 100644 --- a/packages/devtools/src/__tests__/analog-load-inherit.test.ts +++ b/packages/devtools/src/__tests__/analog-load-inherit.test.ts @@ -75,4 +75,35 @@ describe('Analog load() of a layout', () => { expect(resolveAnalogReport(project, report).load?.keys).toEqual(['items']); expect(loadFrom).toBe(0); }); + + it('credits the child when it resolves its own load, even to the same object', async () => { + const cached = { items: [1, 2] }; + const routes: Routes = [ + { + path: 'products', + children: [ + analogRoute('/pages/products', '/src/app/pages/products.server.ts', { + path: '', + component: ProductsLayout, + resolve: { load: () => cached }, + children: [ + analogRoute('/pages/products/index', '/src/app/pages/products/index.server.ts', { + path: '', + component: ProductList, + resolve: { load: () => cached }, + }), + ], + }), + ], + }, + ]; + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ providers: [provideRouter(routes)] }); + const router = TestBed.inject(Router); + await router.navigateByUrl('/products'); + const { loadFrom } = chainOf( + router.routerState.snapshot.root as unknown as Record, + ); + expect(loadFrom).toBe(1); + }); }); diff --git a/packages/devtools/src/analog-runtime.ts b/packages/devtools/src/analog-runtime.ts index af4bb62f..545a10a5 100644 --- a/packages/devtools/src/analog-runtime.ts +++ b/packages/devtools/src/analog-runtime.ts @@ -109,8 +109,10 @@ export function chainOf(root: AnyRecord | null): { chain.push({ path: `/${segments.join('/')}`, ...fileOfEndpoint(meta.endpointKey) }); const load = read(() => node!['data']?.['load'], undefined); // An empty-path child inherits its parent's resolved data, so the same - // object again is the parent's load(), not this page's. - if (load !== undefined && load !== data && (loadFrom === undefined || !isEmptyObject(load))) { + // object again is the parent's load(), unless this route resolves its own. + const own = read(() => !!config?.['resolve']?.['load'], false); + const inherited = load === data && !own; + if (load !== undefined && !inherited && (loadFrom === undefined || !isEmptyObject(load))) { data = load; loadFrom = chain.length - 1; }