diff --git a/packages/next/build/webpack/loaders/next-flight-server-loader.ts b/packages/next/build/webpack/loaders/next-flight-server-loader.ts index 0419079a76f7b64..8e37149a3f7ed1d 100644 --- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts @@ -1,3 +1,5 @@ +import { builtinModules } from 'module' + import { parse } from '../../swc' import { buildExports } from './utils' @@ -24,10 +26,8 @@ export const createServerComponentFilter = (extensions: string[]) => { return (importSource: string) => regex.test(importSource) } -function createFlightServerRequest(request: string, extensions: string[]) { - return `next-flight-server-loader?${JSON.stringify({ - extensions, - })}!${request}` +function createFlightServerRequest(request: string, options: object) { + return `next-flight-server-loader?${JSON.stringify(options)}!${request}` } function hasFlightLoader(request: string, type: 'client' | 'server') { @@ -67,16 +67,47 @@ async function parseModuleInfo({ let imports = [] let __N_SSP = false let pageRuntime = null + let isBuiltinModule + let isNodeModuleImport const isEsm = type === 'Module' + async function getModuleType(path: string) { + const isBuiltinModule_ = builtinModules.includes(path) + const resolvedPath = isBuiltinModule_ ? path : await resolver(path) + + const isNodeModuleImport_ = resolvedPath.includes('/node_modules/') + + return [isBuiltinModule_, isNodeModuleImport_] as const + } + + function addClientImport(path: string) { + if (isServerComponent(path) || hasFlightLoader(path, 'server')) { + // If it's a server component, we recursively import its dependencies. + imports.push(path) + } else if (isClientComponent(path)) { + // Client component. + imports.push(path) + } else { + // Shared component. + imports.push( + createFlightServerRequest(path, { + extensions, + client: 1, + }) + ) + } + } + for (let i = 0; i < body.length; i++) { const node = body[i] switch (node.type) { case 'ImportDeclaration': const importSource = node.source.value - const resolvedPath = await resolver(importSource) - const isNodeModuleImport = resolvedPath.includes('/node_modules/') + + ;[isBuiltinModule, isNodeModuleImport] = await getModuleType( + importSource + ) // matching node_module package but excluding react cores since react is required to be shared const isReactImports = [ @@ -104,9 +135,10 @@ async function parseModuleInfo({ imports.push(importSource) } else { // A shared component. It should be handled as a server component. - const serverImportSource = isReactImports - ? importSource - : createFlightServerRequest(importSource, extensions) + const serverImportSource = + isReactImports || isBuiltinModule + ? importSource + : createFlightServerRequest(importSource, { extensions }) transformedSource += importDeclarations transformedSource += JSON.stringify(serverImportSource) @@ -116,18 +148,10 @@ async function parseModuleInfo({ } } } else { - // For the client compilation, we skip all modules imports but - // always keep client/shared components in the bundle. All client components - // have to be imported from either server or client components. - if ( - isServerComponent(importSource) || - hasFlightLoader(importSource, 'server') || - // TODO: support handling RSC components from node_modules - isNodeModuleImport - ) { - continue - } - imports.push(importSource) + // For now we assume there is no .client.js inside node_modules. + // TODO: properly handle this. + if (isNodeModuleImport || isBuiltinModule) continue + addClientImport(importSource) } lastIndex = node.source.span.end @@ -158,6 +182,18 @@ async function parseModuleInfo({ } } break + case 'ExportNamedDeclaration': + if (isClientCompilation) { + if (node.source) { + // export { ... } from '...' + const path = node.source.value + ;[isBuiltinModule, isNodeModuleImport] = await getModuleType(path) + if (!isBuiltinModule && !isNodeModuleImport) { + addClientImport(path) + } + } + } + break default: break } diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index dac02a1bb60c456..59156b0c75797b4 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -72,7 +72,7 @@ export class FlightManifestPlugin { // TODO: Hook into deps instead of the target module. // That way we know by the type of dep whether to include. // It also resolves conflicts when the same module is in multiple chunks. - if (!isClientComponent(resource)) { + if (!resource || !isClientComponent(resource)) { return } const moduleExports: any = manifest[resource] || {} diff --git a/test/integration/react-streaming-and-server-components/app/pages/native-module.server.js b/test/integration/react-streaming-and-server-components/app/pages/native-module.server.js new file mode 100644 index 000000000000000..c109332b28e348d --- /dev/null +++ b/test/integration/react-streaming-and-server-components/app/pages/native-module.server.js @@ -0,0 +1,16 @@ +import fs from 'fs' + +import Foo from '../components/foo.client' + +export default function Page() { + return ( + <> +

fs: {typeof fs.readFile}

+ + + ) +} + +export const config = { + runtime: 'nodejs', +} diff --git a/test/integration/react-streaming-and-server-components/app/pages/re-export.server.js b/test/integration/react-streaming-and-server-components/app/pages/re-export.server.js new file mode 100644 index 000000000000000..de73532c227f0b5 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/app/pages/re-export.server.js @@ -0,0 +1 @@ +export { default } from './css-modules.server' diff --git a/test/integration/react-streaming-and-server-components/test/rsc.js b/test/integration/react-streaming-and-server-components/test/rsc.js index 105aa8618377cc9..4fc01e489536f47 100644 --- a/test/integration/react-streaming-and-server-components/test/rsc.js +++ b/test/integration/react-streaming-and-server-components/test/rsc.js @@ -220,6 +220,21 @@ export default function (context, { runtime, env }) { expect(hydratedContent).toContain('Export All: one, two, two') }) + it('should support native modules in server component', async () => { + const html = await renderViaHTTP(context.appPort, '/native-module') + const content = getNodeBySelector(html, '#__next').text() + + expect(content).toContain('fs: function') + expect(content).toContain('foo.client') + }) + + it('should support the re-export syntax in server component', async () => { + const html = await renderViaHTTP(context.appPort, '/re-export') + const content = getNodeBySelector(html, '#__next').text() + + expect(content).toContain('This should be in red') + }) + it('should handle 404 requests and missing routes correctly', async () => { const id = '#text' const content = 'custom-404-page'