Support HOC cases in server entries (#47379)

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Shu Ding 2023-03-22 15:15:08 +01:00 committed by GitHub
parent 6a6977cb71
commit e6a3bab489
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 366 additions and 62 deletions

View file

@ -47,6 +47,7 @@ pub fn server_actions<C: Comments>(
closure_idents: Default::default(),
action_idents: Default::default(),
exported_idents: Default::default(),
inlined_action_idents: Default::default(),
annotations: Default::default(),
extra_items: Default::default(),
@ -73,6 +74,7 @@ struct ServerActions<C: Comments> {
in_action_closure: bool,
closure_idents: Vec<Id>,
action_idents: Vec<Name>,
inlined_action_idents: Vec<(Id, Id)>,
// (ident, export name)
exported_idents: Vec<(Id, String)>,
@ -125,11 +127,17 @@ impl<C: Comments> ServerActions<C> {
ident: &Ident,
function: Option<&mut Box<Function>>,
arrow: Option<&mut ArrowExpr>,
call_expr_and_ident: Option<(&mut CallExpr, CallExpr, Ident)>,
return_paren: bool,
) -> (Option<Box<ParenExpr>>, Option<Box<Function>>) {
let action_name: JsWord = gen_ident(&mut self.ident_cnt);
let action_ident = private_ident!(action_name.clone());
if !self.in_action_file {
self.inlined_action_idents
.push((ident.to_id(), action_ident.to_id()));
}
let export_name: JsWord = if self.in_default_export_decl {
"default".into()
} else {
@ -140,7 +148,7 @@ impl<C: Comments> ServerActions<C> {
self.export_actions.push(export_name.to_string());
// If it's already a top level function, we don't need to hoist it.
if self.top_level && arrow.is_none() {
if self.top_level && arrow.is_none() && call_expr_and_ident.is_none() {
annotate_ident_as_action(
&mut self.annotations,
ident.clone(),
@ -148,6 +156,7 @@ impl<C: Comments> ServerActions<C> {
self.file_name.to_string(),
export_name.to_string(),
false,
None,
);
// export const $ACTION_myAction = myAction;
@ -205,6 +214,7 @@ impl<C: Comments> ServerActions<C> {
self.file_name.to_string(),
export_name.to_string(),
true,
None,
);
if let BlockStmtOrExpr::BlockStmt(block) = &mut *a.body {
@ -304,6 +314,7 @@ impl<C: Comments> ServerActions<C> {
self.file_name.to_string(),
export_name.to_string(),
true,
None,
);
f.body.visit_mut_with(&mut ClosureReplacer {
@ -391,6 +402,60 @@ impl<C: Comments> ServerActions<C> {
}
return (None, Some(Box::new(new_fn)));
} else if let Some((c, original_call, inner_action_ident)) = call_expr_and_ident {
let mut arrow_annotations = Vec::new();
annotate_ident_as_action(
&mut arrow_annotations,
ident.clone(),
vec![],
self.file_name.to_string(),
export_name.to_string(),
true,
Some(inner_action_ident),
);
self.extra_items
.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
span: DUMMY_SP,
decl: Decl::Var(Box::new(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Const,
declare: Default::default(),
decls: vec![VarDeclarator {
span: DUMMY_SP,
name: action_ident.into(),
init: Some(Box::new(Expr::Call(c.clone()))),
definite: Default::default(),
}],
})),
})));
// Create a paren expr to wrap all annotations:
// ($ACTION = hoc(...), $ACTION.$$id = "..", .., $ACTION)
let mut exprs = vec![Box::new(Expr::Assign(AssignExpr {
span: DUMMY_SP,
left: PatOrExpr::Pat(Box::new(Pat::Ident(ident.clone().into()))),
op: op!("="),
right: Box::new(Expr::Call(original_call)),
}))];
exprs.extend(arrow_annotations.into_iter().map(|a| {
if let Stmt::Expr(ExprStmt { expr, .. }) = a {
expr
} else {
unreachable!()
}
}));
exprs.push(Box::new(Expr::Ident(ident.clone())));
let new_paren = ParenExpr {
span: DUMMY_SP,
expr: Box::new(Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs,
})),
};
return (Some(Box::new(new_paren)), None);
}
}
@ -500,6 +565,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
&f.ident,
Some(&mut f.function),
None,
None,
false,
);
@ -611,73 +677,143 @@ impl<C: Comments> VisitMut for ServerActions<C> {
n.visit_mut_children_with(self);
if !self.in_action_file {
match n {
Expr::Arrow(a) => {
let is_action_fn = self.get_action_info(
if let BlockStmtOrExpr::BlockStmt(block) = &mut *a.body {
Some(block)
} else {
None
},
true,
);
if self.in_action_file {
return;
}
if is_action_fn {
// We need to give a name to the arrow function
// action and hoist it to the top.
let action_name = gen_ident(&mut self.ident_cnt);
let ident = private_ident!(action_name);
match n {
Expr::Arrow(a) => {
let is_action_fn = self.get_action_info(
if let BlockStmtOrExpr::BlockStmt(block) = &mut *a.body {
Some(block)
} else {
None
},
true,
);
let (maybe_new_paren, _) = self.add_action_annotations_and_maybe_hoist(
&ident,
None,
Some(a),
true,
);
*n = attach_name_to_expr(
ident,
if let Some(new_paren) = maybe_new_paren {
Expr::Paren(*new_paren)
} else {
Expr::Arrow(a.clone())
},
&mut self.extra_items,
);
}
if !is_action_fn {
return;
}
Expr::Fn(f) => {
let is_action_fn = self.get_action_info(f.function.body.as_mut(), true);
if is_action_fn {
let ident = match f.ident.as_mut() {
None => {
let action_name = gen_ident(&mut self.ident_cnt);
let ident = Ident::new(action_name, DUMMY_SP);
f.ident.insert(ident)
}
Some(i) => i,
};
// We need to give a name to the arrow function
// action and hoist it to the top.
let action_name = gen_ident(&mut self.ident_cnt);
let ident = private_ident!(action_name);
let (maybe_new_paren, _) = self.add_action_annotations_and_maybe_hoist(
ident,
Some(&mut f.function),
None,
true,
);
let (maybe_new_paren, _) =
self.add_action_annotations_and_maybe_hoist(&ident, None, Some(a), None, true);
*n = attach_name_to_expr(
ident,
if let Some(new_paren) = maybe_new_paren {
Expr::Paren(*new_paren)
} else {
Expr::Arrow(a.clone())
},
&mut self.extra_items,
);
}
Expr::Fn(f) => {
let is_action_fn = self.get_action_info(f.function.body.as_mut(), true);
if !is_action_fn {
return;
}
let ident = match f.ident.as_mut() {
None => {
let action_name = gen_ident(&mut self.ident_cnt);
let ident = Ident::new(action_name, DUMMY_SP);
f.ident.insert(ident)
}
Some(i) => i,
};
let (maybe_new_paren, _) = self.add_action_annotations_and_maybe_hoist(
ident,
Some(&mut f.function),
None,
None,
true,
);
if let Some(new_paren) = maybe_new_paren {
*n = attach_name_to_expr(
ident.clone(),
Expr::Paren(*new_paren),
&mut self.extra_items,
);
}
}
Expr::Call(c) => {
// Here we need to handle HOCs that wrap actions, e.g.:
// withValidator(($ACTION = async function () { ... }, ...))
// For now, we only handle the case where the HOC has a single argument:
// the action function.
if c.args.len() != 1 {
return;
}
if let Some(ExprOrSpread {
expr:
box Expr::Paren(ParenExpr {
expr: box Expr::Seq(seq_expr),
..
}),
..
}) = c.args.first_mut()
{
if let Some(box Expr::Assign(AssignExpr {
left: PatOrExpr::Pat(box Pat::Ident(pat_id)),
..
})) = seq_expr.exprs.first_mut()
{
let maybe_action_ident = self
.inlined_action_idents
.iter()
.find(|id| id.0 == pat_id.id.to_id());
if let Some(action_ident) = maybe_action_ident {
// This is a HOC that wraps an
// action.
// We need to give a name to the result
// action and hoist it to the top.
let action_name = gen_ident(&mut self.ident_cnt);
let ident = private_ident!(action_name);
let mut new_call = CallExpr {
span: DUMMY_SP,
callee: c.callee.clone(),
args: vec![ExprOrSpread {
spread: None,
expr: Box::new(Expr::Ident(action_ident.1.clone().into())),
}],
type_args: Default::default(),
};
let (maybe_new_paren, _) = self.add_action_annotations_and_maybe_hoist(
&ident,
None,
None,
Some((&mut new_call, c.clone(), action_ident.0.clone().into())),
true,
);
if let Some(new_paren) = maybe_new_paren {
*n = attach_name_to_expr(
ident.clone(),
Expr::Paren(*new_paren),
ident,
if let Some(new_paren) = maybe_new_paren {
// Keep the original $$bound value.
Expr::Paren(*new_paren)
} else {
Expr::Call(c.clone())
},
&mut self.extra_items,
);
}
}
}
_ => {}
}
_ => {}
}
}
@ -720,6 +856,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
match &**init {
Expr::Fn(_f) => {}
Expr::Arrow(_a) => {}
Expr::Call(_c) => {}
_ => {
disallowed_export_span = *span;
}
@ -813,6 +950,20 @@ impl<C: Comments> VisitMut for ServerActions<C> {
// export default foo
self.exported_idents.push((ident.to_id(), "default".into()));
}
Expr::Call(call) => {
// export default fn()
let new_ident =
Ident::new(gen_ident(&mut self.ident_cnt), DUMMY_SP);
self.exported_idents
.push((new_ident.to_id(), "default".into()));
*default_expr.expr = attach_name_to_expr(
new_ident,
Expr::Call(call.clone()),
&mut self.extra_items,
);
}
_ => {
disallowed_export_span = default_expr.span;
}
@ -857,6 +1008,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
self.file_name.to_string(),
export_name.to_string(),
false,
None,
);
if !self.config.is_server {
let params_ident = private_ident!("args");
@ -1078,6 +1230,7 @@ fn annotate_ident_as_action(
file_name: String,
export_name: String,
has_bound: bool,
re_annotate_action: Option<Ident>,
) {
// myAction.$$typeof = Symbol.for('react.server.reference');
annotations.push(annotate(
@ -1109,11 +1262,23 @@ fn annotate_ident_as_action(
annotations.push(annotate(
&ident,
"$$bound",
ArrayLit {
span: DUMMY_SP,
elems: bound,
}
.into(),
if let Some(re_annotate_ident) = re_annotate_action {
Box::new(Expr::Member(MemberExpr {
span: DUMMY_SP,
obj: Box::new(Expr::Ident(re_annotate_ident)),
prop: MemberProp::Ident(Ident {
sym: "$$bound".into(),
span: DUMMY_SP,
optional: false,
}),
}))
} else {
ArrayLit {
span: DUMMY_SP,
elems: bound,
}
.into()
},
));
// If an action doesn't have any bound values, we add a special property

View file

@ -0,0 +1,19 @@
import { validator, another } from 'auth'
const x = 1
export default function Page () {
const y = 1
return <Foo action={validator(async function (z) {
'use server'
return x + y + z
})} />
}
validator(async () => {
'use server'
})
another(validator(async () => {
'use server'
}))

View file

@ -0,0 +1,32 @@
/* __next_internal_action_entry_do_not_use__ $$ACTION_1,$$ACTION_3,$$ACTION_5,$$ACTION_7,$$ACTION_9,$$ACTION_11,$$ACTION_13 */ import { validator, another } from 'auth';
const x = 1;
export default function Page() {
const y = 1;
return <Foo action={$$ACTION_2 = validator(($$ACTION_0 = async function(z) {
return $$ACTION_1($$ACTION_0.$$bound);
}, $$ACTION_0.$$typeof = Symbol.for("react.server.reference"), $$ACTION_0.$$id = "188d5d945750dc32e2c842b93c75a65763d4a922", $$ACTION_0.$$bound = [
y
], $$ACTION_0)), $$ACTION_2.$$typeof = Symbol.for("react.server.reference"), $$ACTION_2.$$id = "56a859f462d35a297c46a1bbd1e6a9058c104ab8", $$ACTION_2.$$bound = $$ACTION_0.$$bound, $$ACTION_2}/>;
}
export async function $$ACTION_1(closure, z = closure[1]) {
return x + closure[0] + z;
}
var $$ACTION_0;
export const $$ACTION_3 = validator($$ACTION_1);
var $$ACTION_2;
$$ACTION_6 = validator(($$ACTION_4 = async ()=>$$ACTION_5($$ACTION_4.$$bound), $$ACTION_4.$$typeof = Symbol.for("react.server.reference"), $$ACTION_4.$$id = "1383664d1dc2d9cfe33b88df3fa0eaffef8b99bc", $$ACTION_4.$$bound = [
y
], $$ACTION_4)), $$ACTION_6.$$typeof = Symbol.for("react.server.reference"), $$ACTION_6.$$id = "faf016739650cb4995340c9d9ab06ce1c9407fa0", $$ACTION_6.$$bound = $$ACTION_4.$$bound, $$ACTION_6;
export const $$ACTION_5 = async (closure)=>{};
var $$ACTION_4;
export const $$ACTION_7 = validator($$ACTION_5);
var $$ACTION_6;
$$ACTION_12 = another(($$ACTION_10 = validator(($$ACTION_8 = async ()=>$$ACTION_9($$ACTION_8.$$bound), $$ACTION_8.$$typeof = Symbol.for("react.server.reference"), $$ACTION_8.$$id = "0d0ca9684921f1c6dc36a2ec55ce57ba31407820", $$ACTION_8.$$bound = [
y
], $$ACTION_8)), $$ACTION_10.$$typeof = Symbol.for("react.server.reference"), $$ACTION_10.$$id = "dd70487b74c2c510c55e3e68aa3614cfa780850d", $$ACTION_10.$$bound = $$ACTION_8.$$bound, $$ACTION_10)), $$ACTION_12.$$typeof = Symbol.for("react.server.reference"), $$ACTION_12.$$id = "57cbac1f8911efd298cb885cba89919b14153dc1", $$ACTION_12.$$bound = $$ACTION_10.$$bound, $$ACTION_12;
export const $$ACTION_9 = async (closure)=>{};
var $$ACTION_8;
export const $$ACTION_11 = validator($$ACTION_9);
var $$ACTION_10;
export const $$ACTION_13 = another($$ACTION_11);
var $$ACTION_12;

View file

@ -0,0 +1,6 @@
'use server'
import { validator } from 'auth'
export const action = validator(async () => {})
export default validator(async () => {})

View file

@ -0,0 +1,17 @@
/* __next_internal_action_entry_do_not_use__ action,default */ import { validator } from 'auth';
export const action = validator(async ()=>{});
export default $$ACTION_0 = validator(async ()=>{});
var $$ACTION_0;
import ensureServerEntryExports from "private-next-rsc-action-proxy";
ensureServerEntryExports([
action,
$$ACTION_0
]);
action.$$typeof = Symbol.for("react.server.reference");
action.$$id = "f14702b5a021dd117f7ec7a3c838f397c2046d3b";
action.$$bound = [];
action.$$with_bound = false;
$$ACTION_0.$$typeof = Symbol.for("react.server.reference");
$$ACTION_0.$$id = "c18c215a6b7cdc64bf709f3a714ffdef1bf9651d";
$$ACTION_0.$$bound = [];
$$ACTION_0.$$with_bound = false;

View file

@ -14,5 +14,9 @@ export async function callServer(id: string, bound: any[]) {
}),
})
if (!res.ok) {
throw new Error(await res.text())
}
return (await res.json())[0]
}

View file

@ -1398,6 +1398,14 @@ export async function renderToHTMLOrFlight(
res.statusCode = 404
return new RenderResult(await bodyResult({ asNotFound: true }))
}
if (isFetchAction) {
res.statusCode = 500
return new RenderResult(
(err as Error)?.message ?? 'Internal Server Error'
)
}
throw err
}
} else {

View file

@ -65,6 +65,25 @@ createNextDescribe(
}, 'my-not-found')
})
it('should support hoc auth wrappers', async () => {
const browser = await next.browser('/header')
await await browser.eval(`document.cookie = 'auth=0'`)
await browser.elementByCss('#authed').click()
await check(() => {
return browser.elementByCss('h1').text()
}, 'Error: Unauthorized request')
await await browser.eval(`document.cookie = 'auth=1'`)
await browser.elementByCss('#authed').click()
await check(() => {
return browser.elementByCss('h1').text()
}, 'HELLO, WORLD')
})
it('should support importing actions in client components', async () => {
const browser = await next.browser('/client')

View file

@ -1,7 +1,17 @@
import UI from './ui'
import { getCookie, getHeader } from './actions'
import { validator } from './validator'
export default function Page() {
return <UI getCookie={getCookie} getHeader={getHeader} />
return (
<UI
getCookie={getCookie}
getHeader={getHeader}
getAuthedUppercase={validator(async (str) => {
'use server'
return str.toUpperCase()
})}
/>
)
}

View file

@ -2,7 +2,7 @@
import { useState } from 'react'
export default function UI({ getCookie, getHeader }) {
export default function UI({ getCookie, getHeader, getAuthedUppercase }) {
const [result, setResult] = useState('')
return (
@ -29,6 +29,19 @@ export default function UI({ getCookie, getHeader }) {
>
getHeader
</button>
<button
id="authed"
onClick={async () => {
try {
const res = await getAuthedUppercase('hello, world')
setResult(res)
} catch (err) {
setResult('Error: ' + err.message)
}
}}
>
getAuthedUppercase
</button>
</div>
)
}

View file

@ -0,0 +1,11 @@
import { cookies } from 'next/headers'
export function validator(action) {
return async function (...args) {
const auth = cookies().get('auth')
if (auth?.value !== '1') {
throw new Error('Unauthorized request')
}
return action(...args)
}
}