From a572740f46d1735992dee0a0de8247c8dd3c10f5 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 4 Oct 2024 17:31:04 +0200 Subject: [PATCH] Create spans for `createServerReference` and `registerServerReference` (#70564) Creating proper source location spans for `createServerReference` and `registerServerReference` is the next step in enabling source mapping of server actions. With the added e2e test app, we can already verify that this works by mocking `findSourceMapURL`. Properly implementing `findSourceMapURL` will be the last missing piece to complete the puzzle. server action go to definition server action source source map viz server source map viz client --- .../src/transforms/server_actions.rs | 137 ++++++++++-------- .../action-client-wrapper.ts | 3 +- .../src/client/app-find-source-map-url.ts | 4 + packages/next/src/client/app-index.tsx | 4 + .../router-reducer/fetch-server-response.ts | 7 +- .../reducers/server-action-reducer.ts | 8 +- packages/next/types/$$compiled.internal.d.ts | 10 +- test/e2e/app-dir/actions-simple/README.md | 22 +++ .../actions-simple/actions-simple.test.ts | 28 ++++ .../e2e/app-dir/actions-simple/app/actions.ts | 5 + .../actions-simple/app/client/page.tsx | 15 ++ test/e2e/app-dir/actions-simple/app/form.tsx | 14 ++ .../e2e/app-dir/actions-simple/app/layout.tsx | 8 + test/e2e/app-dir/actions-simple/app/page.tsx | 13 ++ .../app/source-maps-turbopack/route.ts | 18 +++ .../find-source-map-url-turbopack-mock.ts | 10 ++ .../find-source-map-url-webpack-mock.ts | 4 + .../e2e/app-dir/actions-simple/next.config.js | 22 +++ 18 files changed, 262 insertions(+), 70 deletions(-) create mode 100644 packages/next/src/client/app-find-source-map-url.ts create mode 100644 test/e2e/app-dir/actions-simple/README.md create mode 100644 test/e2e/app-dir/actions-simple/actions-simple.test.ts create mode 100644 test/e2e/app-dir/actions-simple/app/actions.ts create mode 100644 test/e2e/app-dir/actions-simple/app/client/page.tsx create mode 100644 test/e2e/app-dir/actions-simple/app/form.tsx create mode 100644 test/e2e/app-dir/actions-simple/app/layout.tsx create mode 100644 test/e2e/app-dir/actions-simple/app/page.tsx create mode 100644 test/e2e/app-dir/actions-simple/app/source-maps-turbopack/route.ts create mode 100644 test/e2e/app-dir/actions-simple/find-source-map-url-turbopack-mock.ts create mode 100644 test/e2e/app-dir/actions-simple/find-source-map-url-webpack-mock.ts create mode 100644 test/e2e/app-dir/actions-simple/next.config.js diff --git a/crates/next-custom-transforms/src/transforms/server_actions.rs b/crates/next-custom-transforms/src/transforms/server_actions.rs index bf6d385fe068d..57557f430f875 100644 --- a/crates/next-custom-transforms/src/transforms/server_actions.rs +++ b/crates/next-custom-transforms/src/transforms/server_actions.rs @@ -105,7 +105,7 @@ struct ServerActions { should_track_names: bool, names: Vec, - declared_idents: Vec, + declared_idents: Vec, // This flag allows us to rewrite `function foo() {}` to `const foo = createProxy(...)`. rewrite_fn_decl_to_proxy_decl: Option, @@ -113,7 +113,7 @@ struct ServerActions { rewrite_expr_to_proxy_expr: Option>, // (ident, export name) - exported_idents: Vec<(Id, String)>, + exported_idents: Vec<(Ident, String)>, annotations: Vec, extra_items: Vec, @@ -193,7 +193,7 @@ impl ServerActions { self.has_action = true; self.export_actions.push(action_name.to_string()); - let action_ident = Ident::new(action_name.clone(), DUMMY_SP, self.private_ctxt); + let action_ident = Ident::new(action_name.clone(), arrow.span, self.private_ctxt); let register_action_expr = annotate_ident_as_server_reference( action_ident.clone(), @@ -347,7 +347,7 @@ impl ServerActions { self.has_action = true; self.export_actions.push(action_name.to_string()); - let action_ident = Ident::new(action_name.clone(), DUMMY_SP, self.private_ctxt); + let action_ident = Ident::new(action_name.clone(), function.span, self.private_ctxt); let register_action_expr = annotate_ident_as_server_reference( action_ident.clone(), @@ -1210,14 +1210,16 @@ impl VisitMut for ServerActions { Decl::Fn(f) => { // export function foo() {} self.exported_idents - .push((f.ident.to_id(), f.ident.sym.to_string())); + .push((f.ident.clone(), f.ident.sym.to_string())); } Decl::Var(var) => { // export const foo = 1 - let mut ids: Vec = Vec::new(); - collect_idents_in_var_decls(&var.decls, &mut ids); + let mut idents: Vec = Vec::new(); + collect_idents_in_var_decls(&var.decls, &mut idents); self.exported_idents.extend( - ids.into_iter().map(|id| (id.clone(), id.0.to_string())), + idents + .into_iter() + .map(|ident| (ident.clone(), ident.to_id().0.to_string())), ); for decl in &mut var.decls { @@ -1251,16 +1253,16 @@ impl VisitMut for ServerActions { { // export { foo as bar } self.exported_idents - .push((ident.to_id(), sym.to_string())); + .push((ident.clone(), sym.to_string())); } else if let ModuleExportName::Str(str) = export_name { // export { foo as "bar" } self.exported_idents - .push((ident.to_id(), str.value.to_string())); + .push((ident.clone(), str.value.to_string())); } } else { // export { foo } self.exported_idents - .push((ident.to_id(), ident.sym.to_string())); + .push((ident.clone(), ident.sym.to_string())); } } else { disallowed_export_span = named.span; @@ -1276,17 +1278,21 @@ impl VisitMut for ServerActions { DefaultDecl::Fn(f) => { if let Some(ident) = &f.ident { // export default function foo() {} - self.exported_idents.push((ident.to_id(), "default".into())); + self.exported_idents.push((ident.clone(), "default".into())); } else { // export default function() {} + // Use the span from the function expression + let span = f.function.span; + let new_ident = Ident::new( gen_action_ident(&mut self.reference_index), - DUMMY_SP, + span, self.private_ctxt, ); + f.ident = Some(new_ident.clone()); - self.exported_idents - .push((new_ident.to_id(), "default".into())); + + self.exported_idents.push((new_ident, "default".into())); } } _ => { @@ -1301,14 +1307,17 @@ impl VisitMut for ServerActions { disallowed_export_span = default_expr.span; } else { // export default async () => {} + // Use the span of the arrow function + let span = arrow.span; + let new_ident = Ident::new( gen_action_ident(&mut self.reference_index), - DUMMY_SP, + span, self.private_ctxt, ); self.exported_idents - .push((new_ident.to_id(), "default".into())); + .push((new_ident.clone(), "default".into())); *default_expr.expr = attach_name_to_expr( new_ident, @@ -1319,18 +1328,21 @@ impl VisitMut for ServerActions { } Expr::Ident(ident) => { // export default foo - self.exported_idents.push((ident.to_id(), "default".into())); + self.exported_idents.push((ident.clone(), "default".into())); } Expr::Call(call) => { // export default fn() + // Determining a useful span here is tricky. + let span = call.span; + let new_ident = Ident::new( gen_action_ident(&mut self.reference_index), - DUMMY_SP, + span, self.private_ctxt, ); self.exported_idents - .push((new_ident.to_id(), "default".into())); + .push((new_ident.clone(), "default".into())); *default_expr.expr = attach_name_to_expr( new_ident, @@ -1433,21 +1445,19 @@ impl VisitMut for ServerActions { new.rotate_right(1); } - for (id, export_name) in self.exported_idents.iter() { - let ident = Ident::new(id.0.clone(), DUMMY_SP, id.1); - + for (ident, export_name) in self.exported_idents.iter() { if !self.config.is_react_server_layer { let action_id = generate_action_id(&self.config.hash_salt, &self.file_name, export_name); - let span = Span::dummy_with_cmt(); - self.comments.add_pure_comment(span.lo); + let call_expr_span = Span::dummy_with_cmt(); + self.comments.add_pure_comment(call_expr_span.lo); if export_name == "default" { let export_expr = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr( ExportDefaultExpr { - span: DUMMY_SP, + span: ident.span, expr: Box::new(Expr::Call(CallExpr { - span, + span: call_expr_span, callee: Callee::Expr(Box::new(Expr::Ident( create_ref_ident.clone(), ))), @@ -1473,11 +1483,11 @@ impl VisitMut for ServerActions { decls: vec![VarDeclarator { span: DUMMY_SP, name: Pat::Ident( - IdentName::new(export_name.clone().into(), DUMMY_SP) + IdentName::new(export_name.clone().into(), ident.span) .into(), ), init: Some(Box::new(Expr::Call(CallExpr { - span, + span: call_expr_span, callee: Callee::Expr(Box::new(Expr::Ident( create_ref_ident.clone(), ))), @@ -1549,14 +1559,10 @@ impl VisitMut for ServerActions { elems: self .exported_idents .iter() - .map(|e| { + .map(|(ident, _span)| { Some(ExprOrSpread { spread: None, - expr: Box::new(Expr::Ident(Ident::new( - e.0 .0.clone(), - DUMMY_SP, - e.0 .1, - ))), + expr: Box::new(Expr::Ident(ident.clone())), }) }) .collect(), @@ -1701,7 +1707,10 @@ impl VisitMut for ServerActions { noop_visit_mut_type!(); } -fn retain_names_from_declared_idents(child_names: &mut Vec, current_declared_idents: &[Id]) { +fn retain_names_from_declared_idents( + child_names: &mut Vec, + current_declared_idents: &[Ident], +) { // Collect the names to retain in a separate vector let mut retained_names = Vec::new(); @@ -1733,7 +1742,9 @@ fn retain_names_from_declared_idents(child_names: &mut Vec, current_declar } if should_retain - && current_declared_idents.contains(&name.0) + && current_declared_idents + .iter() + .any(|ident| ident.to_id() == name.0) && !retained_names.contains(name) { retained_names.push(name.clone()); @@ -1826,7 +1837,7 @@ fn annotate_ident_as_server_reference( ) -> Expr { // registerServerReference(reference, id, null) let proxy_expr = Expr::Call(CallExpr { - span: DUMMY_SP, + span: ident.span, callee: quote_ident!("registerServerReference").as_callee(), args: vec![ ExprOrSpread { @@ -2198,96 +2209,100 @@ fn remove_server_directive_index_in_fn( }); } -fn collect_idents_in_array_pat(elems: &[Option], ids: &mut Vec) { +fn collect_idents_in_array_pat(elems: &[Option], idents: &mut Vec) { for elem in elems.iter().flatten() { match elem { Pat::Ident(ident) => { - ids.push(ident.id.to_id()); + idents.push(ident.id.clone()); } Pat::Array(array) => { - collect_idents_in_array_pat(&array.elems, ids); + collect_idents_in_array_pat(&array.elems, idents); } Pat::Object(object) => { - collect_idents_in_object_pat(&object.props, ids); + collect_idents_in_object_pat(&object.props, idents); } Pat::Rest(rest) => { if let Pat::Ident(ident) = &*rest.arg { - ids.push(ident.id.to_id()); + idents.push(ident.id.clone()); } } Pat::Assign(AssignPat { left, .. }) => { - collect_idents_in_pat(left, ids); + collect_idents_in_pat(left, idents); } Pat::Expr(..) | Pat::Invalid(..) => {} } } } -fn collect_idents_in_object_pat(props: &[ObjectPatProp], ids: &mut Vec) { +fn collect_idents_in_object_pat(props: &[ObjectPatProp], idents: &mut Vec) { for prop in props { match prop { ObjectPatProp::KeyValue(KeyValuePatProp { key, value }) => { if let PropName::Ident(ident) = key { - ids.push((ident.sym.clone(), SyntaxContext::empty())); + idents.push(Ident::new( + ident.sym.clone(), + ident.span, + SyntaxContext::empty(), + )); } match &**value { Pat::Ident(ident) => { - ids.push(ident.id.to_id()); + idents.push(ident.id.clone()); } Pat::Array(array) => { - collect_idents_in_array_pat(&array.elems, ids); + collect_idents_in_array_pat(&array.elems, idents); } Pat::Object(object) => { - collect_idents_in_object_pat(&object.props, ids); + collect_idents_in_object_pat(&object.props, idents); } _ => {} } } ObjectPatProp::Assign(AssignPatProp { key, .. }) => { - ids.push(key.to_id()); + idents.push(key.id.clone()); } ObjectPatProp::Rest(RestPat { arg, .. }) => { if let Pat::Ident(ident) = &**arg { - ids.push(ident.id.to_id()); + idents.push(ident.id.clone()); } } } } } -fn collect_idents_in_var_decls(decls: &[VarDeclarator], ids: &mut Vec) { +fn collect_idents_in_var_decls(decls: &[VarDeclarator], idents: &mut Vec) { for decl in decls { - collect_idents_in_pat(&decl.name, ids); + collect_idents_in_pat(&decl.name, idents); } } -fn collect_idents_in_pat(pat: &Pat, ids: &mut Vec) { +fn collect_idents_in_pat(pat: &Pat, idents: &mut Vec) { match pat { Pat::Ident(ident) => { - ids.push(ident.id.to_id()); + idents.push(ident.id.clone()); } Pat::Array(array) => { - collect_idents_in_array_pat(&array.elems, ids); + collect_idents_in_array_pat(&array.elems, idents); } Pat::Object(object) => { - collect_idents_in_object_pat(&object.props, ids); + collect_idents_in_object_pat(&object.props, idents); } Pat::Assign(AssignPat { left, .. }) => { - collect_idents_in_pat(left, ids); + collect_idents_in_pat(left, idents); } Pat::Rest(RestPat { arg, .. }) => { if let Pat::Ident(ident) = &**arg { - ids.push(ident.id.to_id()); + idents.push(ident.id.clone()); } } Pat::Expr(..) | Pat::Invalid(..) => {} } } -fn collect_decl_idents_in_stmt(stmt: &Stmt, ids: &mut Vec) { +fn collect_decl_idents_in_stmt(stmt: &Stmt, idents: &mut Vec) { if let Stmt::Decl(Decl::Var(var)) = &stmt { - collect_idents_in_var_decls(&var.decls, ids); + collect_idents_in_var_decls(&var.decls, idents); } } diff --git a/packages/next/src/build/webpack/loaders/next-flight-loader/action-client-wrapper.ts b/packages/next/src/build/webpack/loaders/next-flight-loader/action-client-wrapper.ts index 38e54b9effa5b..6adc513bd1c90 100644 --- a/packages/next/src/build/webpack/loaders/next-flight-loader/action-client-wrapper.ts +++ b/packages/next/src/build/webpack/loaders/next-flight-loader/action-client-wrapper.ts @@ -2,6 +2,7 @@ // imported by the server. export { callServer } from 'next/dist/client/app-call-server' +export { findSourceMapURL } from 'next/dist/client/app-find-source-map-url' // A noop wrapper to let the Flight client create the server reference. // See also: https://github.com/facebook/react/pull/26632 @@ -16,5 +17,3 @@ export const createServerReference = ( : // eslint-disable-next-line import/no-extraneous-dependencies require('react-server-dom-webpack/client')) as typeof import('react-server-dom-webpack/client') ).createServerReference - -export const findSourceMapURL = undefined diff --git a/packages/next/src/client/app-find-source-map-url.ts b/packages/next/src/client/app-find-source-map-url.ts new file mode 100644 index 0000000000000..de3c4a1792ccd --- /dev/null +++ b/packages/next/src/client/app-find-source-map-url.ts @@ -0,0 +1,4 @@ +// TODO: Will be implemented later. +export function findSourceMapURL(_filename: string): string | null { + return null +} diff --git a/packages/next/src/client/app-index.tsx b/packages/next/src/client/app-index.tsx index bd5f0a789174b..eb8964d70a705 100644 --- a/packages/next/src/client/app-index.tsx +++ b/packages/next/src/client/app-index.tsx @@ -20,6 +20,9 @@ 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' + /// const appElement: HTMLElement | Document | null = document @@ -140,6 +143,7 @@ const readable = new ReadableStream({ const initialServerResponse = createFromReadableStream(readable, { callServer, + findSourceMapURL, }) // React overrides `.then` and doesn't return a new promise chain, diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index 4fa93362a23ee..53b508544a06f 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -33,6 +33,9 @@ import { type NormalizedFlightData, } from '../../flight-data-helpers' +// Importing from dist so that we can define an alias if needed. +import { findSourceMapURL } from 'next/dist/client/app-find-source-map-url' + export interface FetchServerResponseOptions { readonly flightRouterState: FlightRouterState readonly nextUrl: string | null @@ -208,9 +211,7 @@ export async function fetchServerResponse( // Handle the `fetch` readable stream that can be unwrapped by `React.use`. const response: NavigationFlightResponse = await createFromFetch( Promise.resolve(res), - { - callServer, - } + { callServer, findSourceMapURL } ) if (buildId !== response.b) { diff --git a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts index 3d693ac9e0ccc..13fb34bfdce09 100644 --- a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts @@ -10,6 +10,10 @@ import { NEXT_URL, RSC_CONTENT_TYPE_HEADER, } from '../../app-router-headers' + +// Importing from dist so that we can define an alias if needed. +import { findSourceMapURL } from 'next/dist/client/app-find-source-map-url' + // // eslint-disable-next-line import/no-extraneous-dependencies // import { createFromFetch } from 'react-server-dom-webpack/client' // // eslint-disable-next-line import/no-extraneous-dependencies @@ -138,9 +142,7 @@ async function fetchServerAction( if (contentType?.startsWith(RSC_CONTENT_TYPE_HEADER)) { const response: ActionFlightResponse = await createFromFetch( Promise.resolve(res), - { - callServer, - } + { callServer, findSourceMapURL } ) if (location) { diff --git a/packages/next/types/$$compiled.internal.d.ts b/packages/next/types/$$compiled.internal.d.ts index 7b629c14609f9..c26ba711b7e40 100644 --- a/packages/next/types/$$compiled.internal.d.ts +++ b/packages/next/types/$$compiled.internal.d.ts @@ -21,7 +21,15 @@ declare module 'next/dist/compiled/react-server-dom-turbopack/client.browser' declare module 'next/dist/compiled/react-server-dom-turbopack/server.browser' declare module 'next/dist/compiled/react-server-dom-turbopack/server.edge' declare module 'next/dist/compiled/react-server-dom-turbopack/static.edge' -declare module 'next/dist/client/app-call-server' +declare module 'next/dist/client/app-call-server' { + export function callServer( + actionId: string, + actionArgs: unknown[] + ): Promise +} +declare module 'next/dist/client/app-find-source-map-url' { + export function findSourceMapURL(filename: string): string | null +} declare module 'next/dist/compiled/react-dom/server' declare module 'next/dist/compiled/react-dom/server.edge' declare module 'next/dist/compiled/browserslist' diff --git a/test/e2e/app-dir/actions-simple/README.md b/test/e2e/app-dir/actions-simple/README.md new file mode 100644 index 0000000000000..48ef4c869b7bb --- /dev/null +++ b/test/e2e/app-dir/actions-simple/README.md @@ -0,0 +1,22 @@ +The main purpose of this end-to-end test app is to allow manual testing of +server action source mapping within the React DevTools. + +Until we have properly implemented `findSourceMapURL` in Next.js, this demo only +works with Turbopack. This is because we can mock `findSourceMapURL` for the +test app, as Turbopack generates source map files, whereas Webpack uses +`eval-source-map`. + +For client bundles, the source map files are served directly through +`/_next/static/chunks`, and for server bundles, the source map files are read +from disk and served through the `/source-maps-turbopack` route handler. + +To check the source mapping of server actions, follow these steps: + +1. Run `pnpm next dev --turbo test/e2e/app-dir/actions-simple`. +2. Go to [http://localhost:3000]() or [http://localhost:3000/client](). +3. Open the Components panel of the React DevTools. +4. Select the `Form` element. +5. In the props section, right-click on the `action` prop and select "Go to + definition" (sometimes it needs two tries). +6. You should end up in the Chrome DevTools Sources panel with the `actions.ts` + file open and the cursor at `foo()`. diff --git a/test/e2e/app-dir/actions-simple/actions-simple.test.ts b/test/e2e/app-dir/actions-simple/actions-simple.test.ts new file mode 100644 index 0000000000000..309b4d96eb2a9 --- /dev/null +++ b/test/e2e/app-dir/actions-simple/actions-simple.test.ts @@ -0,0 +1,28 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('actions-simple', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should work with server actions passed to client components', async () => { + const browser = await next.browser('/') + expect(await browser.elementByCss('p').text()).toBe('initial') + await browser.elementByCss('button').click() + + await retry(async () => { + expect(await browser.elementByCss('p').text()).toBe('result') + }) + }) + + it('should work with server actions imported from client components', async () => { + const browser = await next.browser('/client') + expect(await browser.elementByCss('p').text()).toBe('initial') + await browser.elementByCss('button').click() + + await retry(async () => { + expect(await browser.elementByCss('p').text()).toBe('result') + }) + }) +}) diff --git a/test/e2e/app-dir/actions-simple/app/actions.ts b/test/e2e/app-dir/actions-simple/app/actions.ts new file mode 100644 index 0000000000000..d26fc49045e51 --- /dev/null +++ b/test/e2e/app-dir/actions-simple/app/actions.ts @@ -0,0 +1,5 @@ +'use server' + +export async function foo() { + return 'result' +} diff --git a/test/e2e/app-dir/actions-simple/app/client/page.tsx b/test/e2e/app-dir/actions-simple/app/client/page.tsx new file mode 100644 index 0000000000000..f7f3fb549ed9c --- /dev/null +++ b/test/e2e/app-dir/actions-simple/app/client/page.tsx @@ -0,0 +1,15 @@ +'use client' + +import { Form } from '../form' +import { foo } from '../actions' +import Link from 'next/link' + +export default function Page() { + return ( +
+

client component page

+
+ server component page +
+ ) +} diff --git a/test/e2e/app-dir/actions-simple/app/form.tsx b/test/e2e/app-dir/actions-simple/app/form.tsx new file mode 100644 index 0000000000000..0247e612542e0 --- /dev/null +++ b/test/e2e/app-dir/actions-simple/app/form.tsx @@ -0,0 +1,14 @@ +'use client' + +import { useActionState } from 'react' + +export function Form({ action }: { action: () => Promise }) { + const [result, formAction] = useActionState(action, 'initial') + + return ( + + +

{result}

+ + ) +} diff --git a/test/e2e/app-dir/actions-simple/app/layout.tsx b/test/e2e/app-dir/actions-simple/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/e2e/app-dir/actions-simple/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/actions-simple/app/page.tsx b/test/e2e/app-dir/actions-simple/app/page.tsx new file mode 100644 index 0000000000000..001ad7d42b7d8 --- /dev/null +++ b/test/e2e/app-dir/actions-simple/app/page.tsx @@ -0,0 +1,13 @@ +import { Form } from './form' +import { foo } from './actions' +import Link from 'next/link' + +export default function Page() { + return ( +
+

server component page

+
+ client component page +
+ ) +} diff --git a/test/e2e/app-dir/actions-simple/app/source-maps-turbopack/route.ts b/test/e2e/app-dir/actions-simple/app/source-maps-turbopack/route.ts new file mode 100644 index 0000000000000..f4d6ef2f0c379 --- /dev/null +++ b/test/e2e/app-dir/actions-simple/app/source-maps-turbopack/route.ts @@ -0,0 +1,18 @@ +import { readFile } from 'fs/promises' +import { NextRequest } from 'next/server' + +// This is for mocking findSourceMapURL until we've implemented it properly. +export async function GET(request: NextRequest): Promise { + const filename = request.nextUrl.searchParams.get('filename') + + try { + // It's not safe not to sanitize the query param, but it's just for a test. + const sourceMapContents = await readFile(`${filename}.map`) + + return new Response(sourceMapContents) + } catch (error) { + console.error(error) + } + + return new Response(null, { status: 404 }) +} diff --git a/test/e2e/app-dir/actions-simple/find-source-map-url-turbopack-mock.ts b/test/e2e/app-dir/actions-simple/find-source-map-url-turbopack-mock.ts new file mode 100644 index 0000000000000..429300b9e3003 --- /dev/null +++ b/test/e2e/app-dir/actions-simple/find-source-map-url-turbopack-mock.ts @@ -0,0 +1,10 @@ +export function findSourceMapURL(filename: string): string | null { + if (filename.startsWith(`${document.location.origin}/_next/static`)) { + return `${filename}.map` + } + + const url = new URL('/source-maps-turbopack', document.location.origin) + url.searchParams.set('filename', filename) + + return url.href +} diff --git a/test/e2e/app-dir/actions-simple/find-source-map-url-webpack-mock.ts b/test/e2e/app-dir/actions-simple/find-source-map-url-webpack-mock.ts new file mode 100644 index 0000000000000..77d6ec64fa28d --- /dev/null +++ b/test/e2e/app-dir/actions-simple/find-source-map-url-webpack-mock.ts @@ -0,0 +1,4 @@ +export function findSourceMapURL(_filename: string): string | null { + // TODO + return null +} diff --git a/test/e2e/app-dir/actions-simple/next.config.js b/test/e2e/app-dir/actions-simple/next.config.js new file mode 100644 index 0000000000000..a288abc41898b --- /dev/null +++ b/test/e2e/app-dir/actions-simple/next.config.js @@ -0,0 +1,22 @@ +/** + * @type {import('next').NextConfig} + */ + +const nextConfig = { + webpack(config) { + config.resolve.alias['next/dist/client/app-find-source-map-url'] = + require.resolve('./find-source-map-url-webpack-mock.ts') + + return config + }, + experimental: { + turbo: { + resolveAlias: { + 'next/dist/client/app-find-source-map-url': + './find-source-map-url-turbopack-mock.ts', + }, + }, + }, +} + +module.exports = nextConfig