Skip to content

Commit 5fd1d53

Browse files
kdy1huozhi
andauthored
feat(next-swc): lint for ssr: false in server components (#70378)
Co-authored-by: Jiachi Liu <inbox@huozhi.im>
1 parent 1d62f47 commit 5fd1d53

File tree

18 files changed

+192
-51
lines changed

18 files changed

+192
-51
lines changed

crates/next-custom-transforms/src/transforms/react_server_components.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use swc_core::{
2121
},
2222
};
2323

24-
use super::cjs_finder::contains_cjs;
24+
use super::{cjs_finder::contains_cjs, import_analyzer::ImportMap};
2525

2626
#[derive(Clone, Debug, Deserialize)]
2727
#[serde(untagged)]
@@ -76,6 +76,7 @@ enum RSCErrorKind {
7676
NextRscErrConflictMetadataExport(Span),
7777
NextRscErrInvalidApi((String, Span)),
7878
NextRscErrDeprecatedApi((String, String, Span)),
79+
NextSsrDynamicFalseNotAllowed(Span),
7980
}
8081

8182
impl<C: Comments> VisitMut for ReactServerComponents<C> {
@@ -301,6 +302,11 @@ fn report_error(app_dir: &Option<PathBuf>, filepath: &str, error_kind: RSCErrorK
301302
),
302303
_ => (format!("\"{source}\" is deprecated."), span),
303304
},
305+
RSCErrorKind::NextSsrDynamicFalseNotAllowed(span) => (
306+
"`ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a client component."
307+
.to_string(),
308+
span,
309+
),
304310
};
305311

306312
HANDLER.with(|handler| handler.struct_span_err(span, msg.as_str()).emit())
@@ -502,6 +508,7 @@ struct ReactServerComponentValidator {
502508
invalid_client_imports: Vec<JsWord>,
503509
invalid_client_lib_apis_mapping: HashMap<&'static str, Vec<&'static str>>,
504510
pub directive_import_collection: Option<(bool, bool, RcVec<ModuleImports>, RcVec<String>)>,
511+
imports: ImportMap,
505512
}
506513

507514
// A type to workaround a clippy warning.
@@ -576,6 +583,7 @@ impl ReactServerComponentValidator {
576583
invalid_client_imports: vec![JsWord::from("server-only"), JsWord::from("next/headers")],
577584

578585
invalid_client_lib_apis_mapping: [("next/server", vec!["unstable_after"])].into(),
586+
imports: ImportMap::default(),
579587
}
580588
}
581589

@@ -584,6 +592,13 @@ impl ReactServerComponentValidator {
584592
RE.is_match(filepath)
585593
}
586594

595+
fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
596+
match callee {
597+
Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
598+
_ => false,
599+
}
600+
}
601+
587602
// Asserts the server lib apis
588603
// e.g.
589604
// assert_invalid_server_lib_apis("react", import)
@@ -817,6 +832,44 @@ impl ReactServerComponentValidator {
817832
}
818833
}
819834
}
835+
836+
/// ```
837+
/// import dynamic from 'next/dynamic'
838+
///
839+
/// dynamic(() => import(...)) // ✅
840+
/// dynamic(() => import(...), { ssr: true }) // ✅
841+
/// dynamic(() => import(...), { ssr: false }) // ❌
842+
/// ```
843+
844+
fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
845+
if !self.is_callee_next_dynamic(&node.callee) {
846+
return None;
847+
}
848+
849+
let ssr_arg = node.args.get(1)?;
850+
let obj = ssr_arg.expr.as_object()?;
851+
852+
for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
853+
let is_ssr = match &prop.key {
854+
PropName::Ident(IdentName { sym, .. }) => sym == "ssr",
855+
PropName::Str(s) => s.value == "ssr",
856+
_ => false,
857+
};
858+
859+
if is_ssr {
860+
let value = prop.value.as_lit()?;
861+
if let Lit::Bool(Bool { value: false, .. }) = value {
862+
report_error(
863+
&self.app_dir,
864+
&self.filepath,
865+
RSCErrorKind::NextSsrDynamicFalseNotAllowed(node.span),
866+
);
867+
}
868+
}
869+
}
870+
871+
None
872+
}
820873
}
821874

822875
impl Visit for ReactServerComponentValidator {
@@ -830,7 +883,17 @@ impl Visit for ReactServerComponentValidator {
830883
}
831884
}
832885

886+
fn visit_call_expr(&mut self, node: &CallExpr) {
887+
node.visit_children_with(self);
888+
889+
if self.is_react_server_layer {
890+
self.check_for_next_ssr_false(node);
891+
}
892+
}
893+
833894
fn visit_module(&mut self, module: &Module) {
895+
self.imports = ImportMap::analyze(module);
896+
834897
let (is_client_entry, is_action_file, imports, export_names) =
835898
collect_top_level_directives_and_imports(&self.app_dir, &self.filepath, module);
836899
let imports = Rc::new(imports);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import dynamic from 'next/dynamic'
2+
3+
export default function () {
4+
return dynamic(() => import('client-only'), { ssr: false })
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import dynamic from 'next/dynamic';
2+
export default function() {
3+
return dynamic(()=>import('client-only'), {
4+
ssr: false
5+
});
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
x `ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a client component.
2+
,-[input.js:4:1]
3+
3 | export default function () {
4+
4 | return dynamic(() => import('client-only'), { ssr: false })
5+
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
5 | }
7+
`----

docs/02-app/01-building-your-application/06-optimizing/07-lazy-loading.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,6 @@ const ComponentC = dynamic(() => import('../components/C'), { ssr: false })
7272
If you dynamically import a Server Component, only the Client Components that are children of the Server Component will be lazy-loaded - not the Server Component itself.
7373
It will also help preload the static assets such as CSS when you're using it in Server Components.
7474

75-
> **Note:** `ssr: false` option is not supported in Server Components.
76-
7775
```jsx filename="app/page.js"
7876
import dynamic from 'next/dynamic'
7977

@@ -89,6 +87,9 @@ export default function ServerComponentExample() {
8987
}
9088
```
9189

90+
> **Note:** `ssr: false` option is not supported in Server Components. You will see an error if you try to use it in Server Components.
91+
> `ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a client component.
92+
9293
### Loading External Libraries
9394

9495
External libraries can be loaded on demand using the [`import()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/import) function. This example uses the external library `fuse.js` for fuzzy search. The module is only loaded on the client after the user types in the search input.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use client'
2+
3+
export default function Client() {
4+
return 'client'
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Root({ children }) {
2+
return (
3+
<html>
4+
<body>{children}</body>
5+
</html>
6+
)
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import dynamic from 'next/dynamic'
2+
3+
const DynamicClient = dynamic(() => import('./client'), { ssr: false })
4+
5+
export default function Page() {
6+
return <DynamicClient />
7+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
import {
3+
assertHasRedbox,
4+
getRedboxDescription,
5+
getRedboxSource,
6+
} from 'next-test-utils'
7+
8+
describe('app-dir - server-component-next-dynamic-ssr-false', () => {
9+
const { next } = nextTestSetup({
10+
files: __dirname,
11+
})
12+
13+
it('should error when use dynamic ssr:false in server component', async () => {
14+
const browser = await next.browser('/')
15+
await assertHasRedbox(browser)
16+
const redbox = {
17+
description: await getRedboxDescription(browser),
18+
source: await getRedboxSource(browser),
19+
}
20+
21+
expect(redbox.description).toBe('Failed to compile')
22+
if (process.env.TURBOPACK) {
23+
expect(redbox.source).toMatchInlineSnapshot(`
24+
"./app/page.js:3:23
25+
Ecmascript file had an error
26+
1 | import dynamic from 'next/dynamic'
27+
2 |
28+
> 3 | const DynamicClient = dynamic(() => import('./client'), { ssr: false })
29+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
30+
4 |
31+
5 | export default function Page() {
32+
6 | return <DynamicClient />
33+
34+
\`ssr: false\` is not allowed with \`next/dynamic\` in Server Components. Please move it into a client component."
35+
`)
36+
} else {
37+
expect(redbox.source).toMatchInlineSnapshot(`
38+
"./app/page.js
39+
Error: x \`ssr: false\` is not allowed with \`next/dynamic\` in Server Components. Please move it into a client component.
40+
,-[3:1]
41+
1 | import dynamic from 'next/dynamic'
42+
2 |
43+
3 | const DynamicClient = dynamic(() => import('./client'), { ssr: false })
44+
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
45+
4 |
46+
5 | export default function Page() {
47+
6 | return <DynamicClient />
48+
\`----"
49+
`)
50+
}
51+
})
52+
})
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import Client from './ssr-false-client'
2-
import Server from './ssr-false-server'
2+
// import Server from './ssr-false-server'
33

44
export default function Comp() {
55
return (
66
<>
77
<Client />
8-
<Server />
8+
{/* <Server /> */}
99
</>
1010
)
1111
}

0 commit comments

Comments
 (0)