From 8e723ede65786d3460e0db436ee58a4f2d1384f8 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 27 Apr 2023 17:02:23 +0200 Subject: [PATCH] Change Server Reference creation on client (#48824) Mostly mirrors the changed made in https://github.com/facebook/react/pull/26632 to our SWC transform. The implementation difference is that the AST transformer only adds a general purpose wrapper call `createServerReference(id)` from an aliased import, so we can easily change the underlying function in the bundler. This change only affects the client layer (when `self.config.is_server === false`). Needs to be landed after another React upgrade: https://github.com/vercel/next.js/pull/48697. cc @sebmarkbage. --- .../crates/core/src/server_actions.rs | 227 ++++++++++-------- .../fixture/server-actions/client/1/output.js | 22 +- .../fixture/server-actions/client/2/output.js | 13 +- .../next-core/js/types/compiled-next.d.ts | 1 + packages/next/src/build/webpack-config.ts | 6 +- .../action-client-wrapper.ts | 10 + packages/next/src/lib/constants.ts | 2 + packages/next/types/misc.d.ts | 1 + 8 files changed, 148 insertions(+), 134 deletions(-) create mode 100644 packages/next/src/build/webpack/loaders/next-flight-loader/action-client-wrapper.ts diff --git a/packages/next-swc/crates/core/src/server_actions.rs b/packages/next-swc/crates/core/src/server_actions.rs index f4051fa84a298..8b6357d93faae 100644 --- a/packages/next-swc/crates/core/src/server_actions.rs +++ b/packages/next-swc/crates/core/src/server_actions.rs @@ -855,59 +855,46 @@ impl VisitMut for ServerActions { // If it's a "use server" file, all exports need to be annotated as actions. if self.in_action_file { + // If it's compiled in the client layer, each export field needs to be + // wrapped by a reference creation call. + let create_ref_ident = private_ident!("createServerReference"); + if !self.config.is_server { + // import createServerReference from 'private-next-rsc-action-client-wrapper' + // createServerReference("action_id") + new.push(ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: vec![ImportSpecifier::Default(ImportDefaultSpecifier { + span: DUMMY_SP, + local: create_ref_ident.clone(), + })], + src: Box::new(Str { + span: DUMMY_SP, + value: "private-next-rsc-action-client-wrapper".into(), + raw: None, + }), + type_only: false, + asserts: None, + }))); + } + for (id, export_name) in self.exported_idents.iter() { let ident = Ident::new(id.0.clone(), DUMMY_SP.with_ctxt(id.1)); - annotate_ident_as_action( - &mut self.annotations, - ident.clone(), - Vec::new(), - self.file_name.to_string(), - export_name.to_string(), - false, - None, - ); + if !self.config.is_server { - let params_ident = private_ident!("args"); - let noop_fn = Box::new(Function { - params: vec![Param { - span: DUMMY_SP, - decorators: Default::default(), - pat: Pat::Rest(RestPat { - span: DUMMY_SP, - dot3_token: DUMMY_SP, - arg: Box::new(Pat::Ident(params_ident.clone().into())), - type_ann: None, - }), - }], - decorators: Vec::new(), - span: DUMMY_SP, - body: Some(BlockStmt { - span: DUMMY_SP, - stmts: vec![Stmt::Return(ReturnStmt { - span: DUMMY_SP, - arg: Some(Box::new(Expr::Call(CallExpr { - span: DUMMY_SP, - callee: Callee::Expr(Box::new(Expr::Ident(private_ident!( - "__build_action__" - )))), - args: vec![ident.clone().as_arg(), params_ident.as_arg()], - type_args: None, - }))), - })], - }), - is_generator: false, - is_async: true, - type_params: None, - return_type: None, - }); + let action_id = + generate_action_id(self.file_name.to_string(), export_name.to_string()); if export_name == "default" { let export_expr = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr( ExportDefaultExpr { span: DUMMY_SP, - expr: Box::new(Expr::Fn(FnExpr { - ident: Some(ident), - function: noop_fn, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: Callee::Expr(Box::new(Expr::Ident( + create_ref_ident.clone(), + ))), + args: vec![action_id.as_arg()], + type_args: None, })), }, )); @@ -916,66 +903,92 @@ impl VisitMut for ServerActions { let export_expr = ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { span: DUMMY_SP, - decl: Decl::Fn(FnDecl { - ident, + decl: Decl::Var(Box::new(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Const, declare: false, - function: noop_fn, - }), + decls: vec![VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(ident.into()), + init: Some(Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: Callee::Expr(Box::new(Expr::Ident( + create_ref_ident.clone(), + ))), + args: vec![action_id.as_arg()], + type_args: None, + }))), + definite: false, + }], + })), })); new.push(export_expr); } + } else { + annotate_ident_as_action( + &mut self.annotations, + ident.clone(), + Vec::new(), + self.file_name.to_string(), + export_name.to_string(), + false, + None, + ); } } - new.append(&mut self.extra_items); - // Ensure that the exports are valid by appending a check - // import { ensureServerEntryExports } from 'private-next-rsc-action-proxy' - // ensureServerEntryExports([action1, action2, ...]) - let ensure_ident = private_ident!("ensureServerEntryExports"); - new.push(ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { - span: DUMMY_SP, - specifiers: vec![ImportSpecifier::Default(ImportDefaultSpecifier { - span: DUMMY_SP, - local: ensure_ident.clone(), - })], - src: Box::new(Str { + if self.config.is_server { + new.append(&mut self.extra_items); + + // Ensure that the exports are valid by appending a check + // import { ensureServerEntryExports } from 'private-next-rsc-action-proxy' + // ensureServerEntryExports([action1, action2, ...]) + let ensure_ident = private_ident!("ensureServerEntryExports"); + new.push(ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { span: DUMMY_SP, - value: "private-next-rsc-action-proxy".into(), - raw: None, - }), - type_only: false, - asserts: None, - }))); - new.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(Expr::Call(CallExpr { + specifiers: vec![ImportSpecifier::Default(ImportDefaultSpecifier { + span: DUMMY_SP, + local: ensure_ident.clone(), + })], + src: Box::new(Str { + span: DUMMY_SP, + value: "private-next-rsc-action-proxy".into(), + raw: None, + }), + type_only: false, + asserts: None, + }))); + new.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { span: DUMMY_SP, - callee: Callee::Expr(Box::new(Expr::Ident(ensure_ident))), - args: vec![ExprOrSpread { - spread: None, - expr: Box::new(Expr::Array(ArrayLit { - span: DUMMY_SP, - elems: self - .exported_idents - .iter() - .map(|e| { - Some(ExprOrSpread { - spread: None, - expr: Box::new(Expr::Ident(Ident::new( - e.0 .0.clone(), - DUMMY_SP.with_ctxt(e.0 .1), - ))), + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: Callee::Expr(Box::new(Expr::Ident(ensure_ident))), + args: vec![ExprOrSpread { + spread: None, + expr: Box::new(Expr::Array(ArrayLit { + span: DUMMY_SP, + elems: self + .exported_idents + .iter() + .map(|e| { + Some(ExprOrSpread { + spread: None, + expr: Box::new(Expr::Ident(Ident::new( + e.0 .0.clone(), + DUMMY_SP.with_ctxt(e.0 .1), + ))), + }) }) - }) - .collect(), - })), - }], - type_args: None, - })), - }))); + .collect(), + })), + }], + type_args: None, + })), + }))); - // Append annotations to the end of the file. - new.extend(self.annotations.drain(..).map(ModuleItem::Stmt)); + // Append annotations to the end of the file. + new.extend(self.annotations.drain(..).map(ModuleItem::Stmt)); + } } *stmts = new; @@ -1149,6 +1162,18 @@ fn pat_to_assign_pat( } } +fn generate_action_id(file_name: String, export_name: String) -> String { + // Attach a checksum to the action using sha1: + // $$id = sha1('file_name' + ':' + 'export_name'); + let mut hasher = Sha1::new(); + hasher.update(file_name.as_bytes()); + hasher.update(b":"); + hasher.update(export_name.as_bytes()); + let result = hasher.finalize(); + + hex_encode(result) +} + fn annotate_ident_as_action( annotations: &mut Vec, ident: Ident, @@ -1173,16 +1198,12 @@ fn annotate_ident_as_action( .into(), )); - // Attach a checksum to the action using sha1: - // myAction.$$id = sha1('file_name' + ':' + 'export_name'); - let mut hasher = Sha1::new(); - hasher.update(file_name.as_bytes()); - hasher.update(b":"); - hasher.update(export_name.as_bytes()); - let result = hasher.finalize(); - // Convert result to hex string - annotations.push(annotate(&ident, "$$id", hex_encode(result).into())); + annotations.push(annotate( + &ident, + "$$id", + generate_action_id(file_name, export_name).into(), + )); // myAction.$$bound = []; annotations.push(annotate( diff --git a/packages/next-swc/crates/core/tests/fixture/server-actions/client/1/output.js b/packages/next-swc/crates/core/tests/fixture/server-actions/client/1/output.js index e20beb9f5cf38..0f3a42cf3bfb8 100644 --- a/packages/next-swc/crates/core/tests/fixture/server-actions/client/1/output.js +++ b/packages/next-swc/crates/core/tests/fixture/server-actions/client/1/output.js @@ -1,20 +1,4 @@ // app/send.ts -/* __next_internal_action_entry_do_not_use__ myAction,default */ export async function myAction(...args) { - return __build_action__(myAction, args); -} -export default async function $$ACTION_0(...args) { - return __build_action__($$ACTION_0, args); -}; -import ensureServerEntryExports from "private-next-rsc-action-proxy"; -ensureServerEntryExports([ - myAction, - $$ACTION_0 -]); -myAction.$$typeof = Symbol.for("react.server.reference"); -myAction.$$id = "e10665baac148856374b2789aceb970f66fec33e"; -myAction.$$bound = []; -myAction.$$with_bound = false; -$$ACTION_0.$$typeof = Symbol.for("react.server.reference"); -$$ACTION_0.$$id = "c18c215a6b7cdc64bf709f3a714ffdef1bf9651d"; -$$ACTION_0.$$bound = []; -$$ACTION_0.$$with_bound = false; +/* __next_internal_action_entry_do_not_use__ myAction,default */ import createServerReference from "private-next-rsc-action-client-wrapper"; +export const myAction = createServerReference("e10665baac148856374b2789aceb970f66fec33e"); +export default createServerReference("c18c215a6b7cdc64bf709f3a714ffdef1bf9651d"); diff --git a/packages/next-swc/crates/core/tests/fixture/server-actions/client/2/output.js b/packages/next-swc/crates/core/tests/fixture/server-actions/client/2/output.js index 26ea8ab043a93..106cea6cc9d9e 100644 --- a/packages/next-swc/crates/core/tests/fixture/server-actions/client/2/output.js +++ b/packages/next-swc/crates/core/tests/fixture/server-actions/client/2/output.js @@ -1,12 +1,3 @@ // app/send.ts -/* __next_internal_action_entry_do_not_use__ foo */ export async function foo(...args) { - return __build_action__(foo, args); -} -import ensureServerEntryExports from "private-next-rsc-action-proxy"; -ensureServerEntryExports([ - foo -]); -foo.$$typeof = Symbol.for("react.server.reference"); -foo.$$id = "ab21efdafbe611287bc25c0462b1e0510d13e48b"; -foo.$$bound = []; -foo.$$with_bound = false; +/* __next_internal_action_entry_do_not_use__ foo */ import createServerReference from "private-next-rsc-action-client-wrapper"; +export const foo = createServerReference("ab21efdafbe611287bc25c0462b1e0510d13e48b"); diff --git a/packages/next-swc/crates/next-core/js/types/compiled-next.d.ts b/packages/next-swc/crates/next-core/js/types/compiled-next.d.ts index bc9f894f61ea4..7dc4c97f0788d 100644 --- a/packages/next-swc/crates/next-core/js/types/compiled-next.d.ts +++ b/packages/next-swc/crates/next-core/js/types/compiled-next.d.ts @@ -1 +1,2 @@ declare module 'next/dist/compiled/react-server-dom-webpack/client' +declare module 'next/dist/client/app-call-server' diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 2fae133d4fa8b..3b616a8f5318e 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -12,6 +12,7 @@ import { APP_DIR_ALIAS, WEBPACK_LAYERS, RSC_ACTION_PROXY_ALIAS, + RSC_ACTION_CLIENT_WRAPPER_ALIAS, } from '../lib/constants' import { fileExists } from '../lib/file-exists' import { CustomRoutes } from '../lib/load-custom-routes.js' @@ -1058,6 +1059,9 @@ export default async function getBaseWebpackConfig( [RSC_ACTION_PROXY_ALIAS]: 'next/dist/build/webpack/loaders/next-flight-loader/action-proxy', + [RSC_ACTION_CLIENT_WRAPPER_ALIAS]: + 'next/dist/build/webpack/loaders/next-flight-loader/action-client-wrapper', + ...(isClient || isEdgeServer ? { [clientResolveRewrites]: hasRewrites @@ -1263,7 +1267,7 @@ export default async function getBaseWebpackConfig( } const notExternalModules = - /^(?:private-next-pages\/|next\/(?:dist\/pages\/|(?:app|document|link|image|legacy\/image|constants|dynamic|script|navigation|headers)$)|string-hash|private-next-rsc-action-proxy$)/ + /^(?:private-next-pages\/|next\/(?:dist\/pages\/|(?:app|document|link|image|legacy\/image|constants|dynamic|script|navigation|headers)$)|string-hash|private-next-rsc-action-proxy|private-next-rsc-action-client-wrapper$)/ if (notExternalModules.test(request)) { return } 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 new file mode 100644 index 0000000000000..4c7ffc1457bc1 --- /dev/null +++ b/packages/next/src/build/webpack/loaders/next-flight-loader/action-client-wrapper.ts @@ -0,0 +1,10 @@ +// This file must be bundled in the app's client layer. + +import { createServerReference } from 'next/dist/compiled/react-server-dom-webpack/client' +import { callServer } from 'next/dist/client/app-call-server' + +// A noop wrapper to let the Flight client create the server reference. +// See also: https://github.com/facebook/react/pull/26632 +export default function (id: string) { + return createServerReference(id, callServer) +} diff --git a/packages/next/src/lib/constants.ts b/packages/next/src/lib/constants.ts index 0b7ddfbb88900..edf9124914c47 100644 --- a/packages/next/src/lib/constants.ts +++ b/packages/next/src/lib/constants.ts @@ -25,6 +25,8 @@ export const ROOT_DIR_ALIAS = 'private-next-root-dir' export const APP_DIR_ALIAS = 'private-next-app-dir' export const RSC_MOD_REF_PROXY_ALIAS = 'private-next-rsc-mod-ref-proxy' export const RSC_ACTION_PROXY_ALIAS = 'private-next-rsc-action-proxy' +export const RSC_ACTION_CLIENT_WRAPPER_ALIAS = + 'private-next-rsc-action-client-wrapper' export const PUBLIC_DIR_MIDDLEWARE_CONFLICT = `You can not have a '_next' folder inside of your public folder. This conflicts with the internal '/_next' route. https://nextjs.org/docs/messages/public-next-folder-conflict` diff --git a/packages/next/types/misc.d.ts b/packages/next/types/misc.d.ts index 6cfd24aeadfc8..4bbb3b53939ac 100644 --- a/packages/next/types/misc.d.ts +++ b/packages/next/types/misc.d.ts @@ -15,6 +15,7 @@ declare module 'next/dist/compiled/react-server-dom-webpack/client.edge' declare module 'next/dist/compiled/react-server-dom-webpack/client.browser' declare module 'next/dist/compiled/react-server-dom-webpack/server.browser' declare module 'next/dist/compiled/react-server-dom-webpack/server.edge' +declare module 'next/dist/client/app-call-server' declare module 'next/dist/compiled/react-dom/server' declare module 'next/dist/compiled/react-dom/server.edge' declare module 'next/dist/compiled/react-dom/server.browser'