Skip to content

Commit 295f9da

Browse files
huozhishuding
andauthored
Client directive (#40415)
## Feature Change server components convention from using `.server.js` / `.client.js` file extension to determine it's a server or client component to using `'client'` js literal as a directive for determine client components boundary. React RFC: reactjs/rfcs#189 New behavior doesn't consume `.server.js` as server components any more, if you're enabling `serverComponents` flag, every `page.js` in app dir will become server components by default. If you adding a `'client'` directive to the page, then that page will become a client component. This rule also applies to the normal js components, client components will require a `'client'` directive to indicate its identity, instead of having a `.client.js` extension. - [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` Co-authored-by: Shu Ding <[email protected]>
1 parent f0ed328 commit 295f9da

File tree

181 files changed

+282
-334
lines changed

Some content is hidden

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

181 files changed

+282
-334
lines changed

packages/next/build/analysis/get-page-static-info.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { SERVER_RUNTIME } from '../../lib/constants'
1313
import { ServerRuntime } from 'next/types'
1414
import { checkCustomRoutes } from '../../lib/load-custom-routes'
1515
import { matcher } from 'next/dist/compiled/micromatch'
16+
import { RSC_MODULE_TYPES } from '../../shared/lib/constants'
1617

1718
export interface MiddlewareConfig {
1819
matchers: MiddlewareMatcher[]
@@ -29,9 +30,18 @@ export interface PageStaticInfo {
2930
runtime?: ServerRuntime
3031
ssg?: boolean
3132
ssr?: boolean
33+
rsc?: RSCModuleType
3234
middleware?: Partial<MiddlewareConfig>
3335
}
3436

37+
const CLIENT_MODULE_LABEL = `/* __next_internal_client_entry_do_not_use__ */`
38+
export type RSCModuleType = 'server' | 'client'
39+
export function getRSCModuleType(source: string): RSCModuleType {
40+
return source.includes(CLIENT_MODULE_LABEL)
41+
? RSC_MODULE_TYPES.client
42+
: RSC_MODULE_TYPES.server
43+
}
44+
3545
/**
3646
* Receives a parsed AST from SWC and checks if it belongs to a module that
3747
* requires a runtime to be specified. Those are:
@@ -252,6 +262,7 @@ export async function getPageStaticInfo(params: {
252262
) {
253263
const swcAST = await parseModule(pageFilePath, fileContent)
254264
const { ssg, ssr } = checkExports(swcAST)
265+
const rsc = getRSCModuleType(fileContent)
255266

256267
// default / failsafe value for config
257268
let config: any = {}
@@ -303,10 +314,16 @@ export async function getPageStaticInfo(params: {
303314
return {
304315
ssr,
305316
ssg,
317+
rsc,
306318
...(middlewareConfig && { middleware: middlewareConfig }),
307319
...(runtime && { runtime }),
308320
}
309321
}
310322

311-
return { ssr: false, ssg: false, runtime: nextConfig.experimental?.runtime }
323+
return {
324+
ssr: false,
325+
ssg: false,
326+
rsc: RSC_MODULE_TYPES.server,
327+
runtime: nextConfig.experimental?.runtime,
328+
}
312329
}

packages/next/build/entries.ts

+9-17
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
SERVER_RUNTIME,
2222
WEBPACK_LAYERS,
2323
} from '../lib/constants'
24+
import { RSC_MODULE_TYPES } from '../shared/lib/constants'
2425
import {
2526
CLIENT_STATIC_FILES_RUNTIME_AMP,
2627
CLIENT_STATIC_FILES_RUNTIME_MAIN,
@@ -37,14 +38,12 @@ import { warn } from './output/log'
3738
import {
3839
isMiddlewareFile,
3940
isMiddlewareFilename,
40-
isServerComponentPage,
4141
NestedMiddlewareError,
4242
MiddlewareInServerlessTargetError,
4343
} from './utils'
4444
import { getPageStaticInfo } from './analysis/get-page-static-info'
4545
import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep'
4646
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
47-
import { serverComponentRegex } from './webpack/loaders/utils'
4847
import { ServerRuntime } from '../types'
4948
import { normalizeAppPath } from '../shared/lib/router/utils/app-paths'
5049
import { encodeMatchers } from './webpack/loaders/next-middleware-loader'
@@ -66,14 +65,12 @@ export function getPageFromPath(pagePath: string, pageExtensions: string[]) {
6665
}
6766

6867
export function createPagesMapping({
69-
hasServerComponents,
7068
isDev,
7169
pageExtensions,
7270
pagePaths,
7371
pagesType,
7472
pagesDir,
7573
}: {
76-
hasServerComponents: boolean
7774
isDev: boolean
7875
pageExtensions: string[]
7976
pagePaths: string[]
@@ -90,13 +87,6 @@ export function createPagesMapping({
9087

9188
const pageKey = getPageFromPath(pagePath, pageExtensions)
9289

93-
// Assume that if there's a Client Component, that there is
94-
// a matching Server Component that will map to the page.
95-
// so we will not process it
96-
if (hasServerComponents && /\.client$/.test(pageKey)) {
97-
return result
98-
}
99-
10090
if (pageKey in result) {
10191
warn(
10292
`Duplicate page detected. ${chalk.cyan(
@@ -208,10 +198,7 @@ export function getEdgeServerEntry(opts: {
208198
absolutePagePath: opts.absolutePagePath,
209199
buildId: opts.buildId,
210200
dev: opts.isDev,
211-
isServerComponent: isServerComponentPage(
212-
opts.config,
213-
opts.absolutePagePath
214-
),
201+
isServerComponent: opts.isServerComponent,
215202
page: opts.page,
216203
stringifiedConfig: JSON.stringify(opts.config),
217204
pagesType: opts.pagesType,
@@ -418,8 +405,10 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
418405
nestedMiddleware.push(page)
419406
}
420407

421-
const isServerComponent = serverComponentRegex.test(absolutePagePath)
422-
const isInsideAppDir = appDir && absolutePagePath.startsWith(appDir)
408+
const isInsideAppDir =
409+
!!appDir &&
410+
(absolutePagePath.startsWith(APP_DIR_ALIAS) ||
411+
absolutePagePath.startsWith(appDir))
423412

424413
const staticInfo = await getPageStaticInfo({
425414
nextConfig: config,
@@ -428,6 +417,9 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
428417
page,
429418
})
430419

420+
const isServerComponent =
421+
isInsideAppDir && staticInfo.rsc !== RSC_MODULE_TYPES.client
422+
431423
if (isMiddlewareFile(page)) {
432424
middlewareMatchers = staticInfo.middleware?.matchers ?? [
433425
{ regexp: '.*' },

packages/next/build/index.ts

+11-18
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
COMPILER_NAMES,
5959
APP_BUILD_MANIFEST,
6060
FLIGHT_SERVER_CSS_MANIFEST,
61+
RSC_MODULE_TYPES,
6162
} from '../shared/lib/constants'
6263
import { getSortedRoutes, isDynamicRoute } from '../shared/lib/router/utils'
6364
import { __ApiPreviewProps } from '../server/api-utils'
@@ -96,7 +97,6 @@ import {
9697
printTreeView,
9798
copyTracedFiles,
9899
isReservedPage,
99-
isServerComponentPage,
100100
} from './utils'
101101
import getBaseWebpackConfig from './webpack-config'
102102
import { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
@@ -489,7 +489,6 @@ export default async function build(
489489
.traceChild('create-pages-mapping')
490490
.traceFn(() =>
491491
createPagesMapping({
492-
hasServerComponents,
493492
isDev: false,
494493
pageExtensions: config.pageExtensions,
495494
pagesType: 'pages',
@@ -506,7 +505,6 @@ export default async function build(
506505
.traceFn(() =>
507506
createPagesMapping({
508507
pagePaths: appPaths!,
509-
hasServerComponents,
510508
isDev: false,
511509
pagesType: 'app',
512510
pageExtensions: config.pageExtensions,
@@ -518,7 +516,6 @@ export default async function build(
518516
let mappedRootPaths: { [page: string]: string } = {}
519517
if (rootPaths.length > 0) {
520518
mappedRootPaths = createPagesMapping({
521-
hasServerComponents,
522519
isDev: false,
523520
pageExtensions: config.pageExtensions,
524521
pagePaths: rootPaths,
@@ -1267,21 +1264,17 @@ export default async function build(
12671264
)
12681265
: appPaths?.find((p) => p.startsWith(actualPage + '/page.'))
12691266

1270-
const pageRuntime =
1267+
const staticInfo =
12711268
pagesDir && pageType === 'pages' && pagePath
1272-
? (
1273-
await getPageStaticInfo({
1274-
pageFilePath: join(pagesDir, pagePath),
1275-
nextConfig: config,
1276-
})
1277-
).runtime
1278-
: undefined
1279-
1280-
if (hasServerComponents && pagePath) {
1281-
if (isServerComponentPage(config, pagePath)) {
1282-
isServerComponent = true
1283-
}
1284-
}
1269+
? await getPageStaticInfo({
1270+
pageFilePath: join(pagesDir, pagePath),
1271+
nextConfig: config,
1272+
})
1273+
: {}
1274+
const pageRuntime = staticInfo.runtime
1275+
isServerComponent =
1276+
pageType === 'app' &&
1277+
staticInfo.rsc !== RSC_MODULE_TYPES.client
12851278

12861279
if (
12871280
// Only calculate page static information if the page is not an

packages/next/build/utils.ts

-27
Original file line numberDiff line numberDiff line change
@@ -1327,33 +1327,6 @@ export function detectConflictingPaths(
13271327
}
13281328
}
13291329

1330-
/**
1331-
* With RSC we automatically add .server and .client to page extensions. This
1332-
* function allows to remove them for cases where we just need to strip out
1333-
* the actual extension keeping the .server and .client.
1334-
*/
1335-
export function withoutRSCExtensions(pageExtensions: string[]): string[] {
1336-
return pageExtensions.filter(
1337-
(ext) => !ext.startsWith('client.') && !ext.startsWith('server.')
1338-
)
1339-
}
1340-
1341-
export function isServerComponentPage(
1342-
nextConfig: NextConfigComplete,
1343-
filePath: string
1344-
): boolean {
1345-
if (!nextConfig.experimental.serverComponents) {
1346-
return false
1347-
}
1348-
1349-
const rawPageExtensions = withoutRSCExtensions(
1350-
nextConfig.pageExtensions || []
1351-
)
1352-
return rawPageExtensions.some((ext) => {
1353-
return filePath.endsWith(`.server.${ext}`)
1354-
})
1355-
}
1356-
13571330
export async function copyTracedFiles(
13581331
dir: string,
13591332
distDir: string,

packages/next/build/webpack-config.ts

+15-36
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ import type {
5656
} from './webpack/plugins/telemetry-plugin'
5757
import type { Span } from '../trace'
5858
import type { MiddlewareMatcher } from './analysis/get-page-static-info'
59-
import { withoutRSCExtensions } from './utils'
6059
import browserslist from 'next/dist/compiled/browserslist'
6160
import loadJsConfig from './load-jsconfig'
6261
import { loadBindings } from './swc'
@@ -687,13 +686,7 @@ export default async function getBaseWebpackConfig(
687686
babel: getBabelOrSwcLoader(),
688687
}
689688

690-
const rawPageExtensions = hasServerComponents
691-
? withoutRSCExtensions(config.pageExtensions)
692-
: config.pageExtensions
693-
694-
const serverComponentsRegex = new RegExp(
695-
`\\.server\\.(${rawPageExtensions.join('|')})$`
696-
)
689+
const pageExtensions = config.pageExtensions
697690

698691
const babelIncludeRegexes: RegExp[] = [
699692
/next[\\/]dist[\\/]shared[\\/]lib/,
@@ -801,7 +794,7 @@ export default async function getBaseWebpackConfig(
801794
if (dev) {
802795
customAppAliases[`${PAGES_DIR_ALIAS}/_app`] = [
803796
...(pagesDir
804-
? rawPageExtensions.reduce((prev, ext) => {
797+
? pageExtensions.reduce((prev, ext) => {
805798
prev.push(path.join(pagesDir, `_app.${ext}`))
806799
return prev
807800
}, [] as string[])
@@ -810,7 +803,7 @@ export default async function getBaseWebpackConfig(
810803
]
811804
customAppAliases[`${PAGES_DIR_ALIAS}/_error`] = [
812805
...(pagesDir
813-
? rawPageExtensions.reduce((prev, ext) => {
806+
? pageExtensions.reduce((prev, ext) => {
814807
prev.push(path.join(pagesDir, `_error.${ext}`))
815808
return prev
816809
}, [] as string[])
@@ -819,7 +812,7 @@ export default async function getBaseWebpackConfig(
819812
]
820813
customDocumentAliases[`${PAGES_DIR_ALIAS}/_document`] = [
821814
...(pagesDir
822-
? rawPageExtensions.reduce((prev, ext) => {
815+
? pageExtensions.reduce((prev, ext) => {
823816
prev.push(path.join(pagesDir, `_document.${ext}`))
824817
return prev
825818
}, [] as string[])
@@ -874,7 +867,7 @@ export default async function getBaseWebpackConfig(
874867
...getReactProfilingInProduction(),
875868

876869
[RSC_MOD_REF_PROXY_ALIAS]:
877-
'next/dist/build/webpack/loaders/next-flight-client-loader/module-proxy',
870+
'next/dist/build/webpack/loaders/next-flight-loader/module-proxy',
878871

879872
...(isClient || isEdgeServer
880873
? {
@@ -1151,11 +1144,6 @@ export default async function getBaseWebpackConfig(
11511144
},
11521145
}
11531146

1154-
const serverComponentCodeCondition = {
1155-
test: serverComponentsRegex,
1156-
include: [dir, /next[\\/]dist[\\/]pages/],
1157-
}
1158-
11591147
const rscSharedRegex =
11601148
/(node_modules\/react\/|\/shared\/lib\/(head-manager-context|router-context|flush-effects)\.js|node_modules\/styled-jsx\/)/
11611149

@@ -1451,8 +1439,7 @@ export default async function getBaseWebpackConfig(
14511439
'next-image-loader',
14521440
'next-serverless-loader',
14531441
'next-style-loader',
1454-
'next-flight-client-loader',
1455-
'next-flight-server-loader',
1442+
'next-flight-loader',
14561443
'next-flight-client-entry-loader',
14571444
'noop-loader',
14581445
'next-middleware-loader',
@@ -1492,24 +1479,17 @@ export default async function getBaseWebpackConfig(
14921479
? [
14931480
// RSC server compilation loaders
14941481
{
1495-
...serverComponentCodeCondition,
1482+
test: codeCondition.test,
1483+
include: [
1484+
dir,
1485+
// To let the internal client components passing through flight loader
1486+
/next[\\/]dist[\\/]client[\\/]/,
1487+
],
14961488
issuerLayer: WEBPACK_LAYERS.server,
14971489
use: {
1498-
loader: 'next-flight-server-loader',
1490+
loader: 'next-flight-loader',
14991491
},
15001492
},
1501-
// {
1502-
// test: clientComponentRegex,
1503-
// issuerLayer: WEBPACK_LAYERS.server,
1504-
// use: {
1505-
// loader: 'next-flight-client-loader',
1506-
// },
1507-
// },
1508-
// _app should be treated as a client component as well as all its dependencies.
1509-
{
1510-
test: new RegExp(`_app\\.(${rawPageExtensions.join('|')})$`),
1511-
layer: WEBPACK_LAYERS.client,
1512-
},
15131493
]
15141494
: []
15151495
: []),
@@ -1841,11 +1821,10 @@ export default async function getBaseWebpackConfig(
18411821
isClient &&
18421822
new AppBuildManifestPlugin({ dev }),
18431823
hasServerComponents &&
1824+
!!config.experimental.appDir &&
18441825
(isClient
18451826
? new FlightManifestPlugin({
18461827
dev,
1847-
appDir: !!config.experimental.appDir,
1848-
pageExtensions: rawPageExtensions,
18491828
})
18501829
: new FlightClientEntryPlugin({
18511830
dev,
@@ -1995,7 +1974,7 @@ export default async function getBaseWebpackConfig(
19951974

19961975
const configVars = JSON.stringify({
19971976
crossOrigin: config.crossOrigin,
1998-
pageExtensions: rawPageExtensions,
1977+
pageExtensions: pageExtensions,
19991978
trailingSlash: config.trailingSlash,
20001979
buildActivity: config.devIndicators.buildActivity,
20011980
buildActivityPosition: config.devIndicators.buildActivityPosition,

packages/next/build/webpack/config/blocks/css/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ export const css = curry(async function css(
501501
// If it's inside the app dir, but not importing from a layout file,
502502
// throw an error.
503503
and: [ctx.rootDirectory],
504-
not: [/layout(\.client|\.server)?\.(js|mjs|jsx|ts|tsx)$/],
504+
not: [/layout\.(js|mjs|jsx|ts|tsx)$/],
505505
}
506506
: undefined,
507507
use: {

0 commit comments

Comments
 (0)