Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Built-in flight plugin for additional bundler #76014

Merged
merged 1 commit into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ import { WellKnownErrorsPlugin } from './webpack/plugins/wellknown-errors-plugin
import { regexLikeCss } from './webpack/config/blocks/css'
import { CopyFilePlugin } from './webpack/plugins/copy-file-plugin'
import { ClientReferenceManifestPlugin } from './webpack/plugins/flight-manifest-plugin'
import { FlightClientEntryPlugin } from './webpack/plugins/flight-client-entry-plugin'
import { FlightClientEntryPlugin as NextFlightClientEntryPlugin } from './webpack/plugins/flight-client-entry-plugin'
import { RspackFlightClientEntryPlugin } from './webpack/plugins/rspack-flight-client-entry-plugin'
import { NextTypesPlugin } from './webpack/plugins/next-types-plugin'
import type {
Feature,
Expand Down Expand Up @@ -344,6 +345,11 @@ export default async function getBaseWebpackConfig(

const isRspack = Boolean(process.env.NEXT_RSPACK)

const FlightClientEntryPlugin =
isRspack && process.env.BUILTIN_FLIGHT_CLIENT_ENTRY_PLUGIN
? RspackFlightClientEntryPlugin
: NextFlightClientEntryPlugin

// If the current compilation is aimed at server-side code instead of client-side code.
const isNodeOrEdgeCompilation = isNodeServer || isEdgeServer

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ export default function transformSource(
buildInfo.rsc = {
type: RSC_MODULE_TYPES.client,
}
if (process.env.BUILTIN_FLIGHT_CLIENT_ENTRY_PLUGIN) {
const rscModuleInformationJson = JSON.stringify(buildInfo.rsc)
return (
`/* __rspack_internal_rsc_module_information_do_not_use__ ${rscModuleInformationJson} */\n` +
code
)
}

return code
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ const flightClientModuleLoader: webpack.LoaderDefinitionFunction =
// Assign the RSC meta information to buildInfo.
const buildInfo = getModuleBuildInfo(this._module)
buildInfo.rsc = getRSCModuleInformation(source, false)
let prefix = ''
if (process.env.BUILTIN_FLIGHT_CLIENT_ENTRY_PLUGIN) {
const rscModuleInformationJson = JSON.stringify(buildInfo.rsc)
prefix = `/* __rspack_internal_rsc_module_information_do_not_use__ ${rscModuleInformationJson} */\n`
source = prefix + source
}

// This is a server action entry module in the client layer. We need to
// create re-exports of "virtual modules" to expose the reference IDs to the
Expand All @@ -23,11 +29,14 @@ const flightClientModuleLoader: webpack.LoaderDefinitionFunction =
// production mode. In development mode, we want to preserve the original
// modules (as transformed by SWC) to ensure that source mapping works.
if (buildInfo.rsc.actionIds && process.env.NODE_ENV === 'production') {
return Object.entries(buildInfo.rsc.actionIds)
.map(([id, name]) => {
return `export { ${name} } from 'next-flight-server-reference-proxy-loader?id=${id}&name=${name}!'`
})
.join('\n')
return (
prefix +
Object.entries(buildInfo.rsc.actionIds)
.map(([id, name]) => {
return `export { ${name} } from 'next-flight-server-reference-proxy-loader?id=${id}&name=${name}!'`
})
.join('\n')
)
}

return this.callback(null, source, sourceMap)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,18 @@ export default function transformSource(
if (typeof source !== 'string') {
throw new Error('Expected source to have been transformed to a string.')
}

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(module)
buildInfo.rsc = getRSCModuleInformation(source, true)
let prefix = ''
if (process.env.BUILTIN_FLIGHT_CLIENT_ENTRY_PLUGIN) {
const rscModuleInformationJson = JSON.stringify(buildInfo.rsc)
prefix = `/* __rspack_internal_rsc_module_information_do_not_use__ ${rscModuleInformationJson} */\n`
source = prefix + source
}

// Resource key is the unique identifier for the resource. When RSC renders
// a client module, that key is used to identify that module across all compiler
Expand Down Expand Up @@ -112,7 +117,9 @@ export default function transformSource(
return
}

let esmSource = `\
let esmSource =
prefix +
`\
import { registerClientReference } from "react-server-dom-webpack/server.edge";
`
for (const ref of clientRefs) {
Expand All @@ -139,12 +146,13 @@ ${JSON.stringify(ref)},

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

module.exports = createProxy(${stringifiedResourceKey})
`

return this.callback(null, cjsSource, sourceMap)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -941,38 +941,55 @@ export class FlightClientEntryPlugin {
}

addEntry(
compilation: any,
compilation: webpack.Compilation,
context: string,
dependency: webpack.Dependency,
options: webpack.EntryOptions
): Promise<any> /* Promise<module> */ {
return new Promise((resolve, reject) => {
const entry = compilation.entries.get(options.name)
entry.includeDependencies.push(dependency)
compilation.hooks.addEntry.call(entry, options)
compilation.addModuleTree(
{
context,
dependency,
contextInfo: { issuerLayer: options.layer },
},
(err: Error | undefined, module: any) => {
if ('rspack' in compilation.compiler) {
compilation.addInclude(context, dependency, options, (err, module) => {
if (err) {
compilation.hooks.failedEntry.call(dependency, options, err)
return reject(err)
}

compilation.hooks.succeedEntry.call(dependency, options, module)

compilation.moduleGraph
.getExportsInfo(module)
.getExportsInfo(module!)
.setUsedInUnknownWay(
this.isEdgeServer ? EDGE_RUNTIME_WEBPACK : DEFAULT_RUNTIME_WEBPACK
)

return resolve(module)
}
)
})
} else {
const entry = compilation.entries.get(options.name!)!
entry.includeDependencies.push(dependency)
compilation.hooks.addEntry.call(entry as any, options)
compilation.addModuleTree(
{
context,
dependency,
contextInfo: { issuerLayer: options.layer },
},
(err: any, module: any) => {
if (err) {
compilation.hooks.failedEntry.call(dependency, options, err)
return reject(err)
}

compilation.hooks.succeedEntry.call(dependency, options, module)

compilation.moduleGraph
.getExportsInfo(module)
.setUsedInUnknownWay(
this.isEdgeServer
? EDGE_RUNTIME_WEBPACK
: DEFAULT_RUNTIME_WEBPACK
)

return resolve(module)
}
)
}
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,7 @@ export class ClientReferenceManifestPlugin {
const recordModule = (modId: ModuleId, mod: webpack.NormalModule) => {
let resource =
mod.type === 'css/mini-extract'
? // @ts-expect-error TODO: use `identifier()` instead.
mod._identifier.slice(mod._identifier.lastIndexOf('!') + 1)
? mod.identifier().slice(mod.identifier().lastIndexOf('!') + 1)
: mod.resource

if (!resource) {
Expand Down Expand Up @@ -525,7 +524,11 @@ export class ClientReferenceManifestPlugin {
} else {
// If this is a concatenation, register each child to the parent ID.
if (
connection.module?.constructor.name === 'ConcatenatedModule'
connection.module?.constructor.name ===
'ConcatenatedModule' ||
(Boolean(process.env.NEXT_RSPACK) &&
(connection.module as any)?.constructorName ===
'ConcatenatedModule')
) {
const concatenatedMod = connection.module
const concatenatedModId =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import type { Compiler } from '@rspack/core'
import {
getInvalidator,
getEntries,
EntryTypes,
getEntryKey,
} from '../../../server/dev/on-demand-entry-handler'
import { COMPILER_NAMES } from '../../../shared/lib/constants'

import { getProxiedPluginState } from '../../build-context'
import { PAGE_TYPES } from '../../../lib/page-types'
import { getRspackCore } from '../../../shared/lib/get-rspack'

type Actions = {
[actionId: string]: {
workers: {
[name: string]: { moduleId: string | number; async: boolean }
}
// Record which layer the action is in (rsc or sc_action), in the specific entry.
layer: {
[name: string]: string
}
}
}

export type ActionManifest = {
// Assign a unique encryption key during production build.
encryptionKey: string
node: Actions
edge: Actions
}

export interface ModuleInfo {
moduleId: string | number
async: boolean
}

const pluginState = getProxiedPluginState({
// A map to track "action" -> "list of bundles".
serverActions: {} as ActionManifest['node'],
edgeServerActions: {} as ActionManifest['edge'],

serverActionModules: {} as {
[workerName: string]: { server?: ModuleInfo; client?: ModuleInfo }
},

edgeServerActionModules: {} as {
[workerName: string]: { server?: ModuleInfo; client?: ModuleInfo }
},

ssrModules: {} as { [ssrModuleId: string]: ModuleInfo },
edgeSsrModules: {} as { [ssrModuleId: string]: ModuleInfo },

rscModules: {} as { [rscModuleId: string]: ModuleInfo },
edgeRscModules: {} as { [rscModuleId: string]: ModuleInfo },

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

interface Options {
dev: boolean
appDir: string
isEdgeServer: boolean
encryptionKey: string
}

export class RspackFlightClientEntryPlugin {
plugin: any
compiler?: Compiler

constructor(options: Options) {
const { FlightClientEntryPlugin } = getRspackCore()

this.plugin = new FlightClientEntryPlugin({
...options,
builtinAppLoader: !!process.env.BUILTIN_SWC_LOADER,
shouldInvalidateCb: ({
bundlePath,
entryName,
absolutePagePath,
clientBrowserLoader,
}: any) => {
let shouldInvalidate = false
const compiler = this.compiler!

const entries = getEntries(compiler.outputPath)
const pageKey = getEntryKey(
COMPILER_NAMES.client,
PAGE_TYPES.APP,
bundlePath
)

if (!entries[pageKey]) {
entries[pageKey] = {
type: EntryTypes.CHILD_ENTRY,
parentEntries: new Set([entryName]),
absoluteEntryFilePath: absolutePagePath,
bundlePath,
request: clientBrowserLoader,
dispose: false,
lastActiveTime: Date.now(),
}
shouldInvalidate = true
} else {
const entryData = entries[pageKey]
// New version of the client loader
if (entryData.request !== clientBrowserLoader) {
entryData.request = clientBrowserLoader
shouldInvalidate = true
}
if (entryData.type === EntryTypes.CHILD_ENTRY) {
entryData.parentEntries.add(entryName)
}
entryData.dispose = false
entryData.lastActiveTime = Date.now()
}

return shouldInvalidate
},
invalidateCb: () => {
const compiler = this.compiler!

// Invalidate in development to trigger recompilation
const invalidator = getInvalidator(compiler.outputPath)
// Check if any of the entry injections need an invalidation
if (invalidator) {
invalidator.invalidate([COMPILER_NAMES.client])
}
},
stateCb: (state: any) => {
Object.assign(pluginState.serverActions, state.serverActions)
Object.assign(pluginState.edgeServerActions, state.edgeServerActions)
Object.assign(
pluginState.serverActionModules,
state.serverActionModules
)
Object.assign(
pluginState.edgeServerActionModules,
state.edgeServerActionModules
)
Object.assign(pluginState.ssrModules, state.ssrModules)
Object.assign(pluginState.edgeSsrModules, state.edgeSsrModules)
Object.assign(pluginState.rscModules, state.rscModules)
Object.assign(pluginState.edgeRscModules, state.edgeRscModules)
Object.assign(
pluginState.injectedClientEntries,
state.injectedClientEntries
)
},
})
}

apply(compiler: Compiler) {
this.compiler = compiler
this.plugin.apply(compiler)
}
}
Loading