Repository: vercel/next.js
Pull request: [segment explorer] fix soft navigation case

Adding `remove` function for trie in segment explorer to address the trie updates for soft navigation case to ensure the nodes from previous page is removed after nav.


Changed files:
filename: packages/next/src/next-devtools/dev-overlay/components/overview/segment-explorer.tsx
patch: @@ -80,6 +80,8 @@ function PageSegmentTreeLayerPresentation({
                   {folderName && (
                     <span className="segment-explorer-filename--path">
                       {folderName}
+                      {/* hidden slashes for testing snapshots */}
+                      <small>{'/'}</small>
                     </span>
                   )}
                   <span className="segment-explorer-filename--name">
@@ -151,6 +153,9 @@ export const DEV_TOOLS_INFO_RENDER_FILES_STYLES = css`
   .segment-explorer-filename--path {
     margin-right: 8px;
   }
+  .segment-explorer-filename--path small {
+    width: 0;
+  }
   .segment-explorer-filename--name {
     color: var(--color-gray-800);
   }

filename: packages/next/src/next-devtools/dev-overlay/segment-explorer.test.tsx
patch: @@ -75,7 +75,7 @@ describe('Segment Explorer', () => {
     })
   })
 
-  test.failing('remove node in the middle', () => {
+  test('remove node in the middle', () => {
     insertSegmentNode({ pagePath: '/a/b/@sidebar/page.js', type: 'page' })
     insertSegmentNode({ pagePath: '/a/b/page.js', type: 'page' })
     insertSegmentNode({ pagePath: '/a/b/layout.js', type: 'layout' })

filename: packages/next/src/next-devtools/dev-overlay/segment-explorer.ts
patch: @@ -55,9 +55,11 @@ const createSegmentTreeStore = (): {
 const { subscribe, getSnapshot, getServerSnapshot } = createSegmentTreeStore()
 
 function createTrie<Value = string>({
-  getKey = (k) => k as unknown as string,
+  getCharacters = (item: Value) => [item] as string[],
+  compare = (a: Value | undefined, b: Value | undefined) => a === b,
 }: {
-  getKey: (k: Value) => string
+  getCharacters?: (item: Value) => string[]
+  compare?: (a: Value | undefined, b: Value | undefined) => boolean
 }): Trie<Value> {
   const root: TrieNode<Value> = {
     value: undefined,
@@ -72,8 +74,7 @@ function createTrie<Value = string>({
 
   function insert(value: Value) {
     let currentNode = root
-    const key = getKey(value)
-    const segments = key.split('/')
+    const segments = getCharacters(value)
 
     for (const segment of segments) {
       if (!currentNode.children[segment]) {
@@ -91,8 +92,31 @@ function createTrie<Value = string>({
     markUpdated()
   }
 
-  function remove(_: Value) {
-    // TODO Implement remove functionality
+  function remove(value: Value) {
+    let currentNode = root
+    const segments = getCharacters(value)
+    const stack: TrieNode<Value>[] = []
+    let found = true
+    for (const segment of segments) {
+      if (!currentNode.children[segment]) {
+        found = false
+        break
+      }
+      stack.push(currentNode)
+      currentNode = currentNode.children[segment]!
+    }
+    // If the value is not found, skip removal
+    if (!found || !compare(currentNode.value, value)) {
+      return
+    }
+    currentNode.value = undefined
+    for (let i = stack.length - 1; i >= 0; i--) {
+      const parentNode = stack[i]
+      const segment = segments[i]
+      if (Object.keys(parentNode.children[segment]!.children).length === 0) {
+        delete parentNode.children[segment]
+      }
+    }
 
     markUpdated()
   }
@@ -113,7 +137,11 @@ export type SegmentTrie = Trie<SegmentNode>
 export type SegmentTrieNode = TrieNode<SegmentNode>
 
 const trie: SegmentTrie = createTrie({
-  getKey: (item) => item.pagePath,
+  compare: (a, b) => {
+    if (!a || !b) return false
+    return a.pagePath === b.pagePath && a.type === b.type
+  },
+  getCharacters: (item) => item.pagePath.split('/'),
 })
 export const insertSegmentNode = trie.insert
 export const removeSegmentNode = trie.remove

filename: test/development/app-dir/segment-explorer/segment-explorer.test.ts
patch: @@ -26,44 +26,42 @@ describe('segment-explorer', () => {
   it('should render the segment explorer for parallel routes', async () => {
     const browser = await next.browser('/parallel-routes')
     expect(await getSegmentExplorerContent(browser)).toMatchInlineSnapshot(`
-     "applayout.tsx
-     parallel-routeslayout.tsx
-     parallel-routespage.tsx
-     @barlayout.tsx
-     @barpage.tsx
-     @foolayout.tsx
-     @foopage.tsx"
+     "app/layout.tsx
+     parallel-routes/layout.tsx
+     parallel-routes/page.tsx
+     @bar/layout.tsx
+     @bar/page.tsx
+     @foo/layout.tsx
+     @foo/page.tsx"
     `)
   })
 
   it('should render the segment explorer for nested routes', async () => {
     const browser = await next.browser('/blog/~/grid')
     expect(await getSegmentExplorerContent(browser)).toMatchInlineSnapshot(`
-     "applayout.tsx
-     (v2)layout.tsx
-     (team)layout.tsx
-     (overview)layout.tsx
-     gridpage.tsx"
+     "app/layout.tsx
+     (v2)/layout.tsx
+     (team)/layout.tsx
+     (overview)/layout.tsx
+     grid/page.tsx"
     `)
   })
 
   it('should cleanup on soft navigation', async () => {
     const browser = await next.browser('/soft-navigation/a')
     expect(await getSegmentExplorerContent(browser)).toMatchInlineSnapshot(`
-     "applayout.tsx
-     apage.tsx"
+     "app/layout.tsx
+     a/page.tsx"
     `)
 
     await browser.elementByCss('[href="/soft-navigation/b"]').click()
     await retry(async () => {
       expect(await browser.elementByCss('body').text()).toContain('Page B')
     })
 
-    // FIXME: Should no longer contain /soft-navigation/a/page.js
     expect(await getSegmentExplorerContent(browser)).toMatchInlineSnapshot(`
-     "applayout.tsx
-     apage.tsx
-     bpage.tsx"
+     "app/layout.tsx
+     b/page.tsx"
     `)
   })
 })