Skip to content

Commit

Permalink
Apply react-server conditions to middleware (vercel#65424)
Browse files Browse the repository at this point in the history
### What

Reland vercel#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.
  • Loading branch information
huozhi authored and panteliselef committed May 20, 2024
1 parent be7dab8 commit d649e5e
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 72 deletions.
33 changes: 28 additions & 5 deletions packages/next-swc/crates/next-core/src/next_edge/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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(
Expand All @@ -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()
};

Expand Down
17 changes: 1 addition & 16 deletions packages/next-swc/crates/next-core/src/next_import_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
12 changes: 5 additions & 7 deletions packages/next-swc/crates/next-core/src/next_server/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ impl ServerContextType {
ServerContextType::AppRSC { .. }
| ServerContextType::AppRoute { .. }
| ServerContextType::PagesApi { .. }
| ServerContextType::Middleware { .. }
)
}
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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));
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/build/swc/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)/
Expand Down Expand Up @@ -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(
Expand Down
8 changes: 4 additions & 4 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1208,7 +1208,7 @@ export default async function getBaseWebpackConfig(
issuerLayer: {
or: [
...WEBPACK_LAYERS.GROUP.serverOnly,
...WEBPACK_LAYERS.GROUP.nonClientServerTarget,
...WEBPACK_LAYERS.GROUP.neutralTarget,
],
},
resolve: {
Expand All @@ -1220,7 +1220,7 @@ export default async function getBaseWebpackConfig(
issuerLayer: {
not: [
...WEBPACK_LAYERS.GROUP.serverOnly,
...WEBPACK_LAYERS.GROUP.nonClientServerTarget,
...WEBPACK_LAYERS.GROUP.neutralTarget,
],
},
resolve: {
Expand Down Expand Up @@ -1252,7 +1252,7 @@ export default async function getBaseWebpackConfig(
issuerLayer: {
not: [
...WEBPACK_LAYERS.GROUP.serverOnly,
...WEBPACK_LAYERS.GROUP.nonClientServerTarget,
...WEBPACK_LAYERS.GROUP.neutralTarget,
],
},
options: {
Expand All @@ -1270,7 +1270,7 @@ export default async function getBaseWebpackConfig(
],
loader: 'empty-loader',
issuerLayer: {
or: WEBPACK_LAYERS.GROUP.nonClientServerTarget,
or: WEBPACK_LAYERS.GROUP.neutralTarget,
},
},
...(hasAppDir
Expand Down
10 changes: 5 additions & 5 deletions packages/next/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions test/e2e/module-layer/middleware.js
Original file line number Diff line number Diff line change
@@ -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()
}
69 changes: 37 additions & 32 deletions test/e2e/module-layer/module-layer.test.ts
Original file line number Diff line number Diff line change
@@ -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,
})

Expand Down Expand Up @@ -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()
})
afterAll(async () => {
await next.patchFile(middlewareFile, middlewareContent)
// await next.patchFile(pagesApiFile, pagesApiContent)
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`
)
}
})
})
runTests()
})
}

describe('with server-only in server targets', () => {
runTests()
})
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/module-layer/pages/api/hello.js
Original file line number Diff line number Diff line change
@@ -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')
}

0 comments on commit d649e5e

Please sign in to comment.