Skip to content

Commit

Permalink
rsc: skip next builtin module when apply loaders (vercel#36202)
Browse files Browse the repository at this point in the history
x-ref: vercel/server-components-notes-demo#17

Previously in vercel#35975 we ignore handling node_modules as a workaround for 3rd party packages, but for next buildin components we should still handle them for processing client components
  • Loading branch information
huozhi authored and SukkaW committed Apr 18, 2022
1 parent 0f238cb commit 18049e8
Show file tree
Hide file tree
Showing 5 changed files with 32 additions and 29 deletions.
4 changes: 0 additions & 4 deletions packages/next/build/webpack-config.ts
Expand Up @@ -1211,9 +1211,6 @@ export default async function getBaseWebpackConfig(
...rscCodeCondition,
use: {
loader: 'next-flight-server-loader',
options: {
extensions: rawPageExtensions,
},
},
},
]
Expand All @@ -1225,7 +1222,6 @@ export default async function getBaseWebpackConfig(
loader: 'next-flight-server-loader',
options: {
client: 1,
extensions: rawPageExtensions,
},
},
},
Expand Down
29 changes: 14 additions & 15 deletions packages/next/build/webpack/loaders/next-flight-server-loader.ts
Expand Up @@ -5,9 +5,13 @@ import {
buildExports,
createClientComponentFilter,
createServerComponentFilter,
isNextBuiltinClientComponent,
} from './utils'

function createFlightServerRequest(request: string, options: object) {
function createFlightServerRequest(
request: string,
options?: { client: 1 | undefined }
) {
return `next-flight-server-loader?${JSON.stringify(options)}!${request}`
}

Expand All @@ -18,7 +22,6 @@ function hasFlightLoader(request: string, type: 'client' | 'server') {
async function parseModuleInfo({
resourcePath,
source,
extensions,
isClientCompilation,
isServerComponent,
isClientComponent,
Expand All @@ -27,7 +30,6 @@ async function parseModuleInfo({
resourcePath: string
source: string
isClientCompilation: boolean
extensions: string[]
isServerComponent: (name: string) => boolean
isClientComponent: (name: string) => boolean
resolver: (req: string) => Promise<string>
Expand Down Expand Up @@ -57,7 +59,10 @@ async function parseModuleInfo({
const isBuiltinModule_ = builtinModules.includes(path)
const resolvedPath = isBuiltinModule_ ? path : await resolver(path)

const isNodeModuleImport_ = resolvedPath.includes('/node_modules/')
const isNodeModuleImport_ =
/[\\/]node_modules[\\/]/.test(resolvedPath) &&
// exclude next built-in modules
!isNextBuiltinClientComponent(resolvedPath)

return [isBuiltinModule_, isNodeModuleImport_] as const
}
Expand All @@ -71,12 +76,7 @@ async function parseModuleInfo({
imports.push(path)
} else {
// Shared component.
imports.push(
createFlightServerRequest(path, {
extensions,
client: 1,
})
)
imports.push(createFlightServerRequest(path, { client: 1 }))
}
}

Expand Down Expand Up @@ -119,7 +119,7 @@ async function parseModuleInfo({
const serverImportSource =
isReactImports || isBuiltinModule
? importSource
: createFlightServerRequest(importSource, { extensions })
: createFlightServerRequest(importSource)
transformedSource += importDeclarations
transformedSource += JSON.stringify(serverImportSource)

Expand Down Expand Up @@ -191,7 +191,7 @@ export default async function transformSource(
this: any,
source: string
): Promise<string> {
const { client: isClientCompilation, extensions } = this.getOptions()
const { client: isClientCompilation } = this.getOptions()
const { resourcePath, resolve: resolveFn, context } = this

const resolver = (req: string): Promise<string> => {
Expand All @@ -207,8 +207,8 @@ export default async function transformSource(
throw new Error('Expected source to have been transformed to a string.')
}

const isServerComponent = createServerComponentFilter(extensions)
const isClientComponent = createClientComponentFilter(extensions)
const isServerComponent = createServerComponentFilter()
const isClientComponent = createClientComponentFilter()
const hasAppliedFlightServerLoader = this.loaders.some((loader: any) => {
return hasFlightLoader(loader.path, 'server')
})
Expand All @@ -231,7 +231,6 @@ export default async function transformSource(
} = await parseModuleInfo({
resourcePath,
source,
extensions,
isClientCompilation,
isServerComponent,
isClientComponent,
Expand Down
14 changes: 7 additions & 7 deletions packages/next/build/webpack/loaders/utils.ts
@@ -1,4 +1,4 @@
const defaultJsFileExtensions = ['js', 'mjs', 'jsx', 'ts', 'tsx', 'json']
export const defaultJsFileExtensions = ['js', 'mjs', 'jsx', 'ts', 'tsx']
const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif']
const nextClientComponents = ['link', 'image', 'head', 'script']

Expand All @@ -24,16 +24,14 @@ export function buildExports(moduleExports: any, isESM: boolean) {
return ret
}

export const createClientComponentFilter = (
extensions: string[] = defaultJsFileExtensions
) => {
export const createClientComponentFilter = () => {
// 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(\\.(${extensions.join('|')}))?|` +
`\\.client(\\.(${defaultJsFileExtensions.join('|')}))?|` +
`next/(${nextClientComponents.join('|')})(\\.js)?|` +
`\\.(${imageExtensions.join('|')})` +
')$'
Expand All @@ -42,7 +40,9 @@ export const createClientComponentFilter = (
return (importSource: string) => regex.test(importSource)
}

export const createServerComponentFilter = (extensions: string[]) => {
const regex = new RegExp(`\\.server(\\.(${extensions.join('|')}))?$`)
export const createServerComponentFilter = () => {
const regex = new RegExp(
`\\.server(\\.(${defaultJsFileExtensions.join('|')}))?$`
)
return (importSource: string) => regex.test(importSource)
}
Expand Up @@ -23,6 +23,7 @@ type Options = {

const PLUGIN_NAME = 'FlightManifestPlugin'

const isClientComponent = createClientComponentFilter()
export class FlightManifestPlugin {
dev: boolean = false
pageExtensions: string[]
Expand Down Expand Up @@ -64,7 +65,6 @@ export class FlightManifestPlugin {

createAsset(assets: any, compilation: any) {
const manifest: any = {}
const isClientComponent = createClientComponentFilter(this.pageExtensions)
compilation.chunkGroups.forEach((chunkGroup: any) => {
function recordModule(id: string, _chunk: any, mod: any) {
const resource = mod.resource
Expand Down
12 changes: 10 additions & 2 deletions test/production/react-18-streaming-ssr/index.test.ts
Expand Up @@ -11,7 +11,7 @@ describe('react 18 streaming SSR in minimal mode', () => {
next = await createNext({
files: {
'pages/index.server.js': `
export default function Page() {
export default function Page() {
return <p>static streaming</p>
}
`,
Expand Down Expand Up @@ -65,8 +65,15 @@ describe('react 18 streaming SSR with custom next configs', () => {
}
`,
'pages/hello.js': `
import Link from 'next/link'
export default function Page() {
return <p>hello nextjs</p>
return (
<div>
<p>hello nextjs</p>
<Link href='/'><a>home></a></Link>
</div>
)
}
`,
'pages/multi-byte.js': `
Expand Down Expand Up @@ -114,6 +121,7 @@ describe('react 18 streaming SSR with custom next configs', () => {
expect(redirectRes.status).toBe(308)
expect(res.status).toBe(200)
expect(html).toContain('hello nextjs')
expect(html).toContain('home')
})

it('should render multi-byte characters correctly in streaming', async () => {
Expand Down

0 comments on commit 18049e8

Please sign in to comment.