Skip to content

Commit ffc0e54

Browse files
authored
Allow server-only in server targets and client-only in client components targets to be available (#55394)
Users want to use `server-only` to restrict the middleware / app routes / pages api, but now it's failing as we're treating them as different webpack layers, but validating the `server-only` only with server components layers. Here we modify the rules a bit to let everyone can use "server-only" for the bundles that targeting server-side. For next-swc transformer, we introduce the new option `bundleType` which only has `"server" | "client" | "default"` 3 values: * - `server` for server-side targets, like server components, app routes, pages api, middleware * - `client` for client components targets such as client components app pages, or page routes under pages directory. * - `default` for environment like jest, we don't validate module graph with swc, replaced the `disable_checks` introduced [#54891](#54891). Refactor a bit webpack-config to adapt to the new rules, after that `server-only` will be able to used in the server-side targets conventions like middleware and `pages/api` Fixes #43700 Fixes #54549 Fixes #52833 Closes NEXT-1616 Closes NEXT-1607 Closes NEXT-1385
1 parent b2facf5 commit ffc0e54

File tree

22 files changed

+319
-111
lines changed

22 files changed

+319
-111
lines changed

packages/next-swc/crates/core/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ use turbopack_binding::swc::{
4646
SyntaxContext,
4747
},
4848
ecma::{
49-
ast::EsVersion, parser::parse_file_as_module, transforms::base::pass::noop, visit::Fold,
49+
ast::EsVersion, atoms::JsWord, parser::parse_file_as_module,
50+
transforms::base::pass::noop, visit::Fold,
5051
},
5152
},
5253
custom_transform::modularize_imports,
@@ -97,7 +98,7 @@ pub struct TransformOptions {
9798
pub is_server: bool,
9899

99100
#[serde(default)]
100-
pub disable_checks: bool,
101+
pub bundle_target: JsWord,
101102

102103
#[serde(default)]
103104
pub server_components: Option<react_server_components::Config>,
@@ -198,7 +199,7 @@ where
198199
config.clone(),
199200
comments.clone(),
200201
opts.app_dir.clone(),
201-
opts.disable_checks
202+
opts.bundle_target.clone()
202203
)),
203204
_ => Either::Right(noop()),
204205
},

packages/next-swc/crates/core/src/react_server_components.rs

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ struct ReactServerComponents<C: Comments> {
5050
invalid_client_imports: Vec<JsWord>,
5151
invalid_server_react_apis: Vec<JsWord>,
5252
invalid_server_react_dom_apis: Vec<JsWord>,
53-
disable_checks: bool,
53+
bundle_target: String,
5454
}
5555

5656
struct ModuleImports {
@@ -67,15 +67,25 @@ impl<C: Comments> VisitMut for ReactServerComponents<C> {
6767
let is_cjs = contains_cjs(module);
6868

6969
if self.is_server {
70-
if !is_client_entry {
71-
self.assert_server_graph(&imports, module);
72-
} else {
70+
if is_client_entry {
7371
self.to_module_ref(module, is_cjs);
7472
return;
73+
} else if self.bundle_target == "server" {
74+
// Only assert server graph if file's bundle target is "server", e.g.
75+
// * server components pages
76+
// * pages bundles on SSR layer
77+
// * middleware
78+
// * app/pages api routes
79+
self.assert_server_graph(&imports, module);
7580
}
7681
} else {
77-
if !is_action_file {
78-
self.assert_client_graph(&imports, module);
82+
// Only assert client graph if the file is not an action file,
83+
// and bundle target is "client" e.g.
84+
// * client components pages
85+
// * pages bundles on browser layer
86+
if !is_action_file && self.bundle_target == "client" {
87+
self.assert_client_graph(&imports);
88+
self.assert_invalid_api(module, true);
7989
}
8090
if is_client_entry {
8191
self.prepend_comment_node(module, is_cjs);
@@ -129,7 +139,7 @@ impl<C: Comments> ReactServerComponents<C> {
129139
if is_action_file {
130140
panic_both_directives(expr_stmt.span)
131141
}
132-
} else if !self.disable_checks {
142+
} else if self.bundle_target != "default" {
133143
HANDLER.with(|handler| {
134144
handler
135145
.struct_span_err(
@@ -334,9 +344,6 @@ impl<C: Comments> ReactServerComponents<C> {
334344
}
335345

336346
fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
337-
if self.disable_checks {
338-
return;
339-
}
340347
for import in imports {
341348
let source = import.source.0.clone();
342349
if self.invalid_server_imports.contains(&source) {
@@ -408,10 +415,7 @@ impl<C: Comments> ReactServerComponents<C> {
408415
}
409416
}
410417

411-
fn assert_client_graph(&self, imports: &[ModuleImports], module: &Module) {
412-
if self.disable_checks {
413-
return;
414-
}
418+
fn assert_client_graph(&self, imports: &[ModuleImports]) {
415419
for import in imports {
416420
let source = import.source.0.clone();
417421
if self.invalid_client_imports.contains(&source) {
@@ -425,8 +429,6 @@ impl<C: Comments> ReactServerComponents<C> {
425429
})
426430
}
427431
}
428-
429-
self.assert_invalid_api(module, true);
430432
}
431433

432434
fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
@@ -567,17 +569,17 @@ pub fn server_components<C: Comments>(
567569
config: Config,
568570
comments: C,
569571
app_dir: Option<PathBuf>,
570-
disable_checks: bool,
572+
bundle_target: JsWord,
571573
) -> impl Fold + VisitMut {
572574
let is_server: bool = match &config {
573575
Config::WithOptions(x) => x.is_server,
574576
_ => true,
575577
};
576578
as_folder(ReactServerComponents {
577-
disable_checks,
578579
is_server,
579580
comments,
580581
filepath: filename.to_string(),
582+
bundle_target: bundle_target.to_string(),
581583
app_dir,
582584
export_names: vec![],
583585
invalid_server_imports: vec![

packages/next-swc/crates/core/tests/errors.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ fn react_server_components_server_graph_errors(input: PathBuf) {
9797
),
9898
tr.comments.as_ref().clone(),
9999
None,
100-
false,
100+
String::from("server").into(),
101101
)
102102
},
103103
&input,
@@ -122,7 +122,7 @@ fn react_server_components_client_graph_errors(input: PathBuf) {
122122
),
123123
tr.comments.as_ref().clone(),
124124
None,
125-
false,
125+
String::from("client").into(),
126126
)
127127
},
128128
&input,
@@ -169,7 +169,7 @@ fn react_server_actions_server_errors(input: PathBuf) {
169169
),
170170
tr.comments.as_ref().clone(),
171171
None,
172-
false
172+
String::from("default").into(),
173173
),
174174
server_actions(
175175
&FileName::Real("/app/item.js".into()),
@@ -205,7 +205,7 @@ fn react_server_actions_client_errors(input: PathBuf) {
205205
),
206206
tr.comments.as_ref().clone(),
207207
None,
208-
false
208+
String::from("client").into(),
209209
),
210210
server_actions(
211211
&FileName::Real("/app/item.js".into()),

packages/next-swc/crates/core/tests/fixture.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ fn react_server_components_server_graph_fixture(input: PathBuf) {
326326
),
327327
tr.comments.as_ref().clone(),
328328
None,
329-
false,
329+
String::from("default").into(),
330330
)
331331
},
332332
&input,
@@ -348,7 +348,7 @@ fn react_server_components_no_checks_server_graph_fixture(input: PathBuf) {
348348
),
349349
tr.comments.as_ref().clone(),
350350
None,
351-
true,
351+
String::from("default").into(),
352352
)
353353
},
354354
&input,
@@ -370,7 +370,7 @@ fn react_server_components_client_graph_fixture(input: PathBuf) {
370370
),
371371
tr.comments.as_ref().clone(),
372372
None,
373-
false,
373+
String::from("default").into(),
374374
)
375375
},
376376
&input,
@@ -392,7 +392,7 @@ fn react_server_components_no_checks_client_graph_fixture(input: PathBuf) {
392392
),
393393
tr.comments.as_ref().clone(),
394394
None,
395-
true,
395+
String::from("default").into(),
396396
)
397397
},
398398
&input,

packages/next-swc/crates/core/tests/full.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ fn test(input: &Path, minify: bool) {
8181
auto_modularize_imports: None,
8282
optimize_barrel_exports: None,
8383
optimize_server_react: None,
84-
disable_checks: false,
84+
bundle_target: String::from("default").into(),
8585
};
8686

8787
let unresolved_mark = Mark::new();

packages/next/src/build/swc/options.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type {
55
StyledComponentsConfig,
66
} from '../../server/config-shared'
77

8+
type BundleType = 'client' | 'server' | 'default'
9+
810
const nextDistPath =
911
/(next[\\/]dist[\\/]shared[\\/]lib)|(next[\\/]dist[\\/]client)|(next[\\/]dist[\\/]pages)/
1012

@@ -42,6 +44,7 @@ function getBaseSWCOptions({
4244
jsConfig,
4345
swcCacheDir,
4446
isServerLayer,
47+
bundleTarget,
4548
hasServerComponents,
4649
isServerActionsEnabled,
4750
}: {
@@ -55,6 +58,7 @@ function getBaseSWCOptions({
5558
compilerOptions: NextConfig['compiler']
5659
resolvedBaseUrl?: string
5760
jsConfig: any
61+
bundleTarget: BundleType
5862
swcCacheDir?: string
5963
isServerLayer?: boolean
6064
hasServerComponents?: boolean
@@ -184,7 +188,7 @@ function getBaseSWCOptions({
184188
isServer: !!isServerLayer,
185189
}
186190
: undefined,
187-
disableChecks: false,
191+
bundleTarget,
188192
}
189193
}
190194

@@ -274,13 +278,14 @@ export function getJestSWCOptions({
274278
resolvedBaseUrl,
275279
// Don't apply server layer transformations for Jest
276280
isServerLayer: false,
281+
// Disable server / client graph assertions for Jest
282+
bundleTarget: 'default',
277283
})
278284

279285
const isNextDist = nextDistPath.test(filename)
280286

281287
return {
282288
...baseOptions,
283-
disableChecks: true,
284289
env: {
285290
targets: {
286291
// Targets the current version of Node.js
@@ -317,6 +322,7 @@ export function getLoaderSWCOptions({
317322
isServerLayer,
318323
isServerActionsEnabled,
319324
optimizeBarrelExports,
325+
bundleTarget = 'client',
320326
}: // This is not passed yet as "paths" resolving is handled by webpack currently.
321327
// resolvedBaseUrl,
322328
{
@@ -338,6 +344,7 @@ export function getLoaderSWCOptions({
338344
supportedBrowsers: string[]
339345
swcCacheDir: string
340346
relativeFilePathFromRoot: string
347+
bundleTarget: BundleType
341348
hasServerComponents?: boolean
342349
isServerLayer: boolean
343350
isServerActionsEnabled?: boolean
@@ -357,6 +364,7 @@ export function getLoaderSWCOptions({
357364
hasServerComponents,
358365
isServerLayer,
359366
isServerActionsEnabled,
367+
bundleTarget,
360368
})
361369
baseOptions.fontLoaders = {
362370
fontLoaders: [

packages/next/src/build/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
MiddlewareManifest,
1616
} from './webpack/plugins/middleware-plugin'
1717
import type { StaticGenerationAsyncStorage } from '../client/components/static-generation-async-storage.external'
18+
import type { WebpackLayerName } from '../lib/constants'
1819

1920
import '../server/require-hook'
2021
import '../server/node-polyfill-fetch'
@@ -35,6 +36,7 @@ import {
3536
SERVER_PROPS_SSG_CONFLICT,
3637
MIDDLEWARE_FILENAME,
3738
INSTRUMENTATION_HOOK_FILENAME,
39+
WEBPACK_LAYERS,
3840
} from '../lib/constants'
3941
import { MODERN_BROWSERSLIST_TARGET } from '../shared/lib/constants'
4042
import prettyBytes from '../lib/pretty-bytes'
@@ -2116,3 +2118,15 @@ export function getSupportedBrowsers(
21162118
// Uses modern browsers as the default.
21172119
return MODERN_BROWSERSLIST_TARGET
21182120
}
2121+
2122+
export function isWebpackServerLayer(
2123+
layer: WebpackLayerName | null | undefined
2124+
): boolean {
2125+
return Boolean(layer && WEBPACK_LAYERS.GROUP.server.includes(layer as any))
2126+
}
2127+
2128+
export function isWebpackDefaultLayer(
2129+
layer: WebpackLayerName | null | undefined
2130+
): boolean {
2131+
return layer === null || layer === undefined
2132+
}

0 commit comments

Comments
 (0)