f5917ad3ee
We are currently using `!!ComponentMod.__next_rsc__` as the hint for the renderer to tell if the component is a server component, however that export field (`__next_rsc__`) is assigned to client components (`.client.[ext]`) as well. This PR adds a new `__next_rsc_server__` field which is only true when the component is a server component so the renderer can handle client components correctly. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] 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` - [ ] 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` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint`
198 lines
5.8 KiB
TypeScript
198 lines
5.8 KiB
TypeScript
import { parse } from '../../swc'
|
|
import { getRawPageExtensions } from '../../utils'
|
|
import { buildExports } from './utils'
|
|
|
|
const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif']
|
|
|
|
const createClientComponentFilter = (pageExtensions: string[]) => {
|
|
// Special cases for Next.js APIs that are considered as client components:
|
|
// - .client.[ext]
|
|
// - next/link, next/image
|
|
// - .[imageExt]
|
|
const regex = new RegExp(
|
|
'(' +
|
|
`\\.client(\\.(${pageExtensions.join('|')}))?|` +
|
|
`next/link|next/image|` +
|
|
`\\.(${imageExtensions.join('|')})` +
|
|
')$'
|
|
)
|
|
|
|
return (importSource: string) => regex.test(importSource)
|
|
}
|
|
|
|
const createServerComponentFilter = (pageExtensions: string[]) => {
|
|
const regex = new RegExp(`\\.server(\\.(${pageExtensions.join('|')}))?$`)
|
|
return (importSource: string) => regex.test(importSource)
|
|
}
|
|
|
|
async function parseModuleInfo({
|
|
resourcePath,
|
|
source,
|
|
isClientCompilation,
|
|
isServerComponent,
|
|
isClientComponent,
|
|
}: {
|
|
resourcePath: string
|
|
source: string
|
|
isClientCompilation: boolean
|
|
isServerComponent: (name: string) => boolean
|
|
isClientComponent: (name: string) => boolean
|
|
}): Promise<{
|
|
source: string
|
|
imports: string
|
|
isEsm: boolean
|
|
}> {
|
|
const ast = await parse(source, {
|
|
filename: resourcePath,
|
|
isModule: 'unknown',
|
|
})
|
|
const { type, body } = ast
|
|
let transformedSource = ''
|
|
let lastIndex = 0
|
|
let imports = ''
|
|
const isEsm = type === 'Module'
|
|
|
|
for (let i = 0; i < body.length; i++) {
|
|
const node = body[i]
|
|
switch (node.type) {
|
|
case 'ImportDeclaration': {
|
|
const importSource = node.source.value
|
|
if (!isClientCompilation) {
|
|
// Server compilation for .server.js.
|
|
if (isServerComponent(importSource)) {
|
|
continue
|
|
}
|
|
|
|
const importDeclarations = source.substring(
|
|
lastIndex,
|
|
node.source.span.start
|
|
)
|
|
|
|
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`
|
|
} 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.
|
|
transformedSource += importDeclarations
|
|
transformedSource += JSON.stringify(`${importSource}?__sc_server__`)
|
|
}
|
|
} else {
|
|
// For the client compilation, we skip all modules imports but
|
|
// always keep client components in the bundle. All client components
|
|
// have to be imported from either server or client components.
|
|
if (
|
|
!(
|
|
isClientComponent(importSource) || isServerComponent(importSource)
|
|
)
|
|
) {
|
|
continue
|
|
}
|
|
|
|
imports += `require(${JSON.stringify(importSource)})\n`
|
|
}
|
|
|
|
lastIndex = node.source.span.end
|
|
break
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if (!isClientCompilation) {
|
|
transformedSource += source.substring(lastIndex)
|
|
}
|
|
|
|
return { source: transformedSource, imports, isEsm }
|
|
}
|
|
|
|
export default async function transformSource(
|
|
this: any,
|
|
source: string
|
|
): Promise<string> {
|
|
const { client: isClientCompilation, pageExtensions } = this.getOptions()
|
|
const { resourcePath, resourceQuery } = this
|
|
|
|
if (typeof source !== 'string') {
|
|
throw new Error('Expected source to have been transformed to a string.')
|
|
}
|
|
|
|
// We currently assume that all components are shared components (unsuffixed)
|
|
// from node_modules.
|
|
if (resourcePath.includes('/node_modules/')) {
|
|
return source
|
|
}
|
|
|
|
const rawRawPageExtensions = getRawPageExtensions(pageExtensions)
|
|
const isServerComponent = createServerComponentFilter(rawRawPageExtensions)
|
|
const isClientComponent = createClientComponentFilter(rawRawPageExtensions)
|
|
|
|
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__'
|
|
) {
|
|
return source
|
|
}
|
|
}
|
|
|
|
const {
|
|
source: transformedSource,
|
|
imports,
|
|
isEsm,
|
|
} = await parseModuleInfo({
|
|
resourcePath,
|
|
source,
|
|
isClientCompilation,
|
|
isServerComponent,
|
|
isClientComponent,
|
|
})
|
|
|
|
/**
|
|
* For .server.js files, we handle this loader differently.
|
|
*
|
|
* Server compilation output:
|
|
* (The content of the Server Component module will be kept.)
|
|
* export const __next_rsc__ = { __webpack_require__, _: () => { ... } }
|
|
*
|
|
* Client compilation output:
|
|
* (The content of the Server Component module will be removed.)
|
|
* export const __next_rsc__ = { __webpack_require__, _: () => { ... } }
|
|
*/
|
|
|
|
const rscExports: any = {
|
|
__next_rsc__: `{
|
|
__webpack_require__,
|
|
_: () => {\n${imports}\n}
|
|
}`,
|
|
__next_rsc_server__: isServerComponent(resourcePath) ? 'true' : 'false',
|
|
}
|
|
|
|
if (isClientCompilation) {
|
|
rscExports['default'] = 'function RSC() {}'
|
|
}
|
|
|
|
const output = transformedSource + '\n' + buildExports(rscExports, isEsm)
|
|
return output
|
|
}
|