Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,14 @@ function PageSegmentTreeLayerPresentation({
if (!childNode || !childNode.value) {
return null
}
const fileName =
childNode.value.pagePath.split('/').pop() || ''
const filePath = childNode.value.pagePath
const fileName = filePath.split('/').pop() || ''
return (
<span
key={fileChildSegment}
onClick={() => {
openInEditor({ filePath })
}}
className={cx(
'segment-explorer-file-label',
`segment-explorer-file-label--${childNode.value.type}`
Expand Down Expand Up @@ -215,6 +218,7 @@ export const DEV_TOOLS_INFO_RENDER_FILES_STYLES = css`
font-size: var(--size-12);
font-weight: 500;
user-select: none;
cursor: pointer;
}
.segment-explorer-file-label--layout,
.segment-explorer-file-label--template,
Expand Down Expand Up @@ -242,3 +246,17 @@ export const DEV_TOOLS_INFO_RENDER_FILES_STYLES = css`
color: var(--color-red-900);
}
`

function openInEditor({ filePath }: { filePath: string }) {
const params = new URLSearchParams({
file: filePath,
// Mark the file path is relative to the app directory,
// The editor launcher will complete the full path for it.
isAppRelativePath: '1',
})
fetch(
`${
process.env.__NEXT_ROUTER_BASEPATH || ''
}/__nextjs_launch-editor?${params.toString()}`
)
}
26 changes: 26 additions & 0 deletions packages/next/src/next-devtools/server/launch-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import { cyan, green, red } from '../../lib/picocolors'
import child_process from 'child_process'
import fs from 'fs'
import fsp from 'fs/promises'
import os from 'os'
import path from 'path'
import shellQuote from 'next/dist/compiled/shell-quote'
Expand Down Expand Up @@ -428,3 +429,28 @@ export function launchEditor(
})
}
}

// Open the file in editor if exists, otherwise return an error
export async function openFileInEditor(
filePath: string,
line: number,
col: number
) {
const result = {
found: false,
error: null as Error | null,
}
const existed = await fsp.access(filePath, fs.constants.F_OK).then(
() => true,
() => false
)
if (existed) {
try {
launchEditor(filePath, line, col)
result.found = true
} catch (err) {
result.error = err as Error
}
}
return result
}
8 changes: 6 additions & 2 deletions packages/next/src/server/dev/hot-reloader-turbopack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function getSourceMapFromTurbopack(
}

export async function createHotReloaderTurbopack(
opts: SetupOpts,
opts: SetupOpts & { isSrcDir: boolean },
serverFields: ServerFields,
distDir: string,
resetFetch: () => void
Expand Down Expand Up @@ -652,7 +652,11 @@ export async function createHotReloaderTurbopack(
)

const middlewares = [
getOverlayMiddleware(project, projectPath),
getOverlayMiddleware({
project,
projectPath,
isSrcDir: opts.isSrcDir,
}),
getSourceMapMiddleware(project),
getNextErrorFeedbackMiddleware(opts.telemetry),
getDevOverlayFontMiddleware(),
Expand Down
5 changes: 5 additions & 0 deletions packages/next/src/server/dev/hot-reloader-webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
}
private devtoolsFrontendUrl: string | undefined
private reloadAfterInvalidation: boolean = false
private isSrcDir: boolean

public serverStats: webpack.Stats | null
public edgeServerStats: webpack.Stats | null
Expand All @@ -274,6 +275,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
dir: string,
{
config,
isSrcDir,
pagesDir,
distDir,
buildId,
Expand All @@ -285,6 +287,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
resetFetch,
}: {
config: NextConfigComplete
isSrcDir: boolean
pagesDir?: string
distDir: string
buildId: string
Expand All @@ -302,6 +305,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
this.buildId = buildId
this.encryptionKey = encryptionKey
this.dir = dir
this.isSrcDir = isSrcDir
this.middlewares = []
this.pagesDir = pagesDir
this.appDir = appDir
Expand Down Expand Up @@ -1567,6 +1571,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
this.middlewares = [
getOverlayMiddleware({
rootDirectory: this.dir,
isSrcDir: this.isSrcDir,
clientStats: () => this.clientStats,
serverStats: () => this.serverStats,
edgeServerStats: () => this.edgeServerStats,
Expand Down
52 changes: 35 additions & 17 deletions packages/next/src/server/dev/middleware-turbopack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import {
type OriginalStackFramesResponse,
} from '../../next-devtools/server/shared'
import { middlewareResponse } from '../../next-devtools/server/middleware-response'
import fs, { constants as FS } from 'fs/promises'
import path from 'path'
import url from 'url'
import { launchEditor } from '../../next-devtools/server/launch-editor'
import { openFileInEditor } from '../../next-devtools/server/launch-editor'
import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
import {
SourceMapConsumer,
Expand Down Expand Up @@ -371,7 +370,15 @@ async function createOriginalStackFrame(
}
}

export function getOverlayMiddleware(project: Project, projectPath: string) {
export function getOverlayMiddleware({
project,
projectPath,
isSrcDir,
}: {
project: Project
projectPath: string
isSrcDir: boolean
}) {
return async function (
req: IncomingMessage,
res: ServerResponse,
Expand Down Expand Up @@ -421,23 +428,34 @@ export function getOverlayMiddleware(project: Project, projectPath: string) {

return middlewareResponse.json(res, result)
} else if (pathname === '/__nextjs_launch-editor') {
const frame = createStackFrame(searchParams)

if (!frame) return middlewareResponse.badRequest(res)

const fileExists = await fs.access(frame.file, FS.F_OK).then(
() => true,
() => false
)
if (!fileExists) return middlewareResponse.notFound(res)
const isAppRelativePath = searchParams.get('isAppRelativePath') === '1'

let openEditorResult
if (isAppRelativePath) {
const relativeFilePath = searchParams.get('file') || ''
const absoluteFilePath = path.join(
projectPath,
'app',
isSrcDir ? 'src' : '',
relativeFilePath
)
openEditorResult = await openFileInEditor(absoluteFilePath, 1, 1)
} else {
const frame = createStackFrame(searchParams)
if (!frame) return middlewareResponse.badRequest(res)
openEditorResult = await openFileInEditor(
frame.file,
frame.line ?? 1,
frame.column ?? 1
)
}

try {
launchEditor(frame.file, frame.line ?? 1, frame.column ?? 1)
} catch (err) {
console.log('Failed to launch editor:', err)
if (openEditorResult.error) {
return middlewareResponse.internalServerError(res)
}

if (!openEditorResult.found) {
return middlewareResponse.notFound(res)
}
return middlewareResponse.noContent(res)
}

Expand Down
56 changes: 36 additions & 20 deletions packages/next/src/server/dev/middleware-webpack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { constants as FS, promises as fs } from 'fs'
import { findSourceMap, type SourceMap } from 'module'
import path from 'path'
import { fileURLToPath, pathToFileURL } from 'url'
Expand All @@ -8,7 +7,7 @@ import {
} from 'next/dist/compiled/source-map08'
import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
import { getSourceMapFromFile } from './get-source-map-from-file'
import { launchEditor } from '../../next-devtools/server/launch-editor'
import { openFileInEditor } from '../../next-devtools/server/launch-editor'
import {
getOriginalCodeFrame,
type OriginalStackFrameResponse,
Expand Down Expand Up @@ -514,11 +513,13 @@ async function getOriginalStackFrame({

export function getOverlayMiddleware(options: {
rootDirectory: string
isSrcDir: boolean
clientStats: () => webpack.Stats | null
serverStats: () => webpack.Stats | null
edgeServerStats: () => webpack.Stats | null
}) {
const { rootDirectory, clientStats, serverStats, edgeServerStats } = options
const { rootDirectory, isSrcDir, clientStats, serverStats, edgeServerStats } =
options

return async function (
req: IncomingMessage,
Expand Down Expand Up @@ -577,24 +578,39 @@ export function getOverlayMiddleware(options: {

if (!frame.file) return middlewareResponse.badRequest(res)

// frame files may start with their webpack layer, like (middleware)/middleware.js
const filePath = path.resolve(
rootDirectory,
frame.file.replace(/^\([^)]+\)\//, '')
)
const fileExists = await fs.access(filePath, FS.F_OK).then(
() => true,
() => false
)
if (!fileExists) return middlewareResponse.notFound(res)

try {
launchEditor(filePath, frame.lineNumber, frame.column ?? 1)
} catch (err) {
console.log('Failed to launch editor:', err)
return middlewareResponse.internalServerError(res)
let openEditorResult
const isAppRelativePath = searchParams.get('isAppRelativePath') === '1'
if (isAppRelativePath) {
const relativeFilePath = searchParams.get('file') || ''
const absoluteFilePath = path.join(
rootDirectory,
'app',
isSrcDir ? 'src' : '',
relativeFilePath
)
openEditorResult = await openFileInEditor(absoluteFilePath, 1, 1)
} else {
// frame files may start with their webpack layer, like (middleware)/middleware.js
const filePath = path.resolve(
rootDirectory,
frame.file.replace(/^\([^)]+\)\//, '')
)
openEditorResult = await openFileInEditor(
filePath,
frame.lineNumber,
frame.column ?? 1
)
}
if (openEditorResult.error) {
console.error('Failed to launch editor:', openEditorResult.error)
return middlewareResponse.internalServerError(
res,
openEditorResult.error
)
}
if (!openEditorResult.found) {
return middlewareResponse.notFound(res)
}

return middlewareResponse.noContent(res)
}

Expand Down
12 changes: 10 additions & 2 deletions packages/next/src/server/lib/router-utils/setup-dev-bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,11 @@ export async function propagateServerField(
await opts.renderServer?.instance?.propagateServerField(opts.dir, field, args)
}

async function startWatcher(opts: SetupOpts) {
async function startWatcher(
opts: SetupOpts & {
isSrcDir: boolean
}
) {
const { nextConfig, appDir, pagesDir, dir, resetFetch } = opts
const { useFileSystemPublicRoutes } = nextConfig
const usingTypeScript = await verifyTypeScript(opts)
Expand Down Expand Up @@ -191,6 +195,7 @@ async function startWatcher(opts: SetupOpts) {
const hotReloader: NextJsHotReloaderInterface = opts.turbo
? await createHotReloaderTurbopack(opts, serverFields, distDir, resetFetch)
: new HotReloaderWebpack(opts.dir, {
isSrcDir: opts.isSrcDir,
appDir,
pagesDir,
distDir,
Expand Down Expand Up @@ -1026,7 +1031,10 @@ export async function setupDevBundler(opts: SetupOpts) {
.relative(opts.dir, opts.pagesDir || opts.appDir || '')
.startsWith('src')

const result = await startWatcher(opts)
const result = await startWatcher({
...opts,
isSrcDir,
})

opts.telemetry.record(
eventCliSession(
Expand Down
Loading