Skip to content

Commit 1e9296a

Browse files
authored
Support client component interop (#57)
* Add test cases * Separate test cases * Use different test path * Change outDir * Add serialize function * Format tools code * Add SuperJSONComponent * Fix test cases * Change package files * Refactor and complete * Update README.md
1 parent 7c25cbe commit 1e9296a

File tree

48 files changed

+1273
-916
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1273
-916
lines changed

README.md

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,17 @@
22

33
```jsx
44
export default function Page({ date }) {
5-
return (
6-
<div>
7-
Today is {date.toDateString()}
8-
</div>
9-
)
5+
return <div>Today is {date.toDateString()}</div>;
106
}
117

128
// You can also use getInitialProps, getStaticProps
139
export const getServerSideProps = () => {
1410
return {
1511
props: {
16-
date: new Date()
17-
}
18-
}
19-
}
12+
date: new Date(),
13+
},
14+
};
15+
};
2016
```
2117

2218
<p align="middle">
@@ -45,19 +41,35 @@ Add the plugin into `next.config.js`
4541
// next.config.js
4642
module.exports = {
4743
experimental: {
48-
swcPlugins: [
49-
['next-superjson-plugin', {}],
50-
],
44+
swcPlugins: [["next-superjson-plugin", {}]],
5145
},
52-
}
46+
};
5347
```
5448

5549
### Options
50+
5651
You can use the `excluded` option to exclude specific properties from serialization.
52+
5753
```js
5854
['next-superjson-plugin', { excluded: ["someProp"] }],
5955
```
6056

57+
### Server Component -> Client Component
58+
59+
```jsx
60+
export default function ServerComponent() {
61+
const date = new Date();
62+
return (
63+
<>
64+
<AnotherServerComponent date={date} />
65+
66+
{/* Use "data-superjson" attribute to pass non-serializable props to client components */}
67+
<ClientComponent date={date} data-superjson />
68+
</>
69+
);
70+
}
71+
```
72+
6173
## How it works
6274

6375
```mermaid
@@ -73,12 +85,13 @@ sequenceDiagram
7385
Note over SWC Plugin: getInitialProps <br> getServerSideProps <br> getStaticProps
7486
SuperJSON->>Next.js: Deserialize Props
7587
Note over SWC Plugin: Page Component
76-
88+
7789
```
7890

7991
## Contributing
8092

8193
[Leave an issue](https://github.com/orionmiz/next-superjson-plugin/issues)
8294

8395
## Special Thanks
96+
8497
- [kdy1](https://github.com/kdy1) (Main creator of swc project)

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@
1717
"swc-plugin"
1818
],
1919
"main": "next_superjson.wasm",
20+
"exports": {
21+
".": "./dist/next_superjson.wasm",
22+
"./tools": "./dist/tools.js",
23+
"./client": "./dist/client.js"
24+
},
2025
"scripts": {
21-
"prepack": "tsc && cp target/wasm32-wasi/release/next_superjson.wasm .",
26+
"prepack": "tsc && cp target/wasm32-wasi/release/next_superjson.wasm ./dist",
2227
"prepare": "husky install"
2328
},
2429
"files": [
25-
"tools.*"
30+
"dist"
2631
],
2732
"peerDependencies": {
2833
"next": "^13",

src/app.rs

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
use std::vec;
2+
3+
use swc_common::util::take::Take;
4+
use swc_core::{
5+
common::DUMMY_SP,
6+
ecma::{
7+
ast::*,
8+
utils::{is_valid_ident, prepend_stmts},
9+
visit::*,
10+
},
11+
};
12+
13+
use crate::Config;
14+
15+
static DIRECTIVE: &str = "data-superjson";
16+
static SERIALIZER_FUNCTION: &str = "serialize";
17+
static DESERIALIZER_COMPONENT: &str = "SuperJSONComponent";
18+
static DESERIALIZER_PROPS_ATTR: &str = "props";
19+
static DESERIALIZER_PROPS_COMPONENT: &str = "component";
20+
static TOOLS_SRC: &str = "next-superjson-plugin/tools";
21+
static CLIENT_SRC: &str = "next-superjson-plugin/client";
22+
23+
struct AppTransformer {
24+
transformed: bool,
25+
}
26+
27+
pub fn transform_app(_: Config) -> impl VisitMut {
28+
AppTransformer { transformed: false }
29+
}
30+
31+
trait JSXUtil {
32+
fn as_expr(&self) -> Expr;
33+
}
34+
35+
impl JSXUtil for JSXMemberExpr {
36+
fn as_expr(&self) -> Expr {
37+
match &self.obj {
38+
JSXObject::Ident(id) => id.clone().into(),
39+
JSXObject::JSXMemberExpr(member) => MemberExpr {
40+
obj: Box::new(member.as_expr()),
41+
prop: MemberProp::Ident(member.prop.clone()),
42+
span: DUMMY_SP,
43+
}
44+
.into(),
45+
}
46+
}
47+
}
48+
49+
impl JSXUtil for JSXElementName {
50+
fn as_expr(&self) -> Expr {
51+
match self {
52+
JSXElementName::Ident(id) => {
53+
if is_valid_ident(&id.sym) {
54+
id.clone().into()
55+
} else {
56+
Lit::Str(id.sym.clone().into()).into()
57+
}
58+
}
59+
JSXElementName::JSXMemberExpr(member) => MemberExpr {
60+
obj: Box::new(member.as_expr()),
61+
prop: member.prop.clone().into(),
62+
span: DUMMY_SP,
63+
}
64+
.into(),
65+
// namespace cannot be component
66+
_ => unreachable!(),
67+
}
68+
}
69+
}
70+
71+
impl VisitMut for AppTransformer {
72+
fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
73+
items.visit_mut_children_with(self);
74+
75+
if self.transformed {
76+
// add import decl
77+
78+
prepend_stmts(
79+
items,
80+
vec![
81+
ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
82+
specifiers: vec![ImportNamedSpecifier {
83+
local: Ident::new(SERIALIZER_FUNCTION.into(), DUMMY_SP),
84+
span: DUMMY_SP,
85+
imported: None,
86+
is_type_only: false,
87+
}
88+
.into()],
89+
src: Box::new(TOOLS_SRC.into()),
90+
..ImportDecl::dummy()
91+
})),
92+
ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
93+
specifiers: vec![ImportDefaultSpecifier {
94+
local: Ident::new(DESERIALIZER_COMPONENT.into(), DUMMY_SP),
95+
span: DUMMY_SP,
96+
}
97+
.into()],
98+
src: Box::new(CLIENT_SRC.into()),
99+
..ImportDecl::dummy()
100+
})),
101+
]
102+
.into_iter(),
103+
);
104+
}
105+
}
106+
107+
fn visit_mut_jsx_opening_element(&mut self, elem: &mut JSXOpeningElement) {
108+
elem.visit_mut_children_with(self);
109+
110+
let mut found = false;
111+
112+
// find and remove data-superjson directive
113+
elem.attrs.retain(|attr_or_spread| {
114+
if let JSXAttrOrSpread::JSXAttr(JSXAttr {
115+
name: JSXAttrName::Ident(id),
116+
..
117+
}) = attr_or_spread
118+
{
119+
if &*id.sym == DIRECTIVE {
120+
found = true;
121+
return false;
122+
}
123+
}
124+
true
125+
});
126+
127+
if found {
128+
// attrs -> obj props
129+
let list: Vec<PropOrSpread> = elem
130+
.attrs
131+
.take()
132+
.into_iter()
133+
.map(|attr_or_spread| match attr_or_spread {
134+
JSXAttrOrSpread::JSXAttr(attr) => {
135+
let key: PropName = match attr.name {
136+
JSXAttrName::Ident(id) => id.into(),
137+
JSXAttrName::JSXNamespacedName(ns_name) => PropName::Str(
138+
format!("{}:{}", ns_name.ns.sym, ns_name.name.sym).into(),
139+
),
140+
};
141+
142+
let value: Box<Expr> = match attr.value {
143+
Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
144+
expr: JSXExpr::Expr(expr),
145+
..
146+
})) => expr,
147+
Some(JSXAttrValue::JSXElement(element)) => {
148+
Box::new(Expr::JSXElement(element))
149+
}
150+
Some(JSXAttrValue::JSXFragment(fragment)) => {
151+
Box::new(Expr::JSXFragment(fragment))
152+
}
153+
Some(JSXAttrValue::Lit(lit)) => lit.into(),
154+
None => Box::new(Expr::Lit(Lit::Bool(Bool {
155+
value: true,
156+
span: DUMMY_SP,
157+
}))),
158+
_ => unreachable!(),
159+
};
160+
161+
Box::new(Prop::KeyValue(KeyValueProp { key, value })).into()
162+
}
163+
JSXAttrOrSpread::SpreadElement(spread) => SpreadElement {
164+
expr: spread.expr,
165+
dot3_token: DUMMY_SP,
166+
}
167+
.into(),
168+
})
169+
.collect();
170+
171+
// replace attrs
172+
elem.attrs = vec![
173+
JSXAttr {
174+
name: Ident::new(DESERIALIZER_PROPS_ATTR.into(), DUMMY_SP).into(),
175+
span: DUMMY_SP,
176+
value: Some(
177+
JSXExprContainer {
178+
expr: Box::new(Expr::Call(CallExpr {
179+
args: vec![Expr::Object(ObjectLit {
180+
span: DUMMY_SP,
181+
props: list,
182+
})
183+
.into()],
184+
callee: Box::new(Expr::Ident(Ident::new(
185+
SERIALIZER_FUNCTION.into(),
186+
DUMMY_SP,
187+
)))
188+
.into(),
189+
span: DUMMY_SP,
190+
type_args: None,
191+
}))
192+
.into(),
193+
span: DUMMY_SP,
194+
}
195+
.into(),
196+
),
197+
}
198+
.into(),
199+
JSXAttr {
200+
name: Ident::new(DESERIALIZER_PROPS_COMPONENT.into(), DUMMY_SP).into(),
201+
span: DUMMY_SP,
202+
value: Some(
203+
JSXExprContainer {
204+
expr: Box::new(elem.name.as_expr()).into(),
205+
span: DUMMY_SP,
206+
}
207+
.into(),
208+
),
209+
}
210+
.into(),
211+
];
212+
213+
// change element name
214+
elem.name = Ident::new(DESERIALIZER_COMPONENT.into(), DUMMY_SP).into();
215+
self.transformed = true;
216+
}
217+
}
218+
}

0 commit comments

Comments
 (0)