Skip to content

Commit

Permalink
Use registerClientReference for ESM client component modules (verce…
Browse files Browse the repository at this point in the history
  • Loading branch information
unstubbable authored and stipsan committed Nov 6, 2024
1 parent 498ce6d commit 05303a0
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 162 deletions.
14 changes: 3 additions & 11 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ import {
getBabelLoader,
getReactCompilerLoader,
} from './get-babel-loader-config'
import type { NextFlightLoaderOptions } from './webpack/loaders/next-flight-loader'
import {
NEXT_PROJECT_ROOT,
NEXT_PROJECT_ROOT_DIST_CLIENT,
Expand Down Expand Up @@ -519,13 +518,6 @@ export default async function getBaseWebpackConfig(
babel: useSWCLoader ? swcDefaultLoader : babelLoader!,
}

const nextFlightLoader = {
loader: 'next-flight-loader',
options: {
isEdgeServer,
} satisfies NextFlightLoaderOptions,
}

const appServerLayerLoaders = hasAppDir
? [
// When using Babel, we will have to add the SWC loader
Expand All @@ -539,7 +531,7 @@ export default async function getBaseWebpackConfig(
: []

const instrumentLayerLoaders = [
nextFlightLoader,
'next-flight-loader',
// When using Babel, we will have to add the SWC loader
// as an additional pass to handle RSC correctly.
// This will cause some performance overhead but
Expand All @@ -549,7 +541,7 @@ export default async function getBaseWebpackConfig(
].filter(Boolean)

const middlewareLayerLoaders = [
nextFlightLoader,
'next-flight-loader',
// When using Babel, we will have to use SWC to do the optimization
// for middleware to tree shake the unused default optimized imports like "next/server".
// This will cause some performance overhead but
Expand Down Expand Up @@ -1366,7 +1358,7 @@ export default async function getBaseWebpackConfig(
isEdgeServer,
}),
},
use: nextFlightLoader,
use: 'next-flight-loader',
},
]
: []),
Expand Down
109 changes: 48 additions & 61 deletions packages/next/src/build/webpack/loaders/next-flight-loader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import type { webpack } from 'next/dist/compiled/webpack/webpack'
import { RSC_MOD_REF_PROXY_ALIAS } from '../../../../lib/constants'
import {
BARREL_OPTIMIZATION_PREFIX,
DEFAULT_RUNTIME_WEBPACK,
EDGE_RUNTIME_WEBPACK,
RSC_MODULE_TYPES,
} from '../../../../shared/lib/constants'
import { warnOnce } from '../../../../shared/lib/utils/warn-once'
Expand All @@ -14,11 +12,6 @@ import type {
javascript,
LoaderContext,
} from 'next/dist/compiled/webpack/webpack'
import picomatch from 'next/dist/compiled/picomatch'

export interface NextFlightLoaderOptions {
isEdgeServer: boolean
}

type SourceType = javascript.JavascriptParser['sourceType'] | 'commonjs'

Expand All @@ -27,10 +20,6 @@ const noopHeadPath = require.resolve('next/dist/client/components/noop-head')
const MODULE_PROXY_PATH =
'next/dist/build/webpack/loaders/next-flight-loader/module-proxy'

const isSharedRuntime = picomatch('**/next/dist/**/*.shared-runtime.js', {
dot: true, // required for .pnpm paths
})

export function getAssumedSourceType(
mod: webpack.Module,
sourceType: SourceType
Expand All @@ -42,26 +31,31 @@ export function getAssumedSourceType(
// It's tricky to detect the type of a client boundary, but we should always
// use the `module` type when we can, to support `export *` and `export from`
// syntax in other modules that import this client boundary.
let assumedSourceType = sourceType
if (assumedSourceType === 'auto' && detectedClientEntryType === 'auto') {
if (
clientRefs.length === 0 ||
(clientRefs.length === 1 && clientRefs[0] === '')
) {
// If there's zero export detected in the client boundary, and it's the
// `auto` type, we can safely assume it's a CJS module because it doesn't
// have ESM exports.
assumedSourceType = 'commonjs'
} else if (!clientRefs.includes('*')) {
// Otherwise, we assume it's an ESM module.
assumedSourceType = 'module'

if (sourceType === 'auto') {
if (detectedClientEntryType === 'auto') {
if (
clientRefs.length === 0 ||
(clientRefs.length === 1 && clientRefs[0] === '')
) {
// If there's zero export detected in the client boundary, and it's the
// `auto` type, we can safely assume it's a CJS module because it doesn't
// have ESM exports.
return 'commonjs'
} else if (!clientRefs.includes('*')) {
// Otherwise, we assume it's an ESM module.
return 'module'
}
} else if (detectedClientEntryType === 'cjs') {
return 'commonjs'
}
}
return assumedSourceType

return sourceType
}

export default function transformSource(
this: LoaderContext<NextFlightLoaderOptions>,
this: LoaderContext<undefined>,
source: string,
sourceMap: any
) {
Expand All @@ -70,8 +64,6 @@ export default function transformSource(
throw new Error('Expected source to have been transformed to a string.')
}

const options = this.getOptions()
const { isEdgeServer } = options
const module = this._module!

// Assign the RSC meta information to buildInfo.
Expand Down Expand Up @@ -107,6 +99,7 @@ export default function transformSource(
)

const clientRefs = buildInfo.rsc.clientRefs!
const stringifiedResourceKey = JSON.stringify(resourceKey)

if (assumedSourceType === 'module') {
if (clientRefs.includes('*')) {
Expand All @@ -118,46 +111,40 @@ export default function transformSource(
return
}

if (!isSharedRuntime(resourceKey)) {
// Prevent module concatenation, and prevent export names from being
// mangled, in production builds, so that exports of client reference
// modules can be resolved by React using the metadata from the client
// manifest.
this._compilation!.moduleGraph.getExportsInfo(
module
).setUsedInUnknownWay(
isEdgeServer ? EDGE_RUNTIME_WEBPACK : DEFAULT_RUNTIME_WEBPACK
)
}

// `proxy` is the module proxy that we treat the module as a client boundary.
// For ESM, we access the property of the module proxy directly for each export.
// This is bit hacky that treating using a CJS like module proxy for ESM's exports,
// but this will avoid creating nested proxies for each export. It will be improved in the future.

// Explanation for: await createProxy(...)
// We need to await the module proxy creation because it can be async module for SSR layer
// due to having async dependencies.
// We only apply `the await` for Node.js as only Edge doesn't have external dependencies.
let esmSource = `\
import { createProxy } from "${MODULE_PROXY_PATH}"
const proxy = ${isEdgeServer ? '' : 'await'} createProxy(String.raw\`${resourceKey}\`)
import { registerClientReference } from "react-server-dom-webpack/server.edge";
`
let cnt = 0
for (const ref of clientRefs) {
if (ref === '') {
esmSource += `exports[''] = proxy['']\n`
} else if (ref === 'default') {
esmSource += `export default proxy.default;\n`
if (ref === 'default') {
esmSource += `export default registerClientReference(
function() { throw new Error(${JSON.stringify(`Attempted to call the default \
export of ${stringifiedResourceKey} from the server, but it's on the client. \
It's not possible to invoke a client function from the server, it can only be \
rendered as a Component or passed to props of a Client Component.`)}); },
${stringifiedResourceKey},
"default",
);\n`
} else {
esmSource += `const e${cnt} = proxy["${ref}"];\n`
esmSource += `export { e${cnt++} as ${ref} };\n`
esmSource += `export const ${ref} = registerClientReference(
function() { throw new Error(${JSON.stringify(`Attempted to call ${ref}() from \
the server but ${ref} is on the client. It's not possible to invoke a client \
function from the server, it can only be rendered as a Component or passed to \
props of a Client Component.`)}); },
${stringifiedResourceKey},
${JSON.stringify(ref)},
);`
}
}

this.callback(null, esmSource, sourceMap)
return
return this.callback(null, esmSource, sourceMap)
} else if (assumedSourceType === 'commonjs') {
let cjsSource = `\
const { createProxy } = require("${MODULE_PROXY_PATH}")
module.exports = createProxy(${stringifiedResourceKey})
`

return this.callback(null, cjsSource, sourceMap)
}
}

Expand Down
Loading

0 comments on commit 05303a0

Please sign in to comment.