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} -
- - {learnMoreLink && ( - - Learn More - - )} -
+ {(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 {