diff --git a/packages/next/src/build/define-env.ts b/packages/next/src/build/define-env.ts
index 14593dba7343a..bbb129a933749 100644
--- a/packages/next/src/build/define-env.ts
+++ b/packages/next/src/build/define-env.ts
@@ -279,6 +279,8 @@ export function getDefineEnv({
'process.env.__NEXT_RELATIVE_DIST_DIR': config.distDir,
}
: {}),
+ 'process.env.__NEXT_DEVTOOL_SEGMENT_EXPLORER':
+ config.experimental.devtoolSegmentExplorer ?? false,
}
const userDefines = config.compiler?.define ?? {}
diff --git a/packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-indicator.tsx b/packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-indicator.tsx
index 7303498a69c81..1763496a708dc 100644
--- a/packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-indicator.tsx
+++ b/packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-indicator.tsx
@@ -11,6 +11,7 @@ import { TurbopackInfo } from './dev-tools-info/turbopack-info'
import { RouteInfo } from './dev-tools-info/route-info'
import GearIcon from '../../../icons/gear-icon'
import { UserPreferences } from './dev-tools-info/user-preferences'
+import { SegmentsExplorer } from '../../overview/segment-explorer'
import {
MENU_CURVE,
MENU_DURATION_MS,
@@ -80,6 +81,7 @@ const OVERLAYS = {
Turbo: 'turbo',
Route: 'route',
Preferences: 'preferences',
+ SegmentExplorer: 'segment-explorer',
} as const
export type Overlays = (typeof OVERLAYS)[keyof typeof OVERLAYS]
@@ -123,6 +125,7 @@ function DevToolsPopover({
const isTurbopackInfoOpen = open === OVERLAYS.Turbo
const isRouteInfoOpen = open === OVERLAYS.Route
const isPreferencesOpen = open === OVERLAYS.Preferences
+ const isSegmentExplorerOpen = open === OVERLAYS.SegmentExplorer
const { mounted: menuMounted, rendered: menuRendered } = useDelayedRender(
isMenuOpen,
@@ -325,6 +328,16 @@ function DevToolsPopover({
setScale={setScale}
/>
+ {/* Page Segment Explorer */}
+ {process.env.__NEXT_DEVTOOL_SEGMENT_EXPLORER ? (
+
+ ) : null}
+
{/* Dropdown Menu */}
{menuMounted && (
setOpen(OVERLAYS.Preferences)}
index={isTurbopack ? 2 : 3}
/>
+ {process.env.__NEXT_DEVTOOL_SEGMENT_EXPLORER ? (
+ }
+ onClick={() => setOpen(OVERLAYS.SegmentExplorer)}
+ index={isTurbopack ? 3 : 4}
+ />
+ ) : null}
diff --git a/packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-info/dev-tools-info.tsx b/packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-info/dev-tools-info.tsx
index f9c7b53c74268..cab02773e36ca 100644
--- a/packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-info/dev-tools-info.tsx
+++ b/packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-info/dev-tools-info.tsx
@@ -9,9 +9,10 @@ export interface DevToolsInfoPropsCore {
}
export interface DevToolsInfoProps extends DevToolsInfoPropsCore {
- title: string
+ title: React.ReactNode
children: React.ReactNode
learnMoreLink?: string
+ closeButton?: boolean
}
export function DevToolsInfo({
@@ -20,6 +21,7 @@ export function DevToolsInfo({
learnMoreLink,
isOpen,
triggerRef,
+ closeButton = true,
close,
...props
}: DevToolsInfoProps) {
@@ -55,25 +57,29 @@ export function DevToolsInfo({
{title}
{children}
-
+ {(closeButton || learnMoreLink) && (
+
+ {closeButton ? (
+
+ ) : null}
+ {learnMoreLink && (
+
+ Learn More
+
+ )}
+
+ )}
)
@@ -112,6 +118,7 @@ export const DEV_TOOLS_INFO_STYLES = `
.dev-tools-info-container {
padding: 12px;
+ width: 100%;
}
.dev-tools-info-title {
diff --git a/packages/next/src/client/components/react-dev-overlay/ui/components/overview/segment-explorer.tsx b/packages/next/src/client/components/react-dev-overlay/ui/components/overview/segment-explorer.tsx
new file mode 100644
index 0000000000000..9a20acfbfd700
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/ui/components/overview/segment-explorer.tsx
@@ -0,0 +1,204 @@
+import type { HTMLProps } from 'react'
+import { css } from '../../../utils/css'
+import type { DevToolsInfoPropsCore } from '../errors/dev-tools-indicator/dev-tools-info/dev-tools-info'
+import { DevToolsInfo } from '../errors/dev-tools-indicator/dev-tools-info/dev-tools-info'
+import { cx } from '../../utils/cx'
+import { LeftArrow } from '../../icons/left-arrow'
+import {
+ useSegmentTreeClientState,
+ type SegmentNode,
+} from '../../../../../../shared/lib/devtool/app-segment-tree'
+import type { Trie, TrieNode } from '../../../../../../shared/lib/devtool/trie'
+
+const IconLayout = (props: React.SVGProps) => {
+ return (
+
+ )
+}
+
+const IconPage = (props: React.SVGProps) => {
+ return (
+
+ )
+}
+
+const ICONS = {
+ layout: ,
+ page: ,
+}
+
+function PageSegmentTree({ tree }: { tree: Trie | undefined }) {
+ if (!tree) {
+ return null
+ }
+ return (
+
+ )
+}
+
+function PageSegmentTreeLayerPresentation({
+ tree,
+ node,
+ level,
+}: {
+ tree: Trie
+ node: TrieNode
+ level: number
+}) {
+ const segments = node.value?.pagePath?.split('/') || []
+ const fileName = segments[segments.length - 1] || ''
+ const nodeName = node.value?.type
+ const pagePathPrefix = segments.slice(0, -1).join('/')
+
+ return (
+
+ {!fileName || level === 0 ? null : (
+
+
+
+
+ {nodeName === 'layout' ? ICONS.layout : ICONS.page}
+
+ {pagePathPrefix === '' ? '' : `${pagePathPrefix}/`}
+ {fileName}
+
+
+
+ )}
+
+
+ {Object.entries(node.children).map(
+ ([key, child]) =>
+ child && (
+
+ )
+ )}
+
+
+ )
+}
+
+export function SegmentsExplorer(
+ props: DevToolsInfoPropsCore & HTMLProps
+) {
+ const ctx = useSegmentTreeClientState()
+ if (!ctx) {
+ return null
+ }
+
+ return (
+
+
+ {'Segment Explorer'}
+ >
+ }
+ closeButton={false}
+ {...props}
+ >
+
+
+ )
+}
+
+export const DEV_TOOLS_INFO_RENDER_FILES_STYLES = css`
+ .segment-explorer-back-button {
+ margin-right: 12px;
+ color: var(--color-gray-1000);
+ }
+ .segment-explorer-back-button svg {
+ width: 20px;
+ height: 20px;
+ }
+
+ .segment-explorer-content {
+ overflow-y: auto;
+ padding: 0 12px;
+ font-size: var(--size-14);
+ }
+
+ .segment-explorer-item-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 2px 0;
+ }
+
+ .segment-explorer-filename-path {
+ display: inline-block;
+
+ &:hover {
+ color: var(--color-gray-1000);
+ text-decoration: none;
+ }
+ }
+
+ .segment-explorer-filename-path a {
+ color: inherit;
+ text-decoration: inherit;
+ }
+
+ .segment-explorer-line {
+ white-space: pre;
+ }
+
+ .segment-explorer-line-icon {
+ margin-right: 4px;
+ }
+ .segment-explorer-line-icon-page {
+ color: inherit;
+ }
+ .segment-explorer-line-icon-layout {
+ color: var(--color-gray-1-00);
+ }
+
+ .segment-explorer-line-text-page {
+ color: var(--color-blue-900);
+ font-weight: 500;
+ }
+`
diff --git a/packages/next/src/client/components/react-dev-overlay/ui/styles/component-styles.tsx b/packages/next/src/client/components/react-dev-overlay/ui/styles/component-styles.tsx
index 963a51395efee..f6932fe9c3998 100644
--- a/packages/next/src/client/components/react-dev-overlay/ui/styles/component-styles.tsx
+++ b/packages/next/src/client/components/react-dev-overlay/ui/styles/component-styles.tsx
@@ -21,6 +21,7 @@ import { DEV_TOOLS_INFO_STYLES } from '../components/errors/dev-tools-indicator/
import { DEV_TOOLS_INFO_TURBOPACK_INFO_STYLES } from '../components/errors/dev-tools-indicator/dev-tools-info/turbopack-info'
import { DEV_TOOLS_INFO_ROUTE_INFO_STYLES } from '../components/errors/dev-tools-indicator/dev-tools-info/route-info'
import { DEV_TOOLS_INFO_USER_PREFERENCES_STYLES } from '../components/errors/dev-tools-indicator/dev-tools-info/user-preferences'
+import { DEV_TOOLS_INFO_RENDER_FILES_STYLES } from '../components/overview/segment-explorer'
import { FADER_STYLES } from '../components/fader'
export function ComponentStyles() {
@@ -49,6 +50,7 @@ export function ComponentStyles() {
${DEV_TOOLS_INFO_TURBOPACK_INFO_STYLES}
${DEV_TOOLS_INFO_ROUTE_INFO_STYLES}
${DEV_TOOLS_INFO_USER_PREFERENCES_STYLES}
+ ${DEV_TOOLS_INFO_RENDER_FILES_STYLES}
${FADER_STYLES}
`}
diff --git a/packages/next/src/server/app-render/create-component-tree.tsx b/packages/next/src/server/app-render/create-component-tree.tsx
index 7a1cea2b90b90..9c2f4b7a72ac8 100644
--- a/packages/next/src/server/app-render/create-component-tree.tsx
+++ b/packages/next/src/server/app-render/create-component-tree.tsx
@@ -95,6 +95,7 @@ async function createComponentTreeInternal({
renderOpts: { nextConfigOutput, experimental },
workStore,
componentMod: {
+ SegmentViewNode,
HTTPAccessFallbackBoundary,
LayoutRouter,
RenderFromTemplateContext,
@@ -621,8 +622,26 @@ async function createComponentTreeInternal({
)
}
+ const dir = ctx.renderOpts.dir || process.cwd()
+ const isSegmentViewEnabled =
+ process.env.NODE_ENV === 'development' &&
+ ctx.renderOpts.devtoolSegmentExplorer
+ const nodeName = modType ?? 'page'
+
if (isPage) {
- const PageComponent = Component
+ const PageComponent = isSegmentViewEnabled
+ ? (pageProps: any) => {
+ return (
+
+
+
+ )
+ }
+ : Component
+
// Assign searchParams to props if this is a page
let pageElement: React.ReactNode
if (isClientComponent) {
@@ -708,7 +727,18 @@ async function createComponentTreeInternal({
isPossiblyPartialResponse,
]
} else {
- const SegmentComponent = Component
+ const SegmentComponent = isSegmentViewEnabled
+ ? (segmentProps: any) => {
+ return (
+
+
+
+ )
+ }
+ : Component
const isRootLayoutWithChildrenSlotAndAtLeastOneMoreSlot =
rootLayoutAtThisLevel &&
@@ -964,3 +994,19 @@ function getRootParamsImpl(
)
}
}
+
+function normalizePageOrLayoutFilePath(
+ projectDir: string,
+ layoutOrPagePath: string | undefined
+) {
+ const dir = projectDir /*ctx.renderOpts.dir*/ || process.cwd()
+ const relativePath = (layoutOrPagePath || '')
+ // remove turbopack [project] prefix
+ .replace(/^\[project\][\\/]/, '')
+ // remove the project root from the path
+ .replace(dir, '')
+ // remove /(src/)?app/ dir prefix
+ .replace(/^[\\/](src[\\/])?app[\\/]/, '')
+
+ return relativePath
+}
diff --git a/packages/next/src/server/app-render/entry-base.ts b/packages/next/src/server/app-render/entry-base.ts
index 4ce9a57443c7e..814d47af1ba41 100644
--- a/packages/next/src/server/app-render/entry-base.ts
+++ b/packages/next/src/server/app-render/entry-base.ts
@@ -45,6 +45,13 @@ import { Postpone } from './rsc/postpone'
import { taintObjectReference } from './rsc/taint'
export { collectSegmentData } from './collect-segment-data'
+let SegmentViewNode: typeof import('../../shared/lib/devtool/app-segment-tree').SegmentViewNode =
+ () => null
+if (process.env.NODE_ENV === 'development') {
+ const appSegmentTree: typeof import('../../shared/lib/devtool/app-segment-tree') = require('../../shared/lib/devtool/app-segment-tree')
+ SegmentViewNode = appSegmentTree.SegmentViewNode
+}
+
// patchFetch makes use of APIs such as `React.unstable_postpone` which are only available
// in the experimental channel of React, so export it from here so that it comes from the bundled runtime
function patchFetch() {
@@ -78,4 +85,6 @@ export {
HTTPAccessFallbackBoundary,
patchFetch,
createMetadataComponents,
+ // Development only
+ SegmentViewNode,
}
diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts
index 7b3edfcaab048..602a68d7b3a8d 100644
--- a/packages/next/src/server/app-render/types.ts
+++ b/packages/next/src/server/app-render/types.ts
@@ -179,6 +179,7 @@ export type ServerOnInstrumentationRequestError = (
) => void | Promise
export interface RenderOptsPartial {
+ dir?: string
previewProps: __ApiPreviewProps | undefined
err?: Error | null
dev?: boolean
@@ -269,6 +270,11 @@ export interface RenderOptsPartial {
* statically generated.
*/
doNotThrowOnEmptyStaticShell?: boolean
+
+ /**
+ * next config experimental.devtoolSegmentExplorer
+ */
+ devtoolSegmentExplorer?: boolean
}
export type RenderOpts = LoadComponentsReturnType &
diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts
index be26d836251dd..5aa4a6b62eff8 100644
--- a/packages/next/src/server/base-server.ts
+++ b/packages/next/src/server/base-server.ts
@@ -563,6 +563,7 @@ export default abstract class Server<
}
this.renderOpts = {
+ dir: this.dir,
supportsDynamicResponse: true,
trailingSlash: this.nextConfig.trailingSlash,
deploymentId: this.nextConfig.deploymentId,
@@ -614,6 +615,8 @@ export default abstract class Server<
onInstrumentationRequestError:
this.instrumentationOnRequestError.bind(this),
reactMaxHeadersLength: this.nextConfig.reactMaxHeadersLength,
+ devtoolSegmentExplorer:
+ this.nextConfig.experimental.devtoolSegmentExplorer,
}
// Initialize next/config with the environment configuration
diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts
index 12a17eb5990ae..5722af8fe92e2 100644
--- a/packages/next/src/server/config-schema.ts
+++ b/packages/next/src/server/config-schema.ts
@@ -497,6 +497,7 @@ export const configSchema: zod.ZodType = z.lazy(() =>
})
.optional(),
globalNotFound: z.boolean().optional(),
+ devtoolSegmentExplorer: z.boolean().optional(),
})
.optional(),
exportPathMap: z
diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts
index 3ecbab3cfc0dc..d2504d2d8e144 100644
--- a/packages/next/src/server/config-shared.ts
+++ b/packages/next/src/server/config-shared.ts
@@ -685,6 +685,11 @@ export interface ExperimentalConfig {
*
*/
globalNotFound?: boolean
+
+ /**
+ * Enable segment viewer for the app directory in dev tool.
+ */
+ devtoolSegmentExplorer?: boolean
}
export type ExportPathMap = {
@@ -1382,6 +1387,7 @@ export const defaultConfig: NextConfig = {
useCache: undefined,
slowModuleDetection: undefined,
globalNotFound: false,
+ devtoolSegmentExplorer: false,
},
htmlLimitedBots: undefined,
bundlePagesRouterDependencies: false,
diff --git a/packages/next/src/shared/lib/devtool/app-segment-tree.tsx b/packages/next/src/shared/lib/devtool/app-segment-tree.tsx
new file mode 100644
index 0000000000000..e8515edcda6b6
--- /dev/null
+++ b/packages/next/src/shared/lib/devtool/app-segment-tree.tsx
@@ -0,0 +1,101 @@
+'use client'
+
+import { type ReactNode, useEffect, useSyncExternalStore } from 'react'
+import { createTrie, type Trie } from './trie'
+
+export type SegmentNode = {
+ type: string
+ pagePath: string
+}
+
+type DevtoolClientState = {
+ tree?: Trie
+}
+
+const DEFAULT_CLIENT_STATE =
+ typeof window === 'undefined'
+ ? undefined
+ : createTrie({
+ getKey: (item) => item.pagePath,
+ })
+
+declare global {
+ interface Window {
+ __NEXT_DEVTOOLS_CLIENT_STATE?: DevtoolClientState
+ }
+}
+
+function getSegmentTreeClientState(): DevtoolClientState {
+ if (typeof window === 'undefined') {
+ return {}
+ }
+ if (!window.__NEXT_DEVTOOLS_CLIENT_STATE) {
+ window.__NEXT_DEVTOOLS_CLIENT_STATE = {
+ // Initial state
+ tree: DEFAULT_CLIENT_STATE,
+ }
+ }
+ return window.__NEXT_DEVTOOLS_CLIENT_STATE
+}
+
+const listeners = typeof window === 'undefined' ? null : new Set<() => void>()
+
+const createSegmentTreeStore = (): {
+ subscribe: (callback: () => void) => () => void
+ getSnapshot: () => DevtoolClientState
+ getServerSnapshot: () => undefined
+} => {
+ if (typeof window === 'undefined') {
+ return {
+ subscribe: () => () => void 0,
+ getSnapshot: () => ({}),
+ getServerSnapshot: () => undefined,
+ }
+ }
+
+ // return a store that can be used by useSyncExternalStore
+ return {
+ subscribe: (callback) => {
+ listeners?.add(callback)
+ return () => listeners?.delete(callback)
+ },
+ getSnapshot: () => {
+ return getSegmentTreeClientState()
+ },
+ getServerSnapshot: () => {
+ return undefined
+ },
+ }
+}
+
+const { subscribe, getSnapshot, getServerSnapshot } = createSegmentTreeStore()
+
+export function SegmentViewNode({
+ type,
+ pagePath,
+ children,
+}: {
+ type: string
+ pagePath: string
+ children: ReactNode
+}) {
+ const clientState = getSegmentTreeClientState()
+ const tree = clientState.tree
+
+ useEffect(() => {
+ if (!tree) {
+ return
+ }
+ tree.insert({
+ type,
+ pagePath,
+ })
+ }, [type, pagePath, tree])
+
+ return children
+}
+
+export function useSegmentTreeClientState() {
+ const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
+ return state
+}
diff --git a/packages/next/src/shared/lib/devtool/trie.ts b/packages/next/src/shared/lib/devtool/trie.ts
new file mode 100644
index 0000000000000..700d99df9edea
--- /dev/null
+++ b/packages/next/src/shared/lib/devtool/trie.ts
@@ -0,0 +1,63 @@
+/**
+ * Trie data structure for storing and searching paths
+ *
+ * This can be used to store app router paths and search for them efficiently.
+ * e.g.
+ *
+ * [trie root]
+ * ├── layout.js
+ * ├── page.js
+ * ├── blog
+ * ├── layout.js
+ * ├── page.js
+ * ├── [slug]
+ * ├── layout.js
+ * ├── page.js
+ **/
+
+export type TrieNode = {
+ value?: Value
+ children: {
+ [key: string]: TrieNode | undefined
+ }
+}
+
+export type Trie = {
+ insert: (value: Value) => void
+ getRoot: () => TrieNode
+}
+
+export function createTrie({
+ getKey = (k) => k as unknown as string,
+}: {
+ getKey: (k: Value) => string
+}): Trie {
+ const root: TrieNode = {
+ value: undefined,
+ children: {},
+ }
+
+ function insert(value: Value) {
+ let currentNode = root
+ const key = getKey(value)
+ const segments = key.split('/')
+
+ for (const segment of segments) {
+ if (!currentNode.children[segment]) {
+ currentNode.children[segment] = {
+ // Skip value for intermediate nodes
+ children: {},
+ }
+ }
+ currentNode = currentNode.children[segment]
+ }
+
+ currentNode.value = value
+ }
+
+ function getRoot(): TrieNode {
+ return root
+ }
+
+ return { insert, getRoot }
+}
diff --git a/test/development/app-dir/segment-explorer/app/layout.tsx b/test/development/app-dir/segment-explorer/app/layout.tsx
new file mode 100644
index 0000000000000..716a8db36f52c
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/layout.tsx
@@ -0,0 +1,9 @@
+import { ReactNode } from 'react'
+
+export default function Root({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/test/development/app-dir/segment-explorer/app/parallel-routes/@bar/default.tsx b/test/development/app-dir/segment-explorer/app/parallel-routes/@bar/default.tsx
new file mode 100644
index 0000000000000..e355998a4433c
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/parallel-routes/@bar/default.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return 'default @bar'
+}
diff --git a/test/development/app-dir/segment-explorer/app/parallel-routes/@bar/layout.tsx b/test/development/app-dir/segment-explorer/app/parallel-routes/@bar/layout.tsx
new file mode 100644
index 0000000000000..1e18e748cf81e
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/parallel-routes/@bar/layout.tsx
@@ -0,0 +1,8 @@
+export default function Layout({ children }) {
+ return (
+
+
@bar Layout
+
{children}
+
+ )
+}
diff --git a/test/development/app-dir/segment-explorer/app/parallel-routes/@bar/page.tsx b/test/development/app-dir/segment-explorer/app/parallel-routes/@bar/page.tsx
new file mode 100644
index 0000000000000..eb4a3e2e9df71
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/parallel-routes/@bar/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return 'page @bar'
+}
diff --git a/test/development/app-dir/segment-explorer/app/parallel-routes/@bar/test-page/page.tsx b/test/development/app-dir/segment-explorer/app/parallel-routes/@bar/test-page/page.tsx
new file mode 100644
index 0000000000000..bff087a3a98f9
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/parallel-routes/@bar/test-page/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return 'test-page @bar'
+}
diff --git a/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/default.tsx b/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/default.tsx
new file mode 100644
index 0000000000000..408576f1876b5
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/default.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return 'default @foo'
+}
diff --git a/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/layout.tsx b/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/layout.tsx
new file mode 100644
index 0000000000000..05a3bda4f7be8
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/layout.tsx
@@ -0,0 +1,8 @@
+export default function Layout({ children }) {
+ return (
+
+
@foo Layout
+
{children}
+
+ )
+}
diff --git a/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/no-bar/page.tsx b/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/no-bar/page.tsx
new file mode 100644
index 0000000000000..44ab40e2ddfce
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/no-bar/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return 'no-bar @foo'
+}
diff --git a/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/page.tsx b/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/page.tsx
new file mode 100644
index 0000000000000..3536981cea3ff
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return 'page @foo'
+}
diff --git a/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/test-page/page.tsx b/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/test-page/page.tsx
new file mode 100644
index 0000000000000..8eff01dbe6dec
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/parallel-routes/@foo/test-page/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return 'test-page @foo'
+}
diff --git a/test/development/app-dir/segment-explorer/app/parallel-routes/layout.tsx b/test/development/app-dir/segment-explorer/app/parallel-routes/layout.tsx
new file mode 100644
index 0000000000000..088fb97919687
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/parallel-routes/layout.tsx
@@ -0,0 +1,10 @@
+export default function Layout({ children, bar, foo }) {
+ return (
+
+
Parallel Routes Layout
+
{children}
+
{foo}
+
{bar}
+
+ )
+}
diff --git a/test/development/app-dir/segment-explorer/app/parallel-routes/no-bar/page.tsx b/test/development/app-dir/segment-explorer/app/parallel-routes/no-bar/page.tsx
new file mode 100644
index 0000000000000..9674097ecd098
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/parallel-routes/no-bar/page.tsx
@@ -0,0 +1,10 @@
+import Link from 'next/link'
+
+export default function Page() {
+ return (
+
+
no bar
+
Back to /parallel-routes
+
+ )
+}
diff --git a/test/development/app-dir/segment-explorer/app/parallel-routes/page.tsx b/test/development/app-dir/segment-explorer/app/parallel-routes/page.tsx
new file mode 100644
index 0000000000000..9a69fccf9db2e
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/parallel-routes/page.tsx
@@ -0,0 +1,14 @@
+import Link from 'next/link'
+
+export default function Page() {
+ return (
+
+ Hello from Nested
+
+ To /parallel-routes/test-page
+
+
+ To /parallel-routes/no-bar
+
+ )
+}
diff --git a/test/development/app-dir/segment-explorer/app/parallel-routes/test-page/page.tsx b/test/development/app-dir/segment-explorer/app/parallel-routes/test-page/page.tsx
new file mode 100644
index 0000000000000..ee6b1571a5bfc
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/app/parallel-routes/test-page/page.tsx
@@ -0,0 +1,3 @@
+export default function TestPage() {
+ return 'test page'
+}
diff --git a/test/development/app-dir/segment-explorer/next.config.js b/test/development/app-dir/segment-explorer/next.config.js
new file mode 100644
index 0000000000000..586102a1310fb
--- /dev/null
+++ b/test/development/app-dir/segment-explorer/next.config.js
@@ -0,0 +1,10 @@
+/**
+ * @type {import('next').NextConfig}
+ */
+const nextConfig = {
+ experimental: {
+ devtoolSegmentExplorer: true,
+ },
+}
+
+module.exports = nextConfig
diff --git a/turbopack/crates/turbopack-core/src/ident.rs b/turbopack/crates/turbopack-core/src/ident.rs
index 06fc77f0776c6..8267e7e4c0fd6 100644
--- a/turbopack/crates/turbopack-core/src/ident.rs
+++ b/turbopack/crates/turbopack-core/src/ident.rs
@@ -59,7 +59,7 @@ impl ValueToString for AssetIdent {
let query = self.query.await?;
if !query.is_empty() {
- write!(s, "{}", &*query)?;
+ write!(s, "?{}", &*query)?;
}
if let Some(fragment) = &self.fragment {