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

Add source map support for server components/actions in the browser #71042

Merged
merged 29 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3b25dfb
Extract resuable `getSource` function from `getOverlayMiddleware`
unstubbable Oct 6, 2024
9698f9c
Implement source map dev middleware for Webpack
unstubbable Oct 6, 2024
055bb1e
Support loading `.map` files
unstubbable Oct 6, 2024
82730ff
Add example warning to test component stack
unstubbable Oct 6, 2024
21fb9ff
Support loading map files for `/_next/static` resources
unstubbable Oct 8, 2024
fea812a
Remove `findSourceMapURL` mock
unstubbable Oct 8, 2024
612585f
Use a separate server action for client page
unstubbable Oct 8, 2024
5f7bebd
Simplify warning in server component
unstubbable Oct 8, 2024
6ae7701
Do not tree-shake server actions in dev mode
unstubbable Oct 8, 2024
326acd1
Avoid creating a stack frame object if middleware does not match
unstubbable Oct 8, 2024
da142a0
Fix lookup of client module ids
unstubbable Oct 9, 2024
1a642b7
Update actions-simple README
unstubbable Oct 9, 2024
f16e219
Add inline server action to server component page
unstubbable Oct 9, 2024
8029fb8
In production, omit `findSourceMapURL` from main chunk
unstubbable Oct 9, 2024
e4cf8f9
Re-add accidentally deleted export of `parseStack`
unstubbable Oct 10, 2024
aa13253
Return `400` if `filename` param is missing
unstubbable Oct 10, 2024
9cb8afa
Add source map middleware to Turbopack hot reloader
unstubbable Oct 10, 2024
15818f3
Fix dev overlay code frame for Webpack
unstubbable Oct 10, 2024
e9ce423
Fix dev overlay code frame for Turbopack
unstubbable Oct 10, 2024
659d058
Move `createStackFrame` back to middleware module
unstubbable Oct 11, 2024
e32a7ab
Remove redundant regex replacement
unstubbable Oct 11, 2024
7d361c0
Add example comments to regex replacements
unstubbable Oct 11, 2024
5693840
Move and rename test app
unstubbable Oct 11, 2024
8f09588
Propagate errors in `getSourceMapFromFile`
unstubbable Oct 11, 2024
499327c
Don't assume a React environment name matches `\w+`
unstubbable Oct 11, 2024
87a3224
Skip loading stack frames if filename is `file://`
unstubbable Oct 11, 2024
08199df
Remove `file://` from filenames before reading from fs
unstubbable Oct 11, 2024
efdee01
Include filename in error of source map can't be parsed
unstubbable Oct 11, 2024
2112a2c
Fix existence check for `originalStackFrameResponse`
unstubbable Oct 11, 2024
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
140 changes: 82 additions & 58 deletions crates/napi/src/next_api/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use turbopack_core::{
diagnostics::PlainDiagnostic,
error::PrettyPrintError,
issue::PlainIssue,
source_map::Token,
source_map::{SourceMap, Token},
version::{PartialUpdate, TotalUpdate, Update, VersionState},
SOURCE_MAP_PREFIX,
};
Expand Down Expand Up @@ -1002,74 +1002,76 @@ pub struct StackFrame {
pub method_name: Option<String>,
}

pub async fn get_source_map(
container: Vc<ProjectContainer>,
file_path: String,
) -> Result<Option<Vc<SourceMap>>> {
let (file, module) = match Url::parse(&file_path) {
Ok(url) => match url.scheme() {
"file" => {
let path = urlencoding::decode(url.path())?.to_string();
let module = url.query_pairs().find(|(k, _)| k == "id");
(
path,
match module {
Some(module) => Some(urlencoding::decode(&module.1)?.into_owned().into()),
None => None,
},
)
}
_ => bail!("Unknown url scheme"),
},
Err(_) => (file_path.to_string(), None),
};

let Some(chunk_base) = file.strip_prefix(
&(format!(
"{}/{}/",
container.project().await?.project_path,
container.project().dist_dir().await?
)),
) else {
// File doesn't exist within the dist dir
return Ok(None);
};

let server_path = container.project().node_root().join(chunk_base.into());

let client_path = container
.project()
.client_relative_path()
.join(chunk_base.into());

let mut map = container
.get_source_map(server_path, module.clone())
.await?;

if map.is_none() {
// If the chunk doesn't exist as a server chunk, try a client chunk.
// TODO: Properly tag all server chunks and use the `isServer` query param.
// Currently, this is inaccurate as it does not cover RSC server
// chunks.
map = container.get_source_map(client_path, module).await?;
}

let map = map.context("chunk/module is missing a sourcemap")?;

Ok(Some(map))
}

#[napi]
pub async fn project_trace_source(
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
frame: StackFrame,
) -> napi::Result<Option<StackFrame>> {
let turbo_tasks = project.turbo_tasks.clone();
let container = project.container;
let traced_frame = turbo_tasks
.run_once(async move {
let (file, module) = match Url::parse(&frame.file) {
Ok(url) => match url.scheme() {
"file" => {
let path = urlencoding::decode(url.path())?.to_string();
let module = url.query_pairs().find(|(k, _)| k == "id");
(
path,
match module {
Some(module) => {
Some(urlencoding::decode(&module.1)?.into_owned().into())
}
None => None,
},
)
}
_ => bail!("Unknown url scheme"),
},
Err(_) => (frame.file.to_string(), None),
};

let Some(chunk_base) = file.strip_prefix(
&(format!(
"{}/{}/",
project.container.project().await?.project_path,
project.container.project().dist_dir().await?
)),
) else {
// File doesn't exist within the dist dir
let Some(map) = get_source_map(container, frame.file).await? else {
return Ok(None);
};

let server_path = project
.container
.project()
.node_root()
.join(chunk_base.into());

let client_path = project
.container
.project()
.client_relative_path()
.join(chunk_base.into());

let mut map = project
.container
.get_source_map(server_path, module.clone())
.await?;

if map.is_none() {
// If the chunk doesn't exist as a server chunk, try a client chunk.
// TODO: Properly tag all server chunks and use the `isServer` query param.
// Currently, this is inaccurate as it does not cover RSC server
// chunks.
map = project
.container
.get_source_map(client_path, module)
.await?;
}
let map = map.context("chunk/module is missing a sourcemap")?;

let Some(line) = frame.line else {
return Ok(None);
};
Expand Down Expand Up @@ -1152,6 +1154,28 @@ pub async fn project_get_source_for_asset(
Ok(source)
}

#[napi]
pub async fn project_get_source_map(
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
file_path: String,
) -> napi::Result<Option<String>> {
let turbo_tasks = project.turbo_tasks.clone();
let container = project.container;

let source_map = turbo_tasks
.run_once(async move {
let Some(map) = get_source_map(container, file_path).await? else {
return Ok(None);
};

Ok(Some(map.to_rope().await?.to_str()?.to_string()))
})
.await
.map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;

Ok(source_map)
}

/// Runs exit handlers for the project registered using the [`ExitHandler`] API.
#[napi]
pub async fn project_on_exit(
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/build/swc/generated-native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ export interface StackFrame {
column?: number
methodName?: string
}
export function projectGetSourceMap(
project: { __napiType: 'Project' },
filePath: string
): Promise<string | null>
export function projectTraceSource(
project: { __napiType: 'Project' },
frame: StackFrame
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/build/swc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,10 @@ function bindingToApi(
return binding.projectGetSourceForAsset(this._nativeProject, filePath)
}

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

updateInfoSubscribe(aggregationMs: number) {
return subscribe<TurbopackResult<UpdateMessage>>(true, async (callback) =>
binding.projectUpdateInfoSubscribe(
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/build/swc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ export interface Project {

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

getSourceMap(filePath: string): Promise<string | null>

traceSource(
stackFrame: TurbopackStackFrame
): Promise<TurbopackStackFrame | null>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ const flightClientModuleLoader: webpack.LoaderDefinitionFunction =
const buildInfo = getModuleBuildInfo(this._module)
buildInfo.rsc = getRSCModuleInformation(source, false)

// This is a server action entry module in the client layer. We need to create
// re-exports of "virtual modules" to expose the reference IDs to the client
// separately so they won't be always in the same one module which is not
// splittable.
if (buildInfo.rsc.actionIds) {
// This is a server action entry module in the client layer. We need to
// create re-exports of "virtual modules" to expose the reference IDs to the
// client separately so they won't be always in the same one module which is
// not splittable. This server action module tree shaking is only applied in
// production mode. In development mode, we want to preserve the original
// modules (as transformed by SWC) to ensure that source mapping works.
if (buildInfo.rsc.actionIds && process.env.NODE_ENV === 'production') {
return Object.entries(buildInfo.rsc.actionIds)
.map(([id, name]) => {
return `export { ${name} } from 'next-flight-server-reference-proxy-loader?id=${id}&name=${name}!'`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,29 +51,40 @@ async function getSourceFrame(
try {
const loc =
input.loc || input.dependencies.map((d: any) => d.loc).filter(Boolean)[0]
const originalSource = input.module.originalSource()

const result = await createOriginalStackFrame({
source: originalSource,
rootDirectory: compilation.options.context!,
modulePath: fileName,
frame: {
arguments: [],
file: fileName,
methodName: '',
lineNumber: loc.start.line,
column: loc.start.column,
},
})

return {
frame: result?.originalCodeFrame ?? '',
lineNumber: result?.originalStackFrame?.lineNumber?.toString() ?? '',
column: result?.originalStackFrame?.column?.toString() ?? '',
const module = input.module as webpack.Module
const originalSource = module.originalSource()
const sourceMap = originalSource?.map() ?? undefined

if (sourceMap) {
const moduleId = compilation.chunkGraph.getModuleId(module)

const result = await createOriginalStackFrame({
source: {
type: 'bundle',
sourceMap,
compilation,
moduleId,
modulePath: fileName,
},
rootDirectory: compilation.options.context!,
frame: {
arguments: [],
file: fileName,
methodName: '',
lineNumber: loc.start.line,
column: loc.start.column,
},
})

return {
frame: result?.originalCodeFrame ?? '',
lineNumber: result?.originalStackFrame?.lineNumber?.toString() ?? '',
column: result?.originalStackFrame?.column?.toString() ?? '',
}
}
} catch {
return { frame: '', lineNumber: '', column: '' }
}
} catch {}

return { frame: '', lineNumber: '', column: '' }
}

function getFormattedFileName(
Expand Down
20 changes: 16 additions & 4 deletions packages/next/src/client/app-find-source-map-url.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
// TODO: Will be implemented later.
export function findSourceMapURL(_filename: string): string | null {
return null
}
const basePath = process.env.__NEXT_ROUTER_BASEPATH || ''
const pathname = `${basePath}/__nextjs_source-map`

export const findSourceMapURL =
process.env.NODE_ENV === 'development'
? function findSourceMapURL(filename: string): string | null {
eps1lon marked this conversation as resolved.
Show resolved Hide resolved
const url = new URL(pathname, document.location.origin)

url.searchParams.set(
'filename',
filename.replace(new RegExp(`^${document.location.origin}`), '')
)

return url.href
}
: undefined
4 changes: 1 addition & 3 deletions packages/next/src/client/app-index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createFromReadableStream } from 'react-server-dom-webpack/client'
import { HeadManagerContext } from '../shared/lib/head-manager-context.shared-runtime'
import { onRecoverableError } from './on-recoverable-error'
import { callServer } from './app-call-server'
import { findSourceMapURL } from './app-find-source-map-url'
import {
type AppRouterActionQueue,
createMutableActionQueue,
Expand All @@ -20,9 +21,6 @@ import type { InitialRSCPayload } from '../server/app-render/types'
import { createInitialRouterState } from './components/router-reducer/create-initial-router-state'
import { MissingSlotContext } from '../shared/lib/app-router-context.shared-runtime'

// Importing from dist so that we can define an alias if needed.
import { findSourceMapURL } from 'next/dist/client/app-find-source-map-url'

/// <reference types="react-dom/experimental" />

const appElement: HTMLElement | Document | null = document
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BuildError } from '../internal/container/BuildError'
import { Errors } from '../internal/container/Errors'
import { StaticIndicator } from '../internal/container/StaticIndicator'
import type { SupportedErrorEvent } from '../internal/container/Errors'
import { parseStack } from '../internal/helpers/parseStack'
import { parseStack } from '../internal/helpers/parse-stack'
import { Base } from '../internal/styles/Base'
import { ComponentStyles } from '../internal/styles/ComponentStyles'
import { CssReset } from '../internal/styles/CssReset'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
ACTION_VERSION_INFO,
useErrorOverlayReducer,
} from '../shared'
import { parseStack } from '../internal/helpers/parseStack'
import { parseStack } from '../internal/helpers/parse-stack'
import ReactDevOverlay from './ReactDevOverlay'
import { useErrorHandler } from '../internal/helpers/use-error-handler'
import { RuntimeErrorHandler } from '../internal/helpers/runtime-error-handler'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
import { LeftRightDialogHeader } from '../components/LeftRightDialogHeader'
import { Overlay } from '../components/Overlay'
import { Toast } from '../components/Toast'
import { getErrorByType } from '../helpers/getErrorByType'
import type { ReadyRuntimeError } from '../helpers/getErrorByType'
import { getErrorByType } from '../helpers/get-error-by-type'
import type { ReadyRuntimeError } from '../helpers/get-error-by-type'
import { noop as css } from '../helpers/noop-template'
import { CloseIcon } from '../icons/CloseIcon'
import { RuntimeError } from './RuntimeError'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react'
import { CodeFrame } from '../../components/CodeFrame'
import type { ReadyRuntimeError } from '../../helpers/getErrorByType'
import type { ReadyRuntimeError } from '../../helpers/get-error-by-type'
import { noop as css } from '../../helpers/noop-template'
import { groupStackFramesByFramework } from '../../helpers/group-stack-frames-by-framework'
import { GroupedStackFrames } from './GroupedStackFrames'
Expand Down
Loading
Loading