Apply react-server conditions to middleware (#65424)

### What

Reland #57448 , add react-server condition resolving and apply
server-only rules to middleware

Closes NEXT-1653
Closes NEXT-3333

### Why

Middleware as the pre-routing layer that is indended to be light-weight.
Since it's on edge runtime and only run on server but not on client, it
doesn't need to include the client react bundles. Hence we apply
`react-server` export condition, that if users import React we can only
bundle server required APIs and if users use React client hooks we can
error.
This commit is contained in:
Jiachi Liu 2024-05-12 14:26:37 +02:00 committed by GitHub
parent c26840909c
commit 6635cc07a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 87 additions and 72 deletions

View file

@ -25,6 +25,7 @@ use crate::{
next_import_map::get_next_edge_import_map,
next_server::context::ServerContextType,
next_shared::resolve::{
get_invalid_client_only_resolve_plugin, get_invalid_styled_jsx_resolve_plugin,
ModuleFeatureReportResolvePlugin, NextSharedRuntimeResolvePlugin,
UnsupportedModulesResolvePlugin,
},
@ -99,6 +100,32 @@ pub async fn get_edge_resolve_options_context(
let ty = ty.into_value();
let mut plugins = match ty {
ServerContextType::Pages { .. }
| ServerContextType::PagesApi { .. }
| ServerContextType::AppSSR { .. } => {
vec![]
}
ServerContextType::AppRSC { .. }
| ServerContextType::AppRoute { .. }
| ServerContextType::PagesData { .. }
| ServerContextType::Middleware { .. }
| ServerContextType::Instrumentation => {
vec![
Vc::upcast(get_invalid_client_only_resolve_plugin(project_path)),
Vc::upcast(get_invalid_styled_jsx_resolve_plugin(project_path)),
]
}
};
let base_plugins = vec![
Vc::upcast(ModuleFeatureReportResolvePlugin::new(project_path)),
Vc::upcast(UnsupportedModulesResolvePlugin::new(project_path)),
Vc::upcast(NextSharedRuntimeResolvePlugin::new(project_path)),
];
plugins.extend_from_slice(&base_plugins);
// https://github.com/vercel/next.js/blob/bf52c254973d99fed9d71507a2e818af80b8ade7/packages/next/src/build/webpack-config.ts#L96-L102
let mut custom_conditions = vec![mode.await?.condition().to_string()];
custom_conditions.extend(
@ -119,11 +146,7 @@ pub async fn get_edge_resolve_options_context(
import_map: Some(next_edge_import_map),
module: true,
browser: true,
plugins: vec![
Vc::upcast(ModuleFeatureReportResolvePlugin::new(project_path)),
Vc::upcast(UnsupportedModulesResolvePlugin::new(project_path)),
Vc::upcast(NextSharedRuntimeResolvePlugin::new(project_path)),
],
plugins,
..Default::default()
};

View file

@ -613,6 +613,7 @@ async fn insert_next_server_special_aliases(
| ServerContextType::PagesApi { .. }
| ServerContextType::AppRSC { .. }
| ServerContextType::AppRoute { .. }
| ServerContextType::Middleware { .. }
| ServerContextType::Instrumentation => {
insert_exact_alias_map(
import_map,
@ -637,22 +638,6 @@ async fn insert_next_server_special_aliases(
},
);
}
// Potential the bundle introduced into middleware and api can be poisoned by
// client-only but not being used, so we disabled the `client-only` erroring
// on these layers. `server-only` is still available.
ServerContextType::Middleware => {
insert_exact_alias_map(
import_map,
project_path,
indexmap! {
"server-only" => "next/dist/compiled/server-only/empty".to_string(),
"client-only" => "next/dist/compiled/client-only/index".to_string(),
"next/dist/compiled/server-only" => "next/dist/compiled/server-only/empty".to_string(),
"next/dist/compiled/client-only" => "next/dist/compiled/client-only/index".to_string(),
"next/dist/compiled/client-only/error" => "next/dist/compiled/client-only/index".to_string(),
},
);
}
}
import_map.insert_exact_alias(

View file

@ -105,6 +105,7 @@ impl ServerContextType {
ServerContextType::AppRSC { .. }
| ServerContextType::AppRoute { .. }
| ServerContextType::PagesApi { .. }
| ServerContextType::Middleware { .. }
)
}
}
@ -200,8 +201,8 @@ pub async fn get_server_resolve_options_context(
let mut plugins = match ty {
ServerContextType::Pages { .. }
| ServerContextType::PagesData { .. }
| ServerContextType::PagesApi { .. } => {
| ServerContextType::PagesApi { .. }
| ServerContextType::PagesData { .. } => {
vec![
Vc::upcast(module_feature_report_resolve_plugin),
Vc::upcast(unsupported_modules_resolve_plugin),
@ -246,13 +247,13 @@ pub async fn get_server_resolve_options_context(
// means each resolve plugin must be injected only for the context where the
// alias resolves into the error. The alias lives in here: https://github.com/vercel/next.js/blob/0060de1c4905593ea875fa7250d4b5d5ce10897d/packages/next-swc/crates/next-core/src/next_import_map.rs#L534
match ty {
ServerContextType::Pages { .. } => {
ServerContextType::Pages { .. } | ServerContextType::PagesApi { .. } => {
//noop
}
ServerContextType::PagesData { .. }
| ServerContextType::PagesApi { .. }
| ServerContextType::AppRSC { .. }
| ServerContextType::AppRoute { .. }
| ServerContextType::Middleware { .. }
| ServerContextType::Instrumentation => {
plugins.push(Vc::upcast(invalid_client_only_resolve_plugin));
plugins.push(Vc::upcast(invalid_styled_jsx_client_only_resolve_plugin));
@ -261,9 +262,6 @@ pub async fn get_server_resolve_options_context(
//[TODO] Build error in this context makes rsc-build-error.ts fail which expects runtime error code
// looks like webpack and turbopack have different order, webpack runs rsc transform first, turbopack triggers resolve plugin first.
}
ServerContextType::Middleware => {
//noop
}
}
let resolve_options_context = ResolveOptionsContext {

View file

@ -6,6 +6,7 @@ import type {
StyledComponentsConfig,
} from '../../server/config-shared'
import type { ResolvedBaseUrl } from '../load-jsconfig'
import { isWebpackServerOnlyLayer } from '../utils'
const nextDistPath =
/(next[\\/]dist[\\/]shared[\\/]lib)|(next[\\/]dist[\\/]client)|(next[\\/]dist[\\/]pages)/
@ -78,8 +79,7 @@ function getBaseSWCOptions({
serverComponents?: boolean
bundleLayer?: WebpackLayerName
}) {
const isReactServerLayer =
bundleLayer === WEBPACK_LAYERS.reactServerComponents
const isReactServerLayer = isWebpackServerOnlyLayer(bundleLayer)
const parserConfig = getParserOptions({ filename, jsConfig })
const paths = jsConfig?.compilerOptions?.paths
const enableDecorators = Boolean(

View file

@ -1208,7 +1208,7 @@ export default async function getBaseWebpackConfig(
issuerLayer: {
or: [
...WEBPACK_LAYERS.GROUP.serverOnly,
...WEBPACK_LAYERS.GROUP.nonClientServerTarget,
...WEBPACK_LAYERS.GROUP.neutralTarget,
],
},
resolve: {
@ -1220,7 +1220,7 @@ export default async function getBaseWebpackConfig(
issuerLayer: {
not: [
...WEBPACK_LAYERS.GROUP.serverOnly,
...WEBPACK_LAYERS.GROUP.nonClientServerTarget,
...WEBPACK_LAYERS.GROUP.neutralTarget,
],
},
resolve: {
@ -1252,7 +1252,7 @@ export default async function getBaseWebpackConfig(
issuerLayer: {
not: [
...WEBPACK_LAYERS.GROUP.serverOnly,
...WEBPACK_LAYERS.GROUP.nonClientServerTarget,
...WEBPACK_LAYERS.GROUP.neutralTarget,
],
},
options: {
@ -1270,7 +1270,7 @@ export default async function getBaseWebpackConfig(
],
loader: 'empty-loader',
issuerLayer: {
or: WEBPACK_LAYERS.GROUP.nonClientServerTarget,
or: WEBPACK_LAYERS.GROUP.neutralTarget,
},
},
...(hasAppDir

View file

@ -168,16 +168,16 @@ const WEBPACK_LAYERS = {
WEBPACK_LAYERS_NAMES.appMetadataRoute,
WEBPACK_LAYERS_NAMES.appRouteHandler,
WEBPACK_LAYERS_NAMES.instrument,
WEBPACK_LAYERS_NAMES.middleware,
],
neutralTarget: [
// pages api
WEBPACK_LAYERS_NAMES.api,
],
clientOnly: [
WEBPACK_LAYERS_NAMES.serverSideRendering,
WEBPACK_LAYERS_NAMES.appPagesBrowser,
],
nonClientServerTarget: [
// middleware and pages api
WEBPACK_LAYERS_NAMES.middleware,
WEBPACK_LAYERS_NAMES.api,
],
app: [
WEBPACK_LAYERS_NAMES.reactServerComponents,
WEBPACK_LAYERS_NAMES.actionBrowser,

View file

@ -1,7 +1,11 @@
import 'server-only'
import React from 'react'
import { NextResponse } from 'next/server'
// import './lib/mixed-lib'
export function middleware(request) {
if (React.useState) {
throw new Error('React.useState should not be defined in server layer')
}
return NextResponse.next()
}

View file

@ -1,7 +1,8 @@
import { nextTestSetup } from 'e2e-utils'
import { getRedboxSource, hasRedbox, retry } from 'next-test-utils'
describe('module layer', () => {
const { next, isNextStart } = nextTestSetup({
const { next, isNextStart, isNextDev, isTurbopack } = nextTestSetup({
files: __dirname,
})
@ -59,43 +60,47 @@ describe('module layer', () => {
}
}
describe('no server-only in server targets', () => {
const middlewareFile = 'middleware.js'
// const pagesApiFile = 'pages/api/hello.js'
let middlewareContent = ''
// let pagesApiContent = ''
if (isNextDev) {
describe('client packages in middleware', () => {
const middlewareFile = 'middleware.js'
let middlewareContent = ''
beforeAll(async () => {
await next.stop()
afterAll(async () => {
await next.patchFile(middlewareFile, middlewareContent)
})
middlewareContent = await next.readFile(middlewareFile)
// pagesApiContent = await next.readFile(pagesApiFile)
it('should error when import server packages in middleware', async () => {
const browser = await next.browser('/')
await next.patchFile(
middlewareFile,
middlewareContent
.replace("import 'server-only'", "// import 'server-only'")
.replace("// import './lib/mixed-lib'", "import './lib/mixed-lib'")
)
middlewareContent = await next.readFile(middlewareFile)
// await next.patchFile(
// pagesApiFile,
// pagesApiContent
// .replace("import 'server-only'", "// import 'server-only'")
// .replace(
// "// import '../../lib/mixed-lib'",
// "import '../../lib/mixed-lib'"
// )
// )
await next.patchFile(
middlewareFile,
middlewareContent
.replace("import 'server-only'", "// import 'server-only'")
.replace("// import './lib/mixed-lib'", "import './lib/mixed-lib'")
)
await next.start()
const existingCliOutputLength = next.cliOutput.length
await retry(async () => {
expect(await hasRedbox(browser)).toBe(true)
const source = await getRedboxSource(browser)
expect(source).toContain(
`'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component.`
)
})
if (!isTurbopack) {
const newCliOutput = next.cliOutput.slice(existingCliOutputLength)
expect(newCliOutput).toContain('./middleware.js')
expect(newCliOutput).toContain(
`'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component`
)
}
})
})
afterAll(async () => {
await next.patchFile(middlewareFile, middlewareContent)
// await next.patchFile(pagesApiFile, pagesApiContent)
})
runTests()
})
}
describe('with server-only in server targets', () => {
runTests()
})

View file

@ -1,5 +1,5 @@
import 'server-only'
export default function handler(req, res) {
return res.send('pages/api/hello.js:')
return res.send('pages/api/hello.js')
}