From afecd03265942ab3a213ca7453929d3915fbe300 Mon Sep 17 00:00:00 2001 From: Will Binns-Smith Date: Thu, 13 Feb 2025 10:15:31 -0800 Subject: [PATCH] Built-in flight plugin for additional bundler Co-authored-by: hardfist Co-authored-by: Tim Neutkens --- packages/next/src/build/webpack-config.ts | 8 +- .../next-flight-client-entry-loader.ts | 7 + .../next-flight-client-module-loader.ts | 19 +- .../loaders/next-flight-loader/index.ts | 16 +- .../plugins/flight-client-entry-plugin.ts | 53 ++++-- .../webpack/plugins/flight-manifest-plugin.ts | 9 +- .../rspack-flight-client-entry-plugin.ts | 164 ++++++++++++++++++ 7 files changed, 245 insertions(+), 31 deletions(-) create mode 100644 packages/next/src/build/webpack/plugins/rspack-flight-client-entry-plugin.ts diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index c7c8c1612fb76..880e0e9b35b3d 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -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, @@ -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 diff --git a/packages/next/src/build/webpack/loaders/next-flight-client-entry-loader.ts b/packages/next/src/build/webpack/loaders/next-flight-client-entry-loader.ts index 819154545a01a..2346e14593d0a 100644 --- a/packages/next/src/build/webpack/loaders/next-flight-client-entry-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-flight-client-entry-loader.ts @@ -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 } diff --git a/packages/next/src/build/webpack/loaders/next-flight-client-module-loader.ts b/packages/next/src/build/webpack/loaders/next-flight-client-module-loader.ts index cd0538776b90d..186c68d6ed5a3 100644 --- a/packages/next/src/build/webpack/loaders/next-flight-client-module-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-flight-client-module-loader.ts @@ -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 @@ -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) diff --git a/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts b/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts index 51e8a6e304015..e402f884c7778 100644 --- a/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts @@ -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 @@ -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) { @@ -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) } } diff --git a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts index fbdd6961015e6..fde47f366b5c0 100644 --- a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts @@ -941,38 +941,55 @@ export class FlightClientEntryPlugin { } addEntry( - compilation: any, + compilation: webpack.Compilation, context: string, dependency: webpack.Dependency, options: webpack.EntryOptions ): Promise /* Promise */ { 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) + } + ) + } }) } diff --git a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts index 8c10a68229173..7cb97406c9d9d 100644 --- a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts @@ -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) { @@ -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 = diff --git a/packages/next/src/build/webpack/plugins/rspack-flight-client-entry-plugin.ts b/packages/next/src/build/webpack/plugins/rspack-flight-client-entry-plugin.ts new file mode 100644 index 0000000000000..5939d346484ba --- /dev/null +++ b/packages/next/src/build/webpack/plugins/rspack-flight-client-entry-plugin.ts @@ -0,0 +1,164 @@ +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, +}) + +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) => { + console.log( + 'shouldInvalidateCb', + bundlePath, + entryName, + absolutePagePath, + clientBrowserLoader + ) + 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) + } +}