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

Fix export from and native modules in server component #36072

Merged
merged 9 commits into from Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
85 changes: 72 additions & 13 deletions 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'

Expand Down Expand Up @@ -70,13 +72,59 @@ async function parseModuleInfo({

const isEsm = type === 'Module'

async function getModuleType(importSource: string) {
const isBuiltinModule = builtinModules.includes(importSource)
const resolvedPath = isBuiltinModule
? importSource
: await resolver(importSource)

const isNodeModuleImport = resolvedPath.includes('/node_modules/')

return [isBuiltinModule, isNodeModuleImport] as const
}

function addClientImport(
importSource: string,
{
isNodeModuleImport,
isBuiltinModule,
}: { isNodeModuleImport: boolean; isBuiltinModule: boolean }
) {
// For now we assume there is no .client.js inside node_modules.
// TODO: properly handle this.
if (isNodeModuleImport) {
return false
}

if (isBuiltinModule) {
return false
}

if (
isServerComponent(importSource) ||
hasFlightLoader(importSource, 'server')
) {
// If it's a server component, we recursively import its dependencies.
imports.push(importSource)
} else if (isClientComponent(importSource)) {
// Client component.
imports.push(importSource)
} else {
// Shared component.
imports.push(createFlightServerRequest(importSource, extensions))
}
return true
}

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/')

const [isBuiltinModule, isNodeModuleImport] = await getModuleType(
importSource
)

// matching node_module package but excluding react cores since react is required to be shared
const isReactImports = [
Expand Down Expand Up @@ -104,9 +152,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)

Expand All @@ -116,18 +165,14 @@ 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
!addClientImport(importSource, {
shuding marked this conversation as resolved.
Show resolved Hide resolved
isNodeModuleImport,
isBuiltinModule,
})
) {
continue
}
imports.push(importSource)
}

lastIndex = node.source.span.end
Expand Down Expand Up @@ -158,6 +203,20 @@ async function parseModuleInfo({
}
}
break
case 'ExportNamedDeclaration':
if (isClientCompilation) {
if (node.source) {
// export { ... } from '...'
const importSource = node.source.value
const [isBuiltinModule, isNodeModuleImport] = await getModuleType(
importSource
)
addClientImport(importSource, {
shuding marked this conversation as resolved.
Show resolved Hide resolved
isNodeModuleImport,
isBuiltinModule,
})
}
}
default:
break
}
Expand Down
Expand Up @@ -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] || {}
Expand Down
@@ -0,0 +1,16 @@
import fs from 'fs'

import Foo from '../components/foo.client'

export default function Page() {
return (
<>
<h1>fs: {typeof fs.readFile}</h1>
<Foo />
</>
)
}

export const config = {
runtime: 'nodejs',
}
@@ -0,0 +1 @@
export { default } from './css-modules.server'
Expand Up @@ -221,6 +221,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'
Expand Down