Skip to content

Commit

Permalink
Client directive (#40415)
Browse files Browse the repository at this point in the history
## 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 <3676859+shuding@users.noreply.github.com>
  • Loading branch information
huozhi and shuding committed Sep 18, 2022
1 parent f0ed328 commit 295f9da
Show file tree
Hide file tree
Showing 181 changed files with 282 additions and 334 deletions.
19 changes: 18 additions & 1 deletion packages/next/build/analysis/get-page-static-info.ts
Expand Up @@ -13,6 +13,7 @@ import { SERVER_RUNTIME } from '../../lib/constants'
import { ServerRuntime } from 'next/types'
import { checkCustomRoutes } from '../../lib/load-custom-routes'
import { matcher } from 'next/dist/compiled/micromatch'
import { RSC_MODULE_TYPES } from '../../shared/lib/constants'

export interface MiddlewareConfig {
matchers: MiddlewareMatcher[]
Expand All @@ -29,9 +30,18 @@ export interface PageStaticInfo {
runtime?: ServerRuntime
ssg?: boolean
ssr?: boolean
rsc?: RSCModuleType
middleware?: Partial<MiddlewareConfig>
}

const CLIENT_MODULE_LABEL = `/* __next_internal_client_entry_do_not_use__ */`
export type RSCModuleType = 'server' | 'client'
export function getRSCModuleType(source: string): RSCModuleType {
return source.includes(CLIENT_MODULE_LABEL)
? RSC_MODULE_TYPES.client
: RSC_MODULE_TYPES.server
}

/**
* Receives a parsed AST from SWC and checks if it belongs to a module that
* requires a runtime to be specified. Those are:
Expand Down Expand Up @@ -252,6 +262,7 @@ export async function getPageStaticInfo(params: {
) {
const swcAST = await parseModule(pageFilePath, fileContent)
const { ssg, ssr } = checkExports(swcAST)
const rsc = getRSCModuleType(fileContent)

// default / failsafe value for config
let config: any = {}
Expand Down Expand Up @@ -303,10 +314,16 @@ export async function getPageStaticInfo(params: {
return {
ssr,
ssg,
rsc,
...(middlewareConfig && { middleware: middlewareConfig }),
...(runtime && { runtime }),
}
}

return { ssr: false, ssg: false, runtime: nextConfig.experimental?.runtime }
return {
ssr: false,
ssg: false,
rsc: RSC_MODULE_TYPES.server,
runtime: nextConfig.experimental?.runtime,
}
}
26 changes: 9 additions & 17 deletions packages/next/build/entries.ts
Expand Up @@ -21,6 +21,7 @@ import {
SERVER_RUNTIME,
WEBPACK_LAYERS,
} from '../lib/constants'
import { RSC_MODULE_TYPES } from '../shared/lib/constants'
import {
CLIENT_STATIC_FILES_RUNTIME_AMP,
CLIENT_STATIC_FILES_RUNTIME_MAIN,
Expand All @@ -37,14 +38,12 @@ import { warn } from './output/log'
import {
isMiddlewareFile,
isMiddlewareFilename,
isServerComponentPage,
NestedMiddlewareError,
MiddlewareInServerlessTargetError,
} from './utils'
import { getPageStaticInfo } from './analysis/get-page-static-info'
import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { serverComponentRegex } from './webpack/loaders/utils'
import { ServerRuntime } from '../types'
import { normalizeAppPath } from '../shared/lib/router/utils/app-paths'
import { encodeMatchers } from './webpack/loaders/next-middleware-loader'
Expand All @@ -66,14 +65,12 @@ export function getPageFromPath(pagePath: string, pageExtensions: string[]) {
}

export function createPagesMapping({
hasServerComponents,
isDev,
pageExtensions,
pagePaths,
pagesType,
pagesDir,
}: {
hasServerComponents: boolean
isDev: boolean
pageExtensions: string[]
pagePaths: string[]
Expand All @@ -90,13 +87,6 @@ export function createPagesMapping({

const pageKey = getPageFromPath(pagePath, pageExtensions)

// Assume that if there's a Client Component, that there is
// a matching Server Component that will map to the page.
// so we will not process it
if (hasServerComponents && /\.client$/.test(pageKey)) {
return result
}

if (pageKey in result) {
warn(
`Duplicate page detected. ${chalk.cyan(
Expand Down Expand Up @@ -208,10 +198,7 @@ export function getEdgeServerEntry(opts: {
absolutePagePath: opts.absolutePagePath,
buildId: opts.buildId,
dev: opts.isDev,
isServerComponent: isServerComponentPage(
opts.config,
opts.absolutePagePath
),
isServerComponent: opts.isServerComponent,
page: opts.page,
stringifiedConfig: JSON.stringify(opts.config),
pagesType: opts.pagesType,
Expand Down Expand Up @@ -418,8 +405,10 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
nestedMiddleware.push(page)
}

const isServerComponent = serverComponentRegex.test(absolutePagePath)
const isInsideAppDir = appDir && absolutePagePath.startsWith(appDir)
const isInsideAppDir =
!!appDir &&
(absolutePagePath.startsWith(APP_DIR_ALIAS) ||
absolutePagePath.startsWith(appDir))

const staticInfo = await getPageStaticInfo({
nextConfig: config,
Expand All @@ -428,6 +417,9 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
page,
})

const isServerComponent =
isInsideAppDir && staticInfo.rsc !== RSC_MODULE_TYPES.client

if (isMiddlewareFile(page)) {
middlewareMatchers = staticInfo.middleware?.matchers ?? [
{ regexp: '.*' },
Expand Down
29 changes: 11 additions & 18 deletions packages/next/build/index.ts
Expand Up @@ -58,6 +58,7 @@ import {
COMPILER_NAMES,
APP_BUILD_MANIFEST,
FLIGHT_SERVER_CSS_MANIFEST,
RSC_MODULE_TYPES,
} from '../shared/lib/constants'
import { getSortedRoutes, isDynamicRoute } from '../shared/lib/router/utils'
import { __ApiPreviewProps } from '../server/api-utils'
Expand Down Expand Up @@ -96,7 +97,6 @@ import {
printTreeView,
copyTracedFiles,
isReservedPage,
isServerComponentPage,
} from './utils'
import getBaseWebpackConfig from './webpack-config'
import { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
Expand Down Expand Up @@ -489,7 +489,6 @@ export default async function build(
.traceChild('create-pages-mapping')
.traceFn(() =>
createPagesMapping({
hasServerComponents,
isDev: false,
pageExtensions: config.pageExtensions,
pagesType: 'pages',
Expand All @@ -506,7 +505,6 @@ export default async function build(
.traceFn(() =>
createPagesMapping({
pagePaths: appPaths!,
hasServerComponents,
isDev: false,
pagesType: 'app',
pageExtensions: config.pageExtensions,
Expand All @@ -518,7 +516,6 @@ export default async function build(
let mappedRootPaths: { [page: string]: string } = {}
if (rootPaths.length > 0) {
mappedRootPaths = createPagesMapping({
hasServerComponents,
isDev: false,
pageExtensions: config.pageExtensions,
pagePaths: rootPaths,
Expand Down Expand Up @@ -1267,21 +1264,17 @@ export default async function build(
)
: appPaths?.find((p) => p.startsWith(actualPage + '/page.'))

const pageRuntime =
const staticInfo =
pagesDir && pageType === 'pages' && pagePath
? (
await getPageStaticInfo({
pageFilePath: join(pagesDir, pagePath),
nextConfig: config,
})
).runtime
: undefined

if (hasServerComponents && pagePath) {
if (isServerComponentPage(config, pagePath)) {
isServerComponent = true
}
}
? await getPageStaticInfo({
pageFilePath: join(pagesDir, pagePath),
nextConfig: config,
})
: {}
const pageRuntime = staticInfo.runtime
isServerComponent =
pageType === 'app' &&
staticInfo.rsc !== RSC_MODULE_TYPES.client

if (
// Only calculate page static information if the page is not an
Expand Down
27 changes: 0 additions & 27 deletions packages/next/build/utils.ts
Expand Up @@ -1327,33 +1327,6 @@ export function detectConflictingPaths(
}
}

/**
* With RSC we automatically add .server and .client to page extensions. This
* function allows to remove them for cases where we just need to strip out
* the actual extension keeping the .server and .client.
*/
export function withoutRSCExtensions(pageExtensions: string[]): string[] {
return pageExtensions.filter(
(ext) => !ext.startsWith('client.') && !ext.startsWith('server.')
)
}

export function isServerComponentPage(
nextConfig: NextConfigComplete,
filePath: string
): boolean {
if (!nextConfig.experimental.serverComponents) {
return false
}

const rawPageExtensions = withoutRSCExtensions(
nextConfig.pageExtensions || []
)
return rawPageExtensions.some((ext) => {
return filePath.endsWith(`.server.${ext}`)
})
}

export async function copyTracedFiles(
dir: string,
distDir: string,
Expand Down
51 changes: 15 additions & 36 deletions packages/next/build/webpack-config.ts
Expand Up @@ -56,7 +56,6 @@ import type {
} from './webpack/plugins/telemetry-plugin'
import type { Span } from '../trace'
import type { MiddlewareMatcher } from './analysis/get-page-static-info'
import { withoutRSCExtensions } from './utils'
import browserslist from 'next/dist/compiled/browserslist'
import loadJsConfig from './load-jsconfig'
import { loadBindings } from './swc'
Expand Down Expand Up @@ -687,13 +686,7 @@ export default async function getBaseWebpackConfig(
babel: getBabelOrSwcLoader(),
}

const rawPageExtensions = hasServerComponents
? withoutRSCExtensions(config.pageExtensions)
: config.pageExtensions

const serverComponentsRegex = new RegExp(
`\\.server\\.(${rawPageExtensions.join('|')})$`
)
const pageExtensions = config.pageExtensions

const babelIncludeRegexes: RegExp[] = [
/next[\\/]dist[\\/]shared[\\/]lib/,
Expand Down Expand Up @@ -801,7 +794,7 @@ export default async function getBaseWebpackConfig(
if (dev) {
customAppAliases[`${PAGES_DIR_ALIAS}/_app`] = [
...(pagesDir
? rawPageExtensions.reduce((prev, ext) => {
? pageExtensions.reduce((prev, ext) => {
prev.push(path.join(pagesDir, `_app.${ext}`))
return prev
}, [] as string[])
Expand All @@ -810,7 +803,7 @@ export default async function getBaseWebpackConfig(
]
customAppAliases[`${PAGES_DIR_ALIAS}/_error`] = [
...(pagesDir
? rawPageExtensions.reduce((prev, ext) => {
? pageExtensions.reduce((prev, ext) => {
prev.push(path.join(pagesDir, `_error.${ext}`))
return prev
}, [] as string[])
Expand All @@ -819,7 +812,7 @@ export default async function getBaseWebpackConfig(
]
customDocumentAliases[`${PAGES_DIR_ALIAS}/_document`] = [
...(pagesDir
? rawPageExtensions.reduce((prev, ext) => {
? pageExtensions.reduce((prev, ext) => {
prev.push(path.join(pagesDir, `_document.${ext}`))
return prev
}, [] as string[])
Expand Down Expand Up @@ -874,7 +867,7 @@ export default async function getBaseWebpackConfig(
...getReactProfilingInProduction(),

[RSC_MOD_REF_PROXY_ALIAS]:
'next/dist/build/webpack/loaders/next-flight-client-loader/module-proxy',
'next/dist/build/webpack/loaders/next-flight-loader/module-proxy',

...(isClient || isEdgeServer
? {
Expand Down Expand Up @@ -1151,11 +1144,6 @@ export default async function getBaseWebpackConfig(
},
}

const serverComponentCodeCondition = {
test: serverComponentsRegex,
include: [dir, /next[\\/]dist[\\/]pages/],
}

const rscSharedRegex =
/(node_modules\/react\/|\/shared\/lib\/(head-manager-context|router-context|flush-effects)\.js|node_modules\/styled-jsx\/)/

Expand Down Expand Up @@ -1451,8 +1439,7 @@ export default async function getBaseWebpackConfig(
'next-image-loader',
'next-serverless-loader',
'next-style-loader',
'next-flight-client-loader',
'next-flight-server-loader',
'next-flight-loader',
'next-flight-client-entry-loader',
'noop-loader',
'next-middleware-loader',
Expand Down Expand Up @@ -1492,24 +1479,17 @@ export default async function getBaseWebpackConfig(
? [
// RSC server compilation loaders
{
...serverComponentCodeCondition,
test: codeCondition.test,
include: [
dir,
// To let the internal client components passing through flight loader
/next[\\/]dist[\\/]client[\\/]/,
],
issuerLayer: WEBPACK_LAYERS.server,
use: {
loader: 'next-flight-server-loader',
loader: 'next-flight-loader',
},
},
// {
// test: clientComponentRegex,
// issuerLayer: WEBPACK_LAYERS.server,
// use: {
// loader: 'next-flight-client-loader',
// },
// },
// _app should be treated as a client component as well as all its dependencies.
{
test: new RegExp(`_app\\.(${rawPageExtensions.join('|')})$`),
layer: WEBPACK_LAYERS.client,
},
]
: []
: []),
Expand Down Expand Up @@ -1841,11 +1821,10 @@ export default async function getBaseWebpackConfig(
isClient &&
new AppBuildManifestPlugin({ dev }),
hasServerComponents &&
!!config.experimental.appDir &&
(isClient
? new FlightManifestPlugin({
dev,
appDir: !!config.experimental.appDir,
pageExtensions: rawPageExtensions,
})
: new FlightClientEntryPlugin({
dev,
Expand Down Expand Up @@ -1995,7 +1974,7 @@ export default async function getBaseWebpackConfig(

const configVars = JSON.stringify({
crossOrigin: config.crossOrigin,
pageExtensions: rawPageExtensions,
pageExtensions: pageExtensions,
trailingSlash: config.trailingSlash,
buildActivity: config.devIndicators.buildActivity,
buildActivityPosition: config.devIndicators.buildActivityPosition,
Expand Down
2 changes: 1 addition & 1 deletion packages/next/build/webpack/config/blocks/css/index.ts
Expand Up @@ -501,7 +501,7 @@ export const css = curry(async function css(
// If it's inside the app dir, but not importing from a layout file,
// throw an error.
and: [ctx.rootDirectory],
not: [/layout(\.client|\.server)?\.(js|mjs|jsx|ts|tsx)$/],
not: [/layout\.(js|mjs|jsx|ts|tsx)$/],
}
: undefined,
use: {
Expand Down

0 comments on commit 295f9da

Please sign in to comment.