Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support shared component with next built-in client components #35975

Merged
merged 8 commits into from Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 10 additions & 12 deletions packages/next/build/webpack-config.ts
Expand Up @@ -980,6 +980,12 @@ export default async function getBaseWebpackConfig(
},
}

const rscCodeCondition = {
test: serverComponentsRegex,
// only apply to the pages as the begin process of rsc loaders
include: [dir, /next[\\/]dist[\\/]pages/],
}

let webpackConfig: webpack.Configuration = {
parallelism: Number(process.env.NEXT_WEBPACK_PARALLELISM) || undefined,
externals: targetWeb
Expand Down Expand Up @@ -1202,32 +1208,24 @@ export default async function getBaseWebpackConfig(
? [
// RSC server compilation loaders
{
...codeCondition,
...rscCodeCondition,
use: {
loader: 'next-flight-server-loader',
options: {
pageExtensions: rawPageExtensions,
extensions: rawPageExtensions,
},
},
},
{
test: codeCondition.test,
resourceQuery: /__sc_client__/,
use: {
loader: 'next-flight-client-loader',
},
},
]
: [
// RSC client compilation loaders
{
...codeCondition,
test: serverComponentsRegex,
...rscCodeCondition,
use: {
loader: 'next-flight-server-loader',
options: {
client: 1,
pageExtensions: rawPageExtensions,
extensions: rawPageExtensions,
},
},
},
Expand Down
Expand Up @@ -87,7 +87,6 @@ async function parseModuleInfo(
} = node
// exports.xxx = xxx
if (
left &&
left.object &&
left.type === 'MemberExpression' &&
left.object.type === 'Identifier' &&
Expand Down
120 changes: 76 additions & 44 deletions packages/next/build/webpack/loaders/next-flight-server-loader.ts
Expand Up @@ -3,14 +3,14 @@ import { buildExports } from './utils'

const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif']

export const createClientComponentFilter = (pageExtensions: string[]) => {
export const createClientComponentFilter = (extensions: string[]) => {
// Special cases for Next.js APIs that are considered as client components:
// - .client.[ext]
// - next built-in client components
// - .[imageExt]
const regex = new RegExp(
'(' +
`\\.client(\\.(${pageExtensions.join('|')}))?|` +
`\\.client(\\.(${extensions.join('|')}))?|` +
`next/(link|image)(\\.js)?|` +
`\\.(${imageExtensions.join('|')})` +
')$'
Expand All @@ -19,26 +19,40 @@ export const createClientComponentFilter = (pageExtensions: string[]) => {
return (importSource: string) => regex.test(importSource)
}

export const createServerComponentFilter = (pageExtensions: string[]) => {
const regex = new RegExp(`\\.server(\\.(${pageExtensions.join('|')}))?$`)
export const createServerComponentFilter = (extensions: string[]) => {
const regex = new RegExp(`\\.server(\\.(${extensions.join('|')}))?$`)
return (importSource: string) => regex.test(importSource)
}

function createFlightServerRequest(request: string, extensions: string[]) {
return `next-flight-server-loader?${JSON.stringify({
extensions,
})}!${request}`
}

function hasFlightLoader(request: string, type: 'client' | 'server') {
return request.includes(`next-flight-${type}-loader`)
}

async function parseModuleInfo({
resourcePath,
source,
extensions,
isClientCompilation,
isServerComponent,
isClientComponent,
resolver,
}: {
resourcePath: string
source: string
isClientCompilation: boolean
extensions: string[]
isServerComponent: (name: string) => boolean
isClientComponent: (name: string) => boolean
resolver: (req: string) => Promise<string>
}): Promise<{
source: string
imports: string
imports: string[]
isEsm: boolean
__N_SSP: boolean
pageRuntime: 'edge' | 'nodejs' | null
Expand All @@ -50,7 +64,7 @@ async function parseModuleInfo({
const { type, body } = ast
let transformedSource = ''
let lastIndex = 0
let imports = ''
let imports = []
let __N_SSP = false
let pageRuntime = null

Expand All @@ -61,6 +75,16 @@ async function parseModuleInfo({
switch (node.type) {
case 'ImportDeclaration':
const importSource = node.source.value
const resolvedPath = await resolver(importSource)
const isNodeModuleImport = resolvedPath.includes('/node_modules/')

// matching node_module package but excluding react cores since react is required to be shared
const isReactImports = [
'react',
'react/jsx-runtime',
'react/jsx-dev-runtime',
].includes(importSource)

if (!isClientCompilation) {
// Server compilation for .server.js.
if (isServerComponent(importSource)) {
Expand All @@ -73,45 +97,37 @@ async function parseModuleInfo({
)

if (isClientComponent(importSource)) {
// A client component. It should be loaded as module reference.
transformedSource += importDeclarations
transformedSource += JSON.stringify(`${importSource}?__sc_client__`)
imports += `require(${JSON.stringify(importSource)})\n`
transformedSource += JSON.stringify(
`next-flight-client-loader!${importSource}`
)
imports.push(importSource)
} else {
// FIXME
// case: 'react'
// Avoid module resolution error like Cannot find `./?__rsc_server__` in react/package.json

// cases: 'react/jsx-runtime', 'react/jsx-dev-runtime'
// This is a special case to avoid the Duplicate React error.
// Since we already include React in the SSR runtime,
// here we can't create a new module with the ?__rsc_server__ query.
if (
['react', 'react/jsx-runtime', 'react/jsx-dev-runtime'].includes(
importSource
)
) {
continue
}

// A shared component. It should be handled as a server
// component.
// A shared component. It should be handled as a server component.
const serverImportSource = isReactImports
? importSource
: createFlightServerRequest(importSource, extensions)
transformedSource += importDeclarations
transformedSource += JSON.stringify(`${importSource}?__sc_server__`)
transformedSource += JSON.stringify(serverImportSource)

// TODO: support handling RSC components from node_modules
if (!isNodeModuleImport) {
imports.push(importSource)
}
}
} else {
// For the client compilation, we skip all modules imports but
// always keep client components in the bundle. All client components
// always keep client/shared components in the bundle. All client components
// have to be imported from either server or client components.
if (
!(
isClientComponent(importSource) || isServerComponent(importSource)
)
isServerComponent(importSource) ||
hasFlightLoader(importSource, 'server') ||
// TODO: support handling RSC components from node_modules
isNodeModuleImport
) {
continue
}

imports += `require(${JSON.stringify(importSource)})\n`
imports.push(importSource)
}

lastIndex = node.source.span.end
Expand Down Expand Up @@ -158,23 +174,33 @@ export default async function transformSource(
this: any,
source: string
): Promise<string> {
const { client: isClientCompilation, pageExtensions } = this.getOptions()
const { resourcePath, resourceQuery } = this
const { client: isClientCompilation, extensions } = this.getOptions()
const { resourcePath, resolve: resolveFn, context } = this

const resolver = (req: string): Promise<string> => {
return new Promise((resolve, reject) => {
resolveFn(context, req, (err: any, result: string) => {
if (err) return reject(err)
resolve(result)
})
})
}

if (typeof source !== 'string') {
throw new Error('Expected source to have been transformed to a string.')
}

const isServerComponent = createServerComponentFilter(pageExtensions)
const isClientComponent = createClientComponentFilter(pageExtensions)
const isServerComponent = createServerComponentFilter(extensions)
const isClientComponent = createClientComponentFilter(extensions)
const hasAppliedFlightServerLoader = this.loaders.some((loader: any) => {
return hasFlightLoader(loader.path, 'server')
})
const isServerExt = isServerComponent(resourcePath)

if (!isClientCompilation) {
// We only apply the loader to server components, or shared components that
// are imported by a server component.
if (
!isServerComponent(resourcePath) &&
resourceQuery !== '?__sc_server__'
) {
if (!isServerExt && !hasAppliedFlightServerLoader) {
return source
}
}
Expand All @@ -188,9 +214,11 @@ export default async function transformSource(
} = await parseModuleInfo({
resourcePath,
source,
extensions,
isClientCompilation,
isServerComponent,
isClientComponent,
resolver,
})

/**
Expand All @@ -208,8 +236,12 @@ export default async function transformSource(
const rscExports: any = {
__next_rsc__: `{
__webpack_require__,
_: () => {\n${imports}\n},
server: ${isServerComponent(resourcePath) ? 'true' : 'false'}
_: () => {
${imports
.map((importSource) => `require('${importSource}');`)
.join('\n')}
},
server: ${isServerExt ? 'true' : 'false'}
}`,
}

Expand Down
@@ -1,11 +1,9 @@
import moment from 'moment'
import nonIsomorphicText from 'non-isomorphic-text'

export default function Page() {
return (
<div>
<div>date:{moment().toString()}</div>
<div>{nonIsomorphicText()}</div>
<div>date:{nonIsomorphicText()}</div>
</div>
)
}
@@ -1,4 +1,4 @@
import Nav from '../components/nav.server'
import Nav from '../components/nav'

const envVar = process.env.ENV_VAR_TEST
const headerKey = 'x-next-test-client'
Expand Down
@@ -1,5 +1,5 @@
import Link from 'next/link'
import Nav from '../../components/nav.server'
import Nav from '../../components/nav'

export default function LinkPage({ router }) {
const { query } = router
Expand Down
@@ -1,5 +1,5 @@
import { Suspense } from 'react'
import Nav from '../components/nav.server'
import Nav from '../components/nav'

let result
let promise
Expand Down
Expand Up @@ -192,7 +192,7 @@ export default function (context, { runtime, env }) {
.readFileSync(join(distServerDir, 'external-imports.js'))
.toString()

expect(bundle).not.toContain('moment')
expect(bundle).not.toContain('non-isomorphic-text')
})
}

Expand Down