Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code frame and sourcemapped error support for Turbopack #56727

Merged
merged 23 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
998c03d
[WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 11, 2023
b6f5e22
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 11, 2023
ed6c1ad
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 12, 2023
e193bfa
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 12, 2023
964b756
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 12, 2023
75d6d32
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 16, 2023
da8d778
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 16, 2023
14f9129
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 16, 2023
ede8ad8
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 16, 2023
f2069e4
Merge remote-tracking branch 'origin/canary' into wbinnssmith/codeframe
wbinnssmith Oct 16, 2023
8d93a00
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 16, 2023
d70a8f6
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 17, 2023
a3d8856
Revert "fixup! [WIP] Code frame and sourcemapped error support for Tu…
wbinnssmith Oct 17, 2023
f76097e
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 17, 2023
f2da815
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 17, 2023
ced85d6
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 17, 2023
a4ad562
Merge remote-tracking branch 'origin/canary' into wbinnssmith/codeframe
wbinnssmith Oct 17, 2023
3fb2a2c
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 17, 2023
fe432d3
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 18, 2023
8af63bc
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 18, 2023
3307238
Merge branch 'canary' into wbinnssmith/codeframe
kodiakhq[bot] Oct 19, 2023
a9da295
Merge branch 'canary' into wbinnssmith/codeframe
kodiakhq[bot] Oct 19, 2023
002571d
fixup! [WIP] Code frame and sourcemapped error support for Turbopack
wbinnssmith Oct 19, 2023
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
63 changes: 33 additions & 30 deletions packages/next-swc/crates/napi/src/next_api/project.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{io::Read, path::PathBuf, sync::Arc, time::Duration};
use std::{path::PathBuf, sync::Arc, time::Duration};

use anyhow::{anyhow, bail, Context, Result};
use napi::{
Expand Down Expand Up @@ -613,14 +613,6 @@ pub fn project_update_info_subscribe(
Ok(())
}

#[turbo_tasks::value]
#[derive(Debug)]
#[napi(object)]
pub struct TracedSource {
pub frame: StackFrame,
pub source: String,
}

#[turbo_tasks::value]
#[derive(Debug)]
#[napi(object)]
Expand All @@ -635,7 +627,7 @@ pub struct StackFrame {
pub async fn project_trace_source(
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
frame: StackFrame,
) -> napi::Result<Option<TracedSource>> {
) -> napi::Result<Option<StackFrame>> {
let turbo_tasks = project.turbo_tasks.clone();
let traced_frame = turbo_tasks
.run_once(async move {
Expand Down Expand Up @@ -677,10 +669,11 @@ pub async fn project_trace_source(
.root()
.join(chunk_path);

let generatable: Vc<Box<dyn GenerateSourceMap>> =
Vc::try_resolve_sidecast(project.container.get_versioned_content(path))
.await?
.context("Chunk cannot produce a sourcemap")?;
let Some(generatable): Option<Vc<Box<dyn GenerateSourceMap>>> =
Vc::try_resolve_sidecast(project.container.get_versioned_content(path)).await?
else {
return Ok(None);
};

let map = generatable
.generate_source_map()
Expand All @@ -694,40 +687,50 @@ pub async fn project_trace_source(
.context("Unable to trace token from sourcemap")?;

let Token::Original(token) = token else {
bail!("Expected token to be an OriginalToken")
return Ok(None);
};

let Some(source_file) = token.original_file.strip_prefix("/turbopack/[project]/")
else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be derived form somewhere from wherever has these names set, instead of us hardcoding again?

bail!("Original file outside project")
};

Ok(Some(StackFrame {
file: source_file.to_string(),
method_name: token.name,
line: token.original_line as u32,
column: Some(token.original_column as u32),
}))
})
.await
.map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
Ok(traced_frame)
}

#[napi]
pub async fn project_get_source_for_asset(
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
file_path: String,
) -> napi::Result<Option<String>> {
let turbo_tasks = project.turbo_tasks.clone();
let source = turbo_tasks
.run_once(async move {
let source_content = &*project
.container
.project()
.project_path()
.join(source_file.to_string())
.join(file_path.to_string())
.read()
.await?;

let FileContent::Content(source_content) = source_content else {
bail!("No content for source file")
return Ok(None);
};

let mut source = "".to_string();
Read::read_to_string(&mut source_content.read(), &mut source)?;

Ok(Some(TracedSource {
frame: StackFrame {
file: source_file.to_string(),
method_name: token.name,
line: token.original_line as u32,
column: Some(token.original_column as u32),
},
source,
}))
Ok(Some(source_content.content().to_str()?.to_string()))
})
.await
.map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
Ok(traced_frame)

Ok(source)
}
25 changes: 9 additions & 16 deletions packages/next/src/build/swc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,9 +521,9 @@ export interface HmrIdentifiers {

export interface StackFrame {
file: string
methodName: string | null
line: number
column: number | undefined
methodName: string | undefined
column: number | null
}

export interface UpdateInfo {
Expand All @@ -547,13 +547,8 @@ export interface Project {
hmrIdentifiersSubscribe(): AsyncIterableIterator<
TurbopackResult<HmrIdentifiers>
>
traceSource(stackFrame: StackFrame): Promise<
| {
frame: StackFrame
source: string
}
| undefined
>
getSourceForAsset(filePath: string): Promise<string | null>
traceSource(stackFrame: StackFrame): Promise<StackFrame | null>
updateInfoSubscribe(): AsyncIterableIterator<TurbopackResult<UpdateInfo>>
}

Expand Down Expand Up @@ -930,16 +925,14 @@ function bindingToApi(binding: any, _wasm: boolean) {
return subscription
}

traceSource(stackFrame: StackFrame): Promise<
| {
frame: StackFrame
source: string
}
| undefined
> {
traceSource(stackFrame: StackFrame): Promise<StackFrame | null> {
return binding.projectTraceSource(this._nativeProject, stackFrame)
}

getSourceForAsset(filePath: string): Promise<string | null> {
return binding.projectGetSourceForAsset(this._nativeProject, filePath)
}

updateInfoSubscribe() {
const subscription = subscribe<TurbopackResult<UpdateInfo>>(
true,
Expand Down
12 changes: 3 additions & 9 deletions packages/next/src/server/lib/router-utils/setup-dev-bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1061,14 +1061,13 @@ async function startWatcher(opts: SetupOpts) {
await writeOtherManifests()
await writeFontManifest()

const middleware = getOverlayMiddleware(project)

const overlayMiddleware = getOverlayMiddleware(project)
const turbopackHotReloader: NextJsHotReloaderInterface = {
turbopackProject: project,
activeWebpackConfigs: undefined,
serverStats: null,
edgeServerStats: null,
async run(req, _res, _parsedUrl) {
async run(req, res, _parsedUrl) {
// intercept page chunks request and ensure them with turbopack
if (req.url?.startsWith('/_next/static/chunks/pages/')) {
const params = matchNextPageBundleRequest(req.url)
Expand All @@ -1090,12 +1089,7 @@ async function startWatcher(opts: SetupOpts) {
}
}

await new Promise<void>((resolve, reject) => {
middleware(req, _res, (err?: Error) => {
if (err) return reject(err)
resolve()
})
})
await overlayMiddleware(req, res)

// Request was not finished.
return { finished: undefined }
Expand Down
129 changes: 80 additions & 49 deletions packages/react-dev-overlay/src/middleware-turbopack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,57 +9,88 @@ import { codeFrameColumns } from '@babel/code-frame'
import { launchEditor } from './internal/helpers/launchEditor'

interface Project {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how to import these from the next package since that depends on this package...

traceSource(stackFrame: RustStackFrame): Promise<
| {
frame: RustStackFrame
source: string
}
| undefined
>
getSourceForAsset(filePath: string): Promise<string | null>
traceSource(stackFrame: RustStackFrame): Promise<RustStackFrame | null>
}

interface RustStackFrame {
file: string
methodName: string | undefined
methodName: string | null
line: number
column: number | undefined
column: number | null
}

export async function createOriginalStackFrame(
project: Project,
frame: StackFrame
): Promise<OriginalStackFrameResponse | null> {
const source = await project.traceSource({
file: frame.file ?? '<unknown>',
const currentSourcesByFile: Map<string, Promise<string | null>> = new Map()
async function batchedTraceSource(project: Project, frame: StackFrame) {
const file = frame.file
if (!file) {
return
}

const rustStackFrame = {
file,
methodName: frame.methodName,
line: frame.lineNumber ?? 0,
column: frame.column ?? undefined,
})
column: frame.column,
}

if (!source) {
return null
const sourceFrame = await project.traceSource(rustStackFrame)
if (!sourceFrame) {
return
}

let source
// Don't show code frames for node_modules. These can also often be large bundled files.
if (!sourceFrame.file.includes('node_modules')) {
let sourcePromise = currentSourcesByFile.get(sourceFrame.file)
if (!sourcePromise) {
sourcePromise = new Promise((resolve) =>
// Batch reading sources content as this can be quite large, and stacks often reference the same files
setTimeout(resolve, 100)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could change this to immediately execute, and keep in in memory for 100ms after it resolves? That way we don't add a 100ms delay to the actual tracing.

).then(() => project.getSourceForAsset(sourceFrame.file))
currentSourcesByFile.set(sourceFrame.file, sourcePromise)
}

source = await sourcePromise
currentSourcesByFile.delete(sourceFrame.file)
}

return {
originalStackFrame: {
file: source.frame.file,
lineNumber: source.frame.line,
column: source.frame.column ?? null,
methodName: source.frame.methodName ?? frame.methodName,
frame: {
file,
lineNumber: sourceFrame.line,
column: sourceFrame.column,
methodName: sourceFrame.methodName ?? frame.methodName,
arguments: [],
},
originalCodeFrame: source.frame.file.includes('node_modules')
? null
: codeFrameColumns(
source.source,
{
start: {
line: source.frame.line,
column: source.frame.column ?? 0,
source: source ?? null,
}
}

export async function createOriginalStackFrame(
project: Project,
frame: StackFrame
): Promise<OriginalStackFrameResponse | null> {
const traced = await batchedTraceSource(project, frame)
if (!traced) {
return null
}

return {
originalStackFrame: traced.frame,
originalCodeFrame:
traced.source === null || traced.frame.file.includes('node_modules')
? null
: codeFrameColumns(
traced.source,
{
start: {
line: traced.frame.lineNumber,
column: traced.frame.column ?? 0,
},
},
},
{ forceColor: true }
),
{ forceColor: true }
),
}
}

Expand All @@ -78,11 +109,7 @@ function stackFrameFromQuery(query: ParsedUrlQuery): StackFrame {
}

export function getOverlayMiddleware(project: Project) {
return async function (
req: IncomingMessage,
res: ServerResponse,
next: (error?: Error) => unknown
) {
return async function (req: IncomingMessage, res: ServerResponse) {
const { pathname, query } = url.parse(req.url!, true)
if (pathname === '/__nextjs_original-stack-frame') {
const frame = stackFrameFromQuery(query)
Expand All @@ -92,27 +119,31 @@ export function getOverlayMiddleware(project: Project) {
} catch (e: any) {
res.statusCode = 500
res.write(e.message)
return res.end()
res.end()
return
}

if (originalStackFrame === null) {
res.statusCode = 404
res.write('Unable to resolve sourcemap')
return res.end()
res.end()
return
}

res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.write(Buffer.from(JSON.stringify(originalStackFrame)))
return res.end()
res.end()
return
} else if (pathname === '/__nextjs_launch-editor') {
const frame = stackFrameFromQuery(query)

const filePath = frame.file?.toString()
if (filePath === undefined) {
res.statusCode = 400
res.write('Bad Request')
return res.end()
res.end()
return
}

const fileExists = await fs.access(filePath, FS.F_OK).then(
Expand All @@ -122,7 +153,8 @@ export function getOverlayMiddleware(project: Project) {
if (!fileExists) {
res.statusCode = 204
res.write('No Content')
return res.end()
res.end()
return
}

try {
Expand All @@ -131,13 +163,12 @@ export function getOverlayMiddleware(project: Project) {
console.log('Failed to launch editor:', err)
res.statusCode = 500
res.write('Internal Server Error')
return res.end()
res.end()
return
}

res.statusCode = 204
return res.end()
res.end()
}

return next()
}
}
Loading