Turbopack: support more server code in tracing error stack frames (#57156)
This: - Uses `isServer` to use the appropriate Turbopack `FileSystem` when creating `FileSystemPath`s - Properly uri decodes path segments originating from `file://` uris - Correctly reads chunks starting at the project path instead of the root path Closes WEB-1815 --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
parent
0d2edbb23f
commit
1ffef0f1a3
10 changed files with 89 additions and 70 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3493,6 +3493,7 @@ dependencies = [
|
|||
"turbo-tasks",
|
||||
"turbopack-binding",
|
||||
"url",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -76,6 +76,7 @@ turbopack-binding = { workspace = true, features = [
|
|||
"__turbopack_ecmascript_hmr_protocol",
|
||||
] }
|
||||
url = {workspace = true}
|
||||
urlencoding = {workspace = true}
|
||||
|
||||
[target.'cfg(not(all(target_os = "linux", target_env = "musl", target_arch = "aarch64")))'.dependencies]
|
||||
turbopack-binding = { workspace = true, features = ["__turbo_tasks_malloc"] }
|
||||
|
|
|
@ -617,10 +617,11 @@ pub fn project_update_info_subscribe(
|
|||
#[derive(Debug)]
|
||||
#[napi(object)]
|
||||
pub struct StackFrame {
|
||||
pub file: String,
|
||||
pub method_name: Option<String>,
|
||||
pub line: u32,
|
||||
pub column: Option<u32>,
|
||||
pub file: String,
|
||||
pub is_server: bool,
|
||||
pub line: u32,
|
||||
pub method_name: Option<String>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
|
@ -633,7 +634,7 @@ pub async fn project_trace_source(
|
|||
.run_once(async move {
|
||||
let file = match Url::parse(&frame.file) {
|
||||
Ok(url) => match url.scheme() {
|
||||
"file" => url.path().to_string(),
|
||||
"file" => urlencoding::decode(url.path())?.to_string(),
|
||||
_ => bail!("Unknown url scheme"),
|
||||
},
|
||||
Err(_) => frame.file.to_string(),
|
||||
|
@ -641,8 +642,8 @@ pub async fn project_trace_source(
|
|||
|
||||
let Some(chunk_base) = file.strip_prefix(
|
||||
&(format!(
|
||||
"{}/{}",
|
||||
project.container.project().await?.root_path,
|
||||
"{}/{}/",
|
||||
project.container.project().await?.project_path,
|
||||
project.container.project().dist_dir().await?
|
||||
)),
|
||||
) else {
|
||||
|
@ -650,32 +651,29 @@ pub async fn project_trace_source(
|
|||
return Ok(None);
|
||||
};
|
||||
|
||||
let chunk_path = format!(
|
||||
"{}{}",
|
||||
let path = if frame.is_server {
|
||||
project
|
||||
.container
|
||||
.project()
|
||||
.node_root()
|
||||
.join(chunk_base.to_owned())
|
||||
} else {
|
||||
project
|
||||
.container
|
||||
.project()
|
||||
.client_relative_path()
|
||||
.await?
|
||||
.path,
|
||||
chunk_base
|
||||
);
|
||||
|
||||
let path = project
|
||||
.container
|
||||
.project()
|
||||
.client_root()
|
||||
.fs()
|
||||
.root()
|
||||
.join(chunk_path);
|
||||
|
||||
let Some(generatable): Option<Vc<Box<dyn GenerateSourceMap>>> =
|
||||
Vc::try_resolve_sidecast(project.container.get_versioned_content(path)).await?
|
||||
else {
|
||||
return Ok(None);
|
||||
.join(chunk_base.to_owned())
|
||||
};
|
||||
|
||||
let map = generatable
|
||||
let Some(versioned) = Vc::try_resolve_sidecast::<Box<dyn GenerateSourceMap>>(
|
||||
project.container.get_versioned_content(path),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
bail!("Could not GenerateSourceMap")
|
||||
};
|
||||
|
||||
let map = versioned
|
||||
.generate_source_map()
|
||||
.await?
|
||||
.context("Chunk is missing a sourcemap")?;
|
||||
|
@ -700,6 +698,7 @@ pub async fn project_trace_source(
|
|||
method_name: token.name,
|
||||
line: token.original_line as u32,
|
||||
column: Some(token.original_column as u32),
|
||||
is_server: frame.is_server,
|
||||
}))
|
||||
})
|
||||
.await
|
||||
|
@ -719,12 +718,14 @@ pub async fn project_get_source_for_asset(
|
|||
.container
|
||||
.project()
|
||||
.project_path()
|
||||
.fs()
|
||||
.root()
|
||||
.join(file_path.to_string())
|
||||
.read()
|
||||
.await?;
|
||||
|
||||
let FileContent::Content(source_content) = source_content else {
|
||||
return Ok(None);
|
||||
bail!("Cannot find source for asset {}", file_path);
|
||||
};
|
||||
|
||||
Ok(Some(source_content.content().to_str()?.to_string()))
|
||||
|
|
|
@ -269,13 +269,13 @@ impl ProjectContainer {
|
|||
pub struct Project {
|
||||
/// A root path from which all files must be nested under. Trying to access
|
||||
/// a file outside this root will fail. Think of this as a chroot.
|
||||
pub root_path: String,
|
||||
root_path: String,
|
||||
|
||||
/// A path where to emit the build outputs. next.config.js's distDir.
|
||||
dist_dir: String,
|
||||
|
||||
/// A path inside the root_path which contains the app/pages directories.
|
||||
project_path: String,
|
||||
pub project_path: String,
|
||||
|
||||
/// Whether to watch the filesystem for file changes.
|
||||
watch: bool,
|
||||
|
@ -369,7 +369,7 @@ impl Project {
|
|||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn node_fs(self: Vc<Self>) -> Result<Vc<Box<dyn FileSystem>>> {
|
||||
pub async fn node_fs(self: Vc<Self>) -> Result<Vc<Box<dyn FileSystem>>> {
|
||||
let this = self.await?;
|
||||
let disk_fs = DiskFileSystem::new("node".to_string(), this.project_path.clone());
|
||||
disk_fs.await?.start_watching_with_invalidation_reason()?;
|
||||
|
|
|
@ -519,11 +519,12 @@ export interface HmrIdentifiers {
|
|||
identifiers: string[]
|
||||
}
|
||||
|
||||
export interface StackFrame {
|
||||
file: string
|
||||
methodName: string | null
|
||||
line: number
|
||||
interface TurbopackStackFrame {
|
||||
column: number | null
|
||||
file: string
|
||||
isServer: boolean
|
||||
line: number
|
||||
methodName: string | null
|
||||
}
|
||||
|
||||
export interface UpdateInfo {
|
||||
|
@ -548,7 +549,9 @@ export interface Project {
|
|||
TurbopackResult<HmrIdentifiers>
|
||||
>
|
||||
getSourceForAsset(filePath: string): Promise<string | null>
|
||||
traceSource(stackFrame: StackFrame): Promise<StackFrame | null>
|
||||
traceSource(
|
||||
stackFrame: TurbopackStackFrame
|
||||
): Promise<TurbopackStackFrame | null>
|
||||
updateInfoSubscribe(): AsyncIterableIterator<TurbopackResult<UpdateInfo>>
|
||||
}
|
||||
|
||||
|
@ -925,7 +928,9 @@ function bindingToApi(binding: any, _wasm: boolean) {
|
|||
return subscription
|
||||
}
|
||||
|
||||
traceSource(stackFrame: StackFrame): Promise<StackFrame | null> {
|
||||
traceSource(
|
||||
stackFrame: TurbopackStackFrame
|
||||
): Promise<TurbopackStackFrame | null> {
|
||||
return binding.projectTraceSource(this._nativeProject, stackFrame)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { parse } from 'next/dist/compiled/stacktrace-parser'
|
||||
import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
|
||||
|
||||
const regexNextStatic = /\/_next(\/static\/.+)/g
|
||||
const regexNextStatic = /\/_next(\/static\/.+)/
|
||||
|
||||
export function parseStack(stack: string): StackFrame[] {
|
||||
const frames = parse(stack)
|
||||
|
|
|
@ -2188,20 +2188,24 @@ async function startWatcher(opts: SetupOpts) {
|
|||
)
|
||||
|
||||
let originalFrame, isEdgeCompiler
|
||||
if (frame?.lineNumber && frame?.file) {
|
||||
const frameFile = frame?.file
|
||||
if (frame?.lineNumber && frameFile) {
|
||||
if (opts.turbo) {
|
||||
try {
|
||||
originalFrame = await createOriginalTurboStackFrame(
|
||||
project!,
|
||||
frame
|
||||
)
|
||||
originalFrame = await createOriginalTurboStackFrame(project!, {
|
||||
file: frameFile,
|
||||
methodName: frame.methodName,
|
||||
line: frame.lineNumber ?? 0,
|
||||
column: frame.column,
|
||||
isServer: true,
|
||||
})
|
||||
} catch {}
|
||||
} else {
|
||||
const moduleId = frame.file!.replace(
|
||||
const moduleId = frameFile.replace(
|
||||
/^(webpack-internal:\/\/\/|file:\/\/)/,
|
||||
''
|
||||
)
|
||||
const modulePath = frame.file.replace(
|
||||
const modulePath = frameFile.replace(
|
||||
/^(webpack-internal:\/\/\/|file:\/\/)(\(.*\)\/)?/,
|
||||
''
|
||||
)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { StackFrame } from 'stacktrace-parser'
|
||||
import { parse } from 'stacktrace-parser'
|
||||
|
||||
const regexNextStatic = /\/_next(\/static\/.+)/g
|
||||
const regexNextStatic = /\/_next(\/static\/.+)/
|
||||
|
||||
export function parseStack(stack: string): StackFrame[] {
|
||||
const frames = parse(stack)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import type { IncomingMessage, ServerResponse } from 'http'
|
||||
import type { StackFrame } from 'stacktrace-parser'
|
||||
import type { ParsedUrlQuery } from 'querystring'
|
||||
import type { OriginalStackFrameResponse } from './middleware'
|
||||
|
||||
|
@ -10,29 +9,30 @@ import { launchEditor } from './internal/helpers/launchEditor'
|
|||
|
||||
interface Project {
|
||||
getSourceForAsset(filePath: string): Promise<string | null>
|
||||
traceSource(stackFrame: RustStackFrame): Promise<RustStackFrame | null>
|
||||
traceSource(
|
||||
stackFrame: TurbopackStackFrame
|
||||
): Promise<TurbopackStackFrame | null>
|
||||
}
|
||||
|
||||
interface RustStackFrame {
|
||||
file: string
|
||||
methodName: string | null
|
||||
line: number
|
||||
interface TurbopackStackFrame {
|
||||
column: number | null
|
||||
file: string
|
||||
isServer: boolean
|
||||
line: number
|
||||
methodName: string | null
|
||||
}
|
||||
|
||||
const currentSourcesByFile: Map<string, Promise<string | null>> = new Map()
|
||||
async function batchedTraceSource(project: Project, frame: StackFrame) {
|
||||
const file = frame.file
|
||||
async function batchedTraceSource(
|
||||
project: Project,
|
||||
frame: TurbopackStackFrame
|
||||
) {
|
||||
const file = frame.file ? decodeURIComponent(frame.file) : undefined
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
const sourceFrame = await project.traceSource({
|
||||
file,
|
||||
methodName: frame.methodName,
|
||||
line: frame.lineNumber ?? 0,
|
||||
column: frame.column,
|
||||
})
|
||||
const sourceFrame = await project.traceSource(frame)
|
||||
|
||||
if (!sourceFrame) {
|
||||
return
|
||||
|
@ -60,7 +60,7 @@ async function batchedTraceSource(project: Project, frame: StackFrame) {
|
|||
file: sourceFrame.file,
|
||||
lineNumber: sourceFrame.line,
|
||||
column: sourceFrame.column,
|
||||
methodName: sourceFrame.methodName ?? frame.methodName,
|
||||
methodName: sourceFrame.methodName ?? frame.methodName ?? '<unknown>',
|
||||
arguments: [],
|
||||
},
|
||||
source: source ?? null,
|
||||
|
@ -69,7 +69,7 @@ async function batchedTraceSource(project: Project, frame: StackFrame) {
|
|||
|
||||
export async function createOriginalStackFrame(
|
||||
project: Project,
|
||||
frame: StackFrame
|
||||
frame: TurbopackStackFrame
|
||||
): Promise<OriginalStackFrameResponse | null> {
|
||||
const traced = await batchedTraceSource(project, frame)
|
||||
if (!traced) {
|
||||
|
@ -94,17 +94,15 @@ export async function createOriginalStackFrame(
|
|||
}
|
||||
}
|
||||
|
||||
function stackFrameFromQuery(query: ParsedUrlQuery): StackFrame {
|
||||
function stackFrameFromQuery(query: ParsedUrlQuery): TurbopackStackFrame {
|
||||
return {
|
||||
file: query.file as string,
|
||||
methodName: query.methodName as string,
|
||||
arguments: query.arguments as string[],
|
||||
lineNumber:
|
||||
typeof query.lineNumber === 'string'
|
||||
? parseInt(query.lineNumber, 10)
|
||||
: null,
|
||||
line:
|
||||
typeof query.lineNumber === 'string' ? parseInt(query.lineNumber, 10) : 0,
|
||||
column:
|
||||
typeof query.column === 'string' ? parseInt(query.column, 10) : null,
|
||||
isServer: query.isServer === 'true',
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,7 +156,7 @@ export function getOverlayMiddleware(project: Project) {
|
|||
}
|
||||
|
||||
try {
|
||||
launchEditor(filePath, frame.lineNumber ?? 1, frame.column ?? 1)
|
||||
launchEditor(filePath, frame.line, frame.column ?? 1)
|
||||
} catch (err) {
|
||||
console.log('Failed to launch editor:', err)
|
||||
res.statusCode = 500
|
||||
|
|
|
@ -65,6 +65,11 @@ describe('Read-only source HMR', () => {
|
|||
const originalContent = await fs.readFile(pagePath, 'utf8')
|
||||
const editedContent = originalContent.replace('Hello World', 'COOL page')
|
||||
|
||||
if (process.env.TURBOPACK) {
|
||||
// TODO Turbopack needs a bit to start watching
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
|
||||
await writeReadOnlyFile(pagePath, editedContent)
|
||||
await check(() => getBrowserBodyText(browser), /COOL page/)
|
||||
|
||||
|
@ -86,8 +91,12 @@ describe('Read-only source HMR', () => {
|
|||
|
||||
const originalContent = await fs.readFile(pagePath, 'utf8')
|
||||
|
||||
await fs.remove(pagePath)
|
||||
if (process.env.TURBOPACK) {
|
||||
// TODO Turbopack needs a bit to start watching
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
|
||||
await fs.remove(pagePath)
|
||||
await writeReadOnlyFile(pagePath, originalContent)
|
||||
await check(() => getBrowserBodyText(browser), /Hello World/)
|
||||
} finally {
|
||||
|
|
Loading…
Reference in a new issue