Repository: vercel/next.js
Pull request: Attach active src route to next global

## Summary
- expose the active app source page on `window.next.__internal_src_page`
- derive the source page from the client router tree instead of sending it via `InitialRSCPayload`
- ensure the value updates on client transitions (including dynamic routes and route groups)
- extend app-dir e2e coverage for initial load and transition behavior

## Testing
- `pnpm test-dev test/e2e/app-dir/app/index.test.ts -t "__internal_src_page"`
- `pnpm --filter=next types`


Changed files:
filename: packages/next/src/client/components/app-router.tsx
patch: @@ -31,7 +31,10 @@ import { findHeadInCache } from './router-reducer/reducers/find-head-in-cache'
 import { unresolvedThenable } from './unresolved-thenable'
 import { removeBasePath } from '../remove-base-path'
 import { hasBasePath } from '../has-base-path'
-import { getSelectedParams } from './router-reducer/compute-changed-path'
+import {
+  extractSourcePageFromFlightRouterState,
+  getSelectedParams,
+} from './router-reducer/compute-changed-path'
 import { useNavFailureHandler } from './nav-failure-handler'
 import {
   dispatchTraverseAction,
@@ -191,6 +194,16 @@ function Router({
     }, [cache, tree])
   }
 
+  useEffect(() => {
+    const sourcePage = extractSourcePageFromFlightRouterState(state.tree)
+
+    if (sourcePage !== undefined) {
+      window.next.__internal_src_page = sourcePage
+    } else {
+      delete window.next.__internal_src_page
+    }
+  }, [state.tree])
+
   useEffect(() => {
     // If the app is restored from bfcache, it's possible that
     // pushRef.mpaNavigation is true, which would mean that any re-render of this component

filename: packages/next/src/client/components/router-reducer/compute-changed-path.ts
patch: @@ -27,6 +27,44 @@ const segmentToPathname = (segment: Segment): string => {
   return segment[1]
 }
 
+const segmentToSourcePagePathname = (segment: Segment): string => {
+  if (typeof segment === 'string') {
+    if (segment === 'children') return ''
+    if (segment.startsWith(PAGE_SEGMENT_KEY)) return 'page'
+    return segment
+  }
+
+  const [paramName, , dynamicParamType] = segment
+
+  switch (dynamicParamType) {
+    case 'c':
+      return `[...${paramName}]`
+    case 'ci(..)(..)':
+      return `(..)(..)[...${paramName}]`
+    case 'ci(.)':
+      return `(.)[...${paramName}]`
+    case 'ci(..)':
+      return `(..)[...${paramName}]`
+    case 'ci(...)':
+      return `(...)[...${paramName}]`
+    case 'oc':
+      return `[[...${paramName}]]`
+    case 'd':
+      return `[${paramName}]`
+    case 'di(..)(..)':
+      return `(..)(..)[${paramName}]`
+    case 'di(.)':
+      return `(.)[${paramName}]`
+    case 'di(..)':
+      return `(..)[${paramName}]`
+    case 'di(...)':
+      return `(...)[${paramName}]`
+    default:
+      dynamicParamType satisfies never
+      return `[${paramName}]`
+  }
+}
+
 function normalizeSegments(segments: string[]): string {
   return (
     segments.reduce((acc, segment) => {
@@ -79,6 +117,55 @@ export function extractPathFromFlightRouterState(
   return normalizeSegments(segments)
 }
 
+function extractSourcePageSegmentsFromFlightRouterState(
+  flightRouterState: FlightRouterState
+): string[] | undefined {
+  const segment = segmentToSourcePagePathname(flightRouterState[0])
+
+  if (segment === DEFAULT_SEGMENT_KEY) {
+    return undefined
+  }
+
+  if (segment === 'page') {
+    return [segment]
+  }
+
+  const parallelRoutes = flightRouterState[1] ?? {}
+
+  const childrenPath = parallelRoutes.children
+    ? extractSourcePageSegmentsFromFlightRouterState(parallelRoutes.children)
+    : undefined
+
+  if (childrenPath !== undefined) {
+    return segment === ''
+      ? childrenPath
+      : [removeLeadingSlash(segment), ...childrenPath]
+  }
+
+  for (const [key, value] of Object.entries(parallelRoutes)) {
+    if (key === 'children') continue
+
+    const childPath = extractSourcePageSegmentsFromFlightRouterState(value)
+
+    if (childPath !== undefined) {
+      return segment === ''
+        ? childPath
+        : [removeLeadingSlash(segment), ...childPath]
+    }
+  }
+
+  return undefined
+}
+
+export function extractSourcePageFromFlightRouterState(
+  flightRouterState: FlightRouterState
+): string | undefined {
+  const sourcePageSegments =
+    extractSourcePageSegmentsFromFlightRouterState(flightRouterState)
+
+  return sourcePageSegments ? `/${sourcePageSegments.join('/')}` : undefined
+}
+
 function computeChangedPathImpl(
   treeA: FlightRouterState,
   treeB: FlightRouterState

filename: test/e2e/app-dir/app/index.test.ts
patch: @@ -145,6 +145,56 @@ describe('app dir - basic', () => {
     })
   })
 
+  it.each([
+    {
+      path: '/dashboard',
+      srcPage: '/dashboard/page',
+    },
+    {
+      path: '/dynamic/category-1/id-2',
+      srcPage: '/dynamic/[category]/[id]/page',
+    },
+    {
+      path: '/dashboard/another',
+      srcPage: '/(newroot)/dashboard/another/page',
+    },
+  ])(
+    'should expose app source page on window.next.__internal_src_page for $path',
+    async ({ path, srcPage }) => {
+      const browser = await next.browser(path)
+
+      await retry(async () => {
+        expect(await browser.eval('window.next.__internal_src_page')).toBe(
+          srcPage
+        )
+      })
+    }
+  )
+
+  it('should update window.next.__internal_src_page on app router transitions', async () => {
+    const browser = await next.browser('/dashboard')
+
+    await retry(async () => {
+      expect(await browser.eval('window.next.__internal_src_page')).toBe(
+        '/dashboard/page'
+      )
+    })
+
+    await browser.eval(`window.next.router.push('/dynamic/category-1/id-2')`)
+    await retry(async () => {
+      expect(await browser.eval('window.next.__internal_src_page')).toBe(
+        '/dynamic/[category]/[id]/page'
+      )
+    })
+
+    await browser.eval(`window.next.router.push('/dashboard/another')`)
+    await retry(async () => {
+      expect(await browser.eval('window.next.__internal_src_page')).toBe(
+        '/(newroot)/dashboard/another/page'
+      )
+    })
+  })
+
   if (!isNextDev) {
     it('should successfully detect app route during prefetch', async () => {
       const browser = await next.browser('/')