Skip to content

Commit

Permalink
Change Server Reference creation on client (#48824)
Browse files Browse the repository at this point in the history
Mostly mirrors the changed made in
facebook/react#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:
#48697.

cc @sebmarkbage.
  • Loading branch information
shuding committed Apr 27, 2023
1 parent fafecb8 commit 8e723ed
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 134 deletions.
227 changes: 124 additions & 103 deletions packages/next-swc/crates/core/src/server_actions.rs
Expand Up @@ -855,59 +855,46 @@ impl<C: Comments> VisitMut for ServerActions<C> {

// 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,
})),
},
));
Expand All @@ -916,66 +903,92 @@ impl<C: Comments> VisitMut for ServerActions<C> {
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;
Expand Down Expand Up @@ -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<Stmt>,
ident: Ident,
Expand All @@ -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(
Expand Down
@@ -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");
@@ -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");
@@ -1 +1,2 @@
declare module 'next/dist/compiled/react-server-dom-webpack/client'
declare module 'next/dist/client/app-call-server'
6 changes: 5 additions & 1 deletion packages/next/src/build/webpack-config.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
@@ -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)
}
2 changes: 2 additions & 0 deletions packages/next/src/lib/constants.ts
Expand Up @@ -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`

Expand Down
1 change: 1 addition & 0 deletions packages/next/types/misc.d.ts
Expand Up @@ -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'
Expand Down

0 comments on commit 8e723ed

Please sign in to comment.