Skip to content

Commit

Permalink
Add RSC module ID mapping to the Client Manifest (#70524)
Browse files Browse the repository at this point in the history
Co-authored-by: Sebastian Markbage <sebastian@calyptus.eu>
  • Loading branch information
shuding and sebmarkbage authored Oct 3, 2024
1 parent a4763cc commit 08fce2e
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 40 deletions.
3 changes: 2 additions & 1 deletion packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import {
getBabelLoader,
getReactCompilerLoader,
} from './get-babel-loader-config'
import type { NextFlightLoaderOptions } from './webpack/loaders/next-flight-loader'

type ExcludesFalse = <T>(x: T | false) => x is T
type ClientEntries = {
Expand Down Expand Up @@ -525,7 +526,7 @@ export default async function getBaseWebpackConfig(
loader: 'next-flight-loader',
options: {
isEdgeServer,
},
} satisfies NextFlightLoaderOptions,
}

const appServerLayerLoaders = hasAppDir
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,35 @@ 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'
import { getRSCModuleInformation } from '../../../analysis/get-page-static-info'
import { formatBarrelOptimizedResource } from '../../utils'
import { getModuleBuildInfo } from '../get-module-build-info'
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'

const noopHeadPath = require.resolve('next/dist/client/components/noop-head')
// For edge runtime it will be aliased to esm version by webpack
const MODULE_PROXY_PATH =
'next/dist/build/webpack/loaders/next-flight-loader/module-proxy'

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

export function getAssumedSourceType(
mod: webpack.Module,
sourceType: SourceType
Expand Down Expand Up @@ -45,7 +61,7 @@ export function getAssumedSourceType(
}

export default function transformSource(
this: any,
this: LoaderContext<NextFlightLoaderOptions>,
source: string,
sourceMap: any
) {
Expand All @@ -56,10 +72,11 @@ export default function transformSource(

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

// Assign the RSC meta information to buildInfo.
// Exclude next internal files which are not marked as client files
const buildInfo = getModuleBuildInfo(this._module)
const buildInfo = getModuleBuildInfo(module)
buildInfo.rsc = getRSCModuleInformation(source, true)

// Resource key is the unique identifier for the resource. When RSC renders
Expand All @@ -75,19 +92,20 @@ export default function transformSource(
// Because of that, we must add another query param to the resource key to
// differentiate them.
let resourceKey: string = this.resourcePath
if (this._module?.matchResource?.startsWith(BARREL_OPTIMIZATION_PREFIX)) {
if (module.matchResource?.startsWith(BARREL_OPTIMIZATION_PREFIX)) {
resourceKey = formatBarrelOptimizedResource(
resourceKey,
this._module.matchResource
module.matchResource
)
}

// A client boundary.
if (buildInfo.rsc?.type === RSC_MODULE_TYPES.client) {
const assumedSourceType = getAssumedSourceType(
this._module,
this._module?.parser?.sourceType
module,
(module.parser as javascript.JavascriptParser).sourceType
)

const clientRefs = buildInfo.rsc.clientRefs!

if (assumedSourceType === 'module') {
Expand All @@ -100,6 +118,18 @@ 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ const pluginState = getProxiedPluginState({
serverModuleIds: {} as Record<string, string | number>,
edgeServerModuleIds: {} as Record<string, string | number>,

rscModuleIds: {} as Record<string, string | number>,
edgeRscModuleIds: {} as Record<string, string | number>,

injectedClientEntries: {} as Record<string, string>,
})

Expand Down Expand Up @@ -209,6 +212,20 @@ export class FlightClientEntryPlugin {
: modPath + modQuery
: mod.resource

if (typeof modId !== 'undefined' && modResource) {
if (mod.layer === WEBPACK_LAYERS.reactServerComponents) {
const key = path
.relative(compiler.context, modResource)
.replace(/\/next\/dist\/esm\//, '/next/dist/')

if (this.isEdgeServer) {
pluginState.edgeRscModuleIds[key] = modId
} else {
pluginState.rscModuleIds[key] = modId
}
}
}

if (mod.layer !== WEBPACK_LAYERS.serverSideRendering) {
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export type ManifestChunks = Array<string>
const pluginState = getProxiedPluginState({
serverModuleIds: {} as Record<string, string | number>,
edgeServerModuleIds: {} as Record<string, string | number>,

rscModuleIds: {} as Record<string, string | number>,
edgeRscModuleIds: {} as Record<string, string | number>,
})

export interface ManifestNode {
Expand Down Expand Up @@ -86,6 +89,12 @@ export type ClientReferenceManifest = {
entryJSFiles?: {
[entry: string]: string[]
}
rscModuleMapping: {
[moduleId: string]: ManifestNode
}
edgeRscModuleMapping: {
[moduleId: string]: ManifestNode
}
}

function getAppPathRequiredChunks(
Expand Down Expand Up @@ -173,6 +182,11 @@ function mergeManifest(
manifestToMerge.edgeSSRModuleMapping
)
Object.assign(manifest.entryCSSFiles, manifestToMerge.entryCSSFiles)
Object.assign(manifest.rscModuleMapping, manifestToMerge.rscModuleMapping)
Object.assign(
manifest.edgeRscModuleMapping,
manifestToMerge.edgeRscModuleMapping
)
}

const PLUGIN_NAME = 'ClientReferenceManifestPlugin'
Expand Down Expand Up @@ -268,6 +282,8 @@ export class ClientReferenceManifestPlugin {
edgeSSRModuleMapping: {},
clientModules: {},
entryCSSFiles: {},
rscModuleMapping: {},
edgeRscModuleMapping: {},
}

// Absolute path without the extension
Expand Down Expand Up @@ -295,6 +311,9 @@ export class ClientReferenceManifestPlugin {
const moduleIdMapping = manifest.ssrModuleMapping
const edgeModuleIdMapping = manifest.edgeSSRModuleMapping

const rscIdMapping = manifest.rscModuleMapping
const edgeRscIdMapping = manifest.edgeRscModuleMapping

// Note that this isn't that reliable as webpack is still possible to assign
// additional queries to make sure there's no conflict even using the `named`
// module ID strategy.
Expand All @@ -303,6 +322,11 @@ export class ClientReferenceManifestPlugin {
mod.resourceResolveData?.path || resource
)

const rscNamedModuleId = relative(
context,
mod.resourceResolveData?.path || resource
)

if (!ssrNamedModuleId.startsWith('.'))
ssrNamedModuleId = `./${ssrNamedModuleId.replace(/\\/g, '/')}`

Expand Down Expand Up @@ -345,7 +369,6 @@ export class ClientReferenceManifestPlugin {
function addSSRIdMapping() {
const exportName = resource
if (
// TODO: Add mapping from client module IDs to RSC module IDs
typeof pluginState.serverModuleIds[ssrNamedModuleId] !== 'undefined'
) {
moduleIdMapping[modId] = moduleIdMapping[modId] || {}
Expand Down Expand Up @@ -375,12 +398,47 @@ export class ClientReferenceManifestPlugin {
}
}

function addRSCIdMapping() {
const exportName = resource
if (
typeof pluginState.rscModuleIds[rscNamedModuleId] !== 'undefined'
) {
rscIdMapping[modId] = rscIdMapping[modId] || {}
rscIdMapping[modId]['*'] = {
...manifest.clientModules[exportName],
// During SSR, we don't have external chunks to load on the server
// side with our architecture of Webpack / Turbopack. We can keep
// this field empty to save some bytes.
chunks: [],
id: pluginState.rscModuleIds[rscNamedModuleId],
}
}

if (
typeof pluginState.edgeRscModuleIds[rscNamedModuleId] !==
'undefined'
) {
edgeRscIdMapping[modId] = edgeRscIdMapping[modId] || {}
edgeRscIdMapping[modId]['*'] = {
...manifest.clientModules[exportName],
// During SSR, we don't have external chunks to load on the server
// side with our architecture of Webpack / Turbopack. We can keep
// this field empty to save some bytes.
chunks: [],
id: pluginState.edgeRscModuleIds[rscNamedModuleId],
}
}
}

addClientReference()
addSSRIdMapping()
addRSCIdMapping()

manifest.clientModules = moduleReferences
manifest.ssrModuleMapping = moduleIdMapping
manifest.edgeSSRModuleMapping = edgeModuleIdMapping
manifest.rscModuleMapping = rscIdMapping
manifest.edgeRscModuleMapping = edgeRscIdMapping
}

const checkedChunkGroups = new Set()
Expand Down Expand Up @@ -479,6 +537,8 @@ export class ClientReferenceManifestPlugin {
edgeSSRModuleMapping: {},
clientModules: {},
entryCSSFiles: {},
rscModuleMapping: {},
edgeRscModuleMapping: {},
}

const segments = [...entryNameToGroupName(pageName).split('/'), 'page']
Expand Down
15 changes: 6 additions & 9 deletions packages/next/src/server/app-render/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
stringToUint8Array,
} from './encryption-utils'

import type { ManifestNode } from '../../build/webpack/plugins/flight-manifest-plugin'
const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'

const textEncoder = new TextEncoder()
const textDecoder = new TextDecoder()
Expand Down Expand Up @@ -96,16 +96,11 @@ export async function decryptActionBoundArgs(
actionId: string,
encrypted: Promise<string>
) {
const clientReferenceManifestSingleton = getClientReferenceManifestSingleton()

// Decrypt the serialized string with the action id as the salt.
const decryped = await decodeActionBoundArg(actionId, await encrypted)

// TODO: We can't use the client reference manifest to resolve the modules
// on the server side - instead they need to be recovered as the module
// references (proxies) again.
// For now, we'll just use an empty module map.
const ssrModuleMap: {
[moduleExport: string]: ManifestNode
} = {}
// Using Flight to deserialize the args from the string.
const deserialized = await createFromReadableStream(
new ReadableStream({
Expand All @@ -120,7 +115,9 @@ export async function decryptActionBoundArgs(
// to be added to the current execution. Instead, we'll wait for any ClientReference
// to be emitted which themselves will handle the preloading.
moduleLoading: null,
moduleMap: ssrModuleMap,
moduleMap: isEdgeRuntime
? clientReferenceManifestSingleton.edgeRscModuleMapping
: clientReferenceManifestSingleton.rscModuleMapping,
},
}
)
Expand Down
26 changes: 9 additions & 17 deletions packages/next/src/server/use-cache/use-cache-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
getServerModuleMap,
} from '../app-render/encryption-utils'

import type { ManifestNode } from '../../build/webpack/plugins/flight-manifest-plugin'
const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'

type CacheEntry = {
value: ReadableStream
Expand Down Expand Up @@ -280,6 +280,11 @@ export function cache(kind: string, id: string, fn: any) {
let entry: undefined | CacheEntry =
await cacheHandler.get(serializedCacheKey)

// Get the clientReferenceManifestSingleton while we're still in the outer Context.
// In case getClientReferenceManifestSingleton is implemented using AsyncLocalStorage.
const clientReferenceManifestSingleton =
getClientReferenceManifestSingleton()

let stream
if (
entry === undefined ||
Expand All @@ -297,11 +302,6 @@ export function cache(kind: string, id: string, fn: any) {
// Note: It is important that we await at least once before this because it lets us
// pop out of any stack specific contexts as well - aka "Sync" Local Storage.

// Get the clientReferenceManifestSingleton while we're still in the outer Context.
// In case getClientReferenceManifestSingleton is implemented using AsyncLocalStorage.
const clientReferenceManifestSingleton =
getClientReferenceManifestSingleton()

stream = await generateCacheEntry(
workStore,
clientReferenceManifestSingleton,
Expand All @@ -315,8 +315,6 @@ export function cache(kind: string, id: string, fn: any) {
if (entry.stale) {
// If this is stale, and we're not in a prerender (i.e. this is dynamic render),
// then we should warm up the cache with a fresh revalidated entry.
const clientReferenceManifestSingleton =
getClientReferenceManifestSingleton()
const ignoredStream = await generateCacheEntry(
workStore,
clientReferenceManifestSingleton,
Expand All @@ -338,20 +336,14 @@ export function cache(kind: string, id: string, fn: any) {
// the server, which is required to pick it up for replaying again on the client.
const replayConsoleLogs = true

// TODO: We can't use the client reference manifest to resolve the modules
// on the server side - instead they need to be recovered as the module
// references (proxies) again.
// For now, we'll just use an empty module map.
const ssrModuleMap: {
[moduleExport: string]: ManifestNode
} = {}

const ssrManifest = {
// moduleLoading must be null because we don't want to trigger preloads of ClientReferences
// to be added to the consumer. Instead, we'll wait for any ClientReference to be emitted
// which themselves will handle the preloading.
moduleLoading: null,
moduleMap: ssrModuleMap,
moduleMap: isEdgeRuntime
? clientReferenceManifestSingleton.edgeRscModuleMapping
: clientReferenceManifestSingleton.rscModuleMapping,
}
return createFromReadableStream(stream, {
ssrManifest,
Expand Down
1 change: 1 addition & 0 deletions packages/next/types/$$compiled.internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ declare module 'next/dist/compiled/webpack/webpack' {
ModuleFilenameHelpers,
} from 'webpack'
export type {
javascript,
LoaderDefinitionFunction,
LoaderContext,
ModuleGraph,
Expand Down
Loading

0 comments on commit 08fce2e

Please sign in to comment.