Skip to content

Commit 53bb90c

Browse files
authored
[Turbopack] fix root and project path usages in a monorepo (#73552)
### What? Improved trace source maps to return relative paths and always relative to process.cwd. `turbopack://` urls in SourceMaps are always relative to the project root. `file://` urls do no longer contain duplicate path segments. Add a test case Also: * improve error when sourcemap lookup throws
1 parent d3529ba commit 53bb90c

File tree

35 files changed

+531
-176
lines changed

35 files changed

+531
-176
lines changed

crates/napi/src/next_api/project.rs

+46-20
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ use turbo_tasks::{
2828
get_effects, Completion, Effects, ReadRef, ResolvedVc, TransientInstance, UpdateInfo, Vc,
2929
};
3030
use turbo_tasks_fs::{
31-
util::uri_from_file, DiskFileSystem, FileContent, FileSystem, FileSystemPath,
31+
get_relative_path_to, util::uri_from_file, DiskFileSystem, FileContent, FileSystem,
32+
FileSystemPath,
3233
};
3334
use turbopack_core::{
3435
diagnostics::PlainDiagnostic,
@@ -1015,6 +1016,7 @@ pub fn project_update_info_subscribe(
10151016
pub struct StackFrame {
10161017
pub is_server: bool,
10171018
pub is_internal: Option<bool>,
1019+
pub original_file: Option<String>,
10181020
pub file: String,
10191021
// 1-indexed, unlike source map tokens
10201022
pub line: Option<u32>,
@@ -1084,6 +1086,7 @@ pub async fn get_source_map(
10841086
pub async fn project_trace_source(
10851087
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
10861088
frame: StackFrame,
1089+
current_directory_file_url: String,
10871090
) -> napi::Result<Option<StackFrame>> {
10881091
let turbo_tasks = project.turbo_tasks.clone();
10891092
let container = project.container;
@@ -1120,27 +1123,50 @@ pub async fn project_trace_source(
11201123
}
11211124
};
11221125

1123-
let project_path_uri =
1124-
uri_from_file(project.container.project().project_path(), None).await? + "/";
1125-
let (source_file, is_internal) =
1126-
if let Some(source_file) = original_file.strip_prefix(&project_path_uri) {
1127-
// Client code uses file://
1128-
(source_file, false)
1129-
} else if let Some(source_file) =
1130-
original_file.strip_prefix(&*SOURCE_MAP_PREFIX_PROJECT)
1131-
{
1132-
// Server code uses turbopack://[project]
1133-
// TODO should this also be file://?
1134-
(source_file, false)
1135-
} else if let Some(source_file) = original_file.strip_prefix(SOURCE_MAP_PREFIX) {
1136-
// All other code like turbopack://[turbopack] is internal code
1137-
(source_file, true)
1138-
} else {
1139-
bail!("Original file ({}) outside project", original_file)
1140-
};
1126+
let project_root_uri =
1127+
uri_from_file(project.container.project().project_root_path(), None).await? + "/";
1128+
let (file, original_file, is_internal) = if let Some(source_file) =
1129+
original_file.strip_prefix(&project_root_uri)
1130+
{
1131+
// Client code uses file://
1132+
(
1133+
get_relative_path_to(&current_directory_file_url, &original_file)
1134+
// TODO(sokra) remove this to include a ./ here to make it a relative path
1135+
.trim_start_matches("./")
1136+
.to_string(),
1137+
Some(source_file.to_string()),
1138+
false,
1139+
)
1140+
} else if let Some(source_file) =
1141+
original_file.strip_prefix(&*SOURCE_MAP_PREFIX_PROJECT)
1142+
{
1143+
// Server code uses turbopack://[project]
1144+
// TODO should this also be file://?
1145+
(
1146+
get_relative_path_to(
1147+
&current_directory_file_url,
1148+
&format!("{}{}", project_root_uri, source_file),
1149+
)
1150+
// TODO(sokra) remove this to include a ./ here to make it a relative path
1151+
.trim_start_matches("./")
1152+
.to_string(),
1153+
Some(source_file.to_string()),
1154+
false,
1155+
)
1156+
} else if let Some(source_file) = original_file.strip_prefix(SOURCE_MAP_PREFIX) {
1157+
// All other code like turbopack://[turbopack] is internal code
1158+
(source_file.to_string(), None, true)
1159+
} else {
1160+
bail!(
1161+
"Original file ({}) outside project ({})",
1162+
original_file,
1163+
project_root_uri
1164+
)
1165+
};
11411166

11421167
Ok(Some(StackFrame {
1143-
file: source_file.to_string(),
1168+
file,
1169+
original_file,
11441170
method_name: name.as_ref().map(ToString::to_string),
11451171
line,
11461172
column,

crates/next-api/src/project.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ impl Project {
631631
}
632632

633633
#[turbo_tasks::function]
634-
fn project_root_path(self: Vc<Self>) -> Vc<FileSystemPath> {
634+
pub fn project_root_path(self: Vc<Self>) -> Vc<FileSystemPath> {
635635
self.project_fs().root()
636636
}
637637

@@ -693,7 +693,7 @@ impl Project {
693693

694694
let node_execution_chunking_context = Vc::upcast(
695695
NodeJsChunkingContext::builder(
696-
self.project_path().to_resolved().await?,
696+
self.project_root_path().to_resolved().await?,
697697
node_root,
698698
node_root,
699699
node_root.join("build/chunks".into()).to_resolved().await?,
@@ -820,7 +820,7 @@ impl Project {
820820
#[turbo_tasks::function]
821821
pub(super) fn client_chunking_context(self: Vc<Self>) -> Vc<Box<dyn ChunkingContext>> {
822822
get_client_chunking_context(
823-
self.project_path(),
823+
self.project_root_path(),
824824
self.client_relative_path(),
825825
self.next_config().computed_asset_prefix(),
826826
self.client_compile_time_info().environment(),
@@ -838,7 +838,7 @@ impl Project {
838838
if client_assets {
839839
get_server_chunking_context_with_client_assets(
840840
self.next_mode(),
841-
self.project_path(),
841+
self.project_root_path(),
842842
self.node_root(),
843843
self.client_relative_path(),
844844
self.next_config().computed_asset_prefix(),
@@ -849,7 +849,7 @@ impl Project {
849849
} else {
850850
get_server_chunking_context(
851851
self.next_mode(),
852-
self.project_path(),
852+
self.project_root_path(),
853853
self.node_root(),
854854
self.server_compile_time_info().environment(),
855855
self.module_id_strategy(),
@@ -866,7 +866,7 @@ impl Project {
866866
if client_assets {
867867
get_edge_chunking_context_with_client_assets(
868868
self.next_mode(),
869-
self.project_path(),
869+
self.project_root_path(),
870870
self.node_root(),
871871
self.client_relative_path(),
872872
self.next_config().computed_asset_prefix(),
@@ -877,7 +877,7 @@ impl Project {
877877
} else {
878878
get_edge_chunking_context(
879879
self.next_mode(),
880-
self.project_path(),
880+
self.project_root_path(),
881881
self.node_root(),
882882
self.edge_compile_time_info().environment(),
883883
self.module_id_strategy(),

crates/next-core/src/next_server/context.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -974,7 +974,7 @@ pub fn get_server_runtime_entries(
974974
#[turbo_tasks::function]
975975
pub async fn get_server_chunking_context_with_client_assets(
976976
mode: Vc<NextMode>,
977-
project_path: ResolvedVc<FileSystemPath>,
977+
root_path: ResolvedVc<FileSystemPath>,
978978
node_root: ResolvedVc<FileSystemPath>,
979979
client_root: ResolvedVc<FileSystemPath>,
980980
asset_prefix: ResolvedVc<Option<RcStr>>,
@@ -987,7 +987,7 @@ pub async fn get_server_chunking_context_with_client_assets(
987987
// different server chunking contexts. OR the build chunking context should
988988
// support both production and development modes.
989989
let mut builder = NodeJsChunkingContext::builder(
990-
project_path,
990+
root_path,
991991
node_root,
992992
client_root,
993993
node_root
@@ -1019,7 +1019,7 @@ pub async fn get_server_chunking_context_with_client_assets(
10191019
#[turbo_tasks::function]
10201020
pub async fn get_server_chunking_context(
10211021
mode: Vc<NextMode>,
1022-
project_path: ResolvedVc<FileSystemPath>,
1022+
root_path: ResolvedVc<FileSystemPath>,
10231023
node_root: ResolvedVc<FileSystemPath>,
10241024
environment: ResolvedVc<Environment>,
10251025
module_id_strategy: ResolvedVc<Box<dyn ModuleIdStrategy>>,
@@ -1030,7 +1030,7 @@ pub async fn get_server_chunking_context(
10301030
// different server chunking contexts. OR the build chunking context should
10311031
// support both production and development modes.
10321032
let mut builder = NodeJsChunkingContext::builder(
1033-
project_path,
1033+
root_path,
10341034
node_root,
10351035
node_root,
10361036
node_root.join("server/chunks".into()).to_resolved().await?,

packages/next/src/build/swc/generated-native.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@ export interface StackFrame {
275275
}
276276
export declare function projectTraceSource(
277277
project: { __napiType: 'Project' },
278-
frame: StackFrame
278+
frame: StackFrame,
279+
currentDirectoryFileUrl: string
279280
): Promise<StackFrame | null>
280281
export declare function projectGetSourceForAsset(
281282
project: { __napiType: 'Project' },

packages/next/src/build/swc/index.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -746,9 +746,14 @@ function bindingToApi(
746746
}
747747

748748
traceSource(
749-
stackFrame: TurbopackStackFrame
749+
stackFrame: TurbopackStackFrame,
750+
currentDirectoryFileUrl: string
750751
): Promise<TurbopackStackFrame | null> {
751-
return binding.projectTraceSource(this._nativeProject, stackFrame)
752+
return binding.projectTraceSource(
753+
this._nativeProject,
754+
stackFrame,
755+
currentDirectoryFileUrl
756+
)
752757
}
753758

754759
getSourceForAsset(filePath: string): Promise<string | null> {

packages/next/src/build/swc/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export interface TurbopackStackFrame {
169169
isServer: boolean
170170
isInternal?: boolean
171171
file: string
172+
originalFile?: string
172173
/** 1-indexed, unlike source map tokens */
173174
line?: number
174175
/** 1-indexed, unlike source map tokens */
@@ -207,7 +208,8 @@ export interface Project {
207208
getSourceMapSync(filePath: string): string | null
208209

209210
traceSource(
210-
stackFrame: TurbopackStackFrame
211+
stackFrame: TurbopackStackFrame,
212+
currentDirectoryFileUrl: string
211213
): Promise<TurbopackStackFrame | null>
212214

213215
updateInfoSubscribe(

packages/next/src/client/components/react-dev-overlay/server/middleware-turbopack.ts

+19-12
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { SourceMapConsumer } from 'next/dist/compiled/source-map08'
1818
import type { Project, TurbopackStackFrame } from '../../../../build/swc/types'
1919
import { getSourceMapFromFile } from '../internal/helpers/get-source-map-from-file'
2020
import { findSourceMap, type SourceMapPayload } from 'node:module'
21+
import { pathToFileURL } from 'node:url'
2122

2223
function shouldIgnorePath(modulePath: string): boolean {
2324
return (
@@ -40,7 +41,9 @@ export async function batchedTraceSource(
4041
: undefined
4142
if (!file) return
4243

43-
const sourceFrame = await project.traceSource(frame)
44+
const currentDirectoryFileUrl = pathToFileURL(process.cwd()).href
45+
46+
const sourceFrame = await project.traceSource(frame, currentDirectoryFileUrl)
4447
if (!sourceFrame) {
4548
return {
4649
frame: {
@@ -56,20 +59,21 @@ export async function batchedTraceSource(
5659
}
5760

5861
let source = null
62+
const originalFile = sourceFrame.originalFile
5963
// Don't look up source for node_modules or internals. These can often be large bundled files.
6064
const ignored =
61-
shouldIgnorePath(sourceFrame.file) ||
65+
shouldIgnorePath(originalFile ?? sourceFrame.file) ||
6266
// isInternal means resource starts with turbopack://[turbopack]
6367
!!sourceFrame.isInternal
64-
if (sourceFrame && sourceFrame.file && !ignored) {
65-
let sourcePromise = currentSourcesByFile.get(sourceFrame.file)
68+
if (originalFile && !ignored) {
69+
let sourcePromise = currentSourcesByFile.get(originalFile)
6670
if (!sourcePromise) {
67-
sourcePromise = project.getSourceForAsset(sourceFrame.file)
68-
currentSourcesByFile.set(sourceFrame.file, sourcePromise)
71+
sourcePromise = project.getSourceForAsset(originalFile)
72+
currentSourcesByFile.set(originalFile, sourcePromise)
6973
setTimeout(() => {
7074
// Cache file reads for 100ms, as frames will often reference the same
7175
// files and can be large.
72-
currentSourcesByFile.delete(sourceFrame.file!)
76+
currentSourcesByFile.delete(originalFile!)
7377
}, 100)
7478
}
7579
source = await sourcePromise
@@ -231,10 +235,7 @@ async function nativeTraceSource(
231235
'<unknown>',
232236
column: (originalPosition.column ?? 0) + 1,
233237
file: originalPosition.source?.startsWith('file://')
234-
? path.relative(
235-
process.cwd(),
236-
url.fileURLToPath(originalPosition.source)
237-
)
238+
? relativeToCwd(originalPosition.source)
238239
: originalPosition.source,
239240
lineNumber: originalPosition.line ?? 0,
240241
// TODO: c&p from async createOriginalStackFrame but why not frame.arguments?
@@ -252,6 +253,12 @@ async function nativeTraceSource(
252253
return undefined
253254
}
254255

256+
function relativeToCwd(file: string): string {
257+
const relPath = path.relative(process.cwd(), url.fileURLToPath(file))
258+
// TODO(sokra) include a ./ here to make it a relative path
259+
return relPath
260+
}
261+
255262
async function createOriginalStackFrame(
256263
project: Project,
257264
frame: TurbopackStackFrame
@@ -288,7 +295,7 @@ export function getOverlayMiddleware(project: Project) {
288295
try {
289296
originalStackFrame = await createOriginalStackFrame(project, frame)
290297
} catch (e: any) {
291-
return internalServerError(res, e.message)
298+
return internalServerError(res, e.stack)
292299
}
293300

294301
if (!originalStackFrame) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ReactNode } from 'react'
2+
export default function Root({ children }: { children: ReactNode }) {
3+
return (
4+
<html>
5+
<body>{children}</body>
6+
</html>
7+
)
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { text } from 'my-package/typescript'
2+
3+
export default function Page() {
4+
return <p>{text}</p>
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use client'
2+
3+
import { text } from 'my-package/typescript'
4+
5+
export default function Page() {
6+
return <p>{text}</p>
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
throw new Error('Expected error')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use client'
2+
3+
import { useEffect } from 'react'
4+
5+
export default function Page() {
6+
useEffect(function effectCallback() {
7+
innerFunction()
8+
})
9+
return <p>Hello Source Maps</p>
10+
}
11+
12+
function innerFunction() {
13+
innerArrowFunction()
14+
}
15+
16+
const innerArrowFunction = () => {
17+
require('../separate-file')
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default async function Page({ searchParams }) {
2+
// We don't want the build to fail in production
3+
if (process.env.NODE_ENV === 'development') {
4+
innerFunction()
5+
}
6+
return <p>Hello Source Maps</p>
7+
}
8+
9+
function innerFunction() {
10+
innerArrowFunction()
11+
}
12+
13+
const innerArrowFunction = () => {
14+
require('../separate-file')
15+
}

0 commit comments

Comments
 (0)