From e9dcee655848467a58965ae2aca363f992813ac3 Mon Sep 17 00:00:00 2001 From: Will Binns-Smith Date: Wed, 19 Feb 2025 16:44:36 -0800 Subject: [PATCH] Next.js plugin for alternate bundler (#76186) This adds a new package allowing users to opt-into using Rspack in place of webpack in Next.js. While there is some logic to handle rspack in core, the actual third-party dependencies are not included there, only conditionally loaded. This package: - Includes the actual npm dependencies on `@rspack/core` and `@rspack/plugin-react-refresh` - Sets the `NEXT_RSPACK` environment variable, which Next.js itself uses to load the dependencies - It exists as a kind of no-op `withRspack` decorator. @timneutkens is this the best approach to this? --- package.json | 2 +- packages/next-plugin-rspack/README.md | 44 ++++++++++++++++++++++ packages/next-plugin-rspack/index.js | 7 ++++ packages/next-plugin-rspack/package.json | 12 ++++++ packages/next/errors.json | 4 +- packages/next/src/shared/lib/get-rspack.ts | 4 +- pnpm-lock.yaml | 9 +++++ test/lib/create-next-install.js | 17 +++++++++ 8 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 packages/next-plugin-rspack/README.md create mode 100644 packages/next-plugin-rspack/index.js create mode 100644 packages/next-plugin-rspack/package.json diff --git a/package.json b/package.json index a9143324d8058..e91161f68b74d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "test-types": "tsc", "test-unit": "jest test/unit/ packages/next/ packages/font", "test-dev": "cross-env NEXT_TEST_MODE=dev pnpm testheadless", - "test-dev-rspack": "cross-env NEXT_TEST_MODE=dev NEXT_RSPACK=1 BUILTIN_FLIGHT_CLIENT_ENTRY_PLUGIN=1 BUILTIN_APP_LOADER=1 BUILTIN_SWC_LOADER=1 pnpm testheadless", + "test-dev-rspack": "cross-env NEXT_TEST_MODE=dev NEXT_TEST_USE_RSPACK=1 BUILTIN_FLIGHT_CLIENT_ENTRY_PLUGIN=1 BUILTIN_APP_LOADER=1 BUILTIN_SWC_LOADER=1 pnpm testheadless", "test-dev-turbo": "cross-env NEXT_TEST_MODE=dev TURBOPACK=1 TURBOPACK_DEV=1 pnpm testheadless", "test-start": "cross-env NEXT_TEST_MODE=start pnpm testheadless", "test-start-rspack": "cross-env NEXT_TEST_MODE=start NEXT_RSPACK=1 BUILTIN_FLIGHT_CLIENT_ENTRY_PLUGIN=1 BUILTIN_APP_LOADER=1 BUILTIN_SWC_LOADER=1 pnpm testheadless", diff --git a/packages/next-plugin-rspack/README.md b/packages/next-plugin-rspack/README.md new file mode 100644 index 0000000000000..4762c00b7b7da --- /dev/null +++ b/packages/next-plugin-rspack/README.md @@ -0,0 +1,44 @@ +# @next/plugin-rspack + +This plugin allows you to use [Rspack](https://rspack.dev) in place of webpack with Next.js. + +## Installation + +``` +npm install @next/plugin-rspack +``` + +or + +``` +yarn add @next/plugin-rspack +``` + +## Usage + +Create or update a `next.config.js`/`next.config.ts` and wrap your existing configuration: + +```js +const withRspack = require('@next/plugin-rspack') + +/** @type {import('next').NextConfig} */ +const nextConfig = { + /* config options here */ +} + +module.exports = withRspack(nextConfig) +``` + +## Usage with next-compose-plugins + +Alternatively, you can use `next-compose-plugins` to quickly integrate `@next/plugin-rspack` with other Next.js plugins: + +```js +const withPlugins = require('next-compose-plugins') +const withRspack = require('@next/plugin-rspack') + +module.exports = withPlugins([ + [withRspack], + // your other plugins here +]) +``` diff --git a/packages/next-plugin-rspack/index.js b/packages/next-plugin-rspack/index.js new file mode 100644 index 0000000000000..006e0c7d4f8a2 --- /dev/null +++ b/packages/next-plugin-rspack/index.js @@ -0,0 +1,7 @@ +module.exports = function withRspack(config) { + process.env.NEXT_RSPACK = 'true' + process.env.BUILTIN_FLIGHT_CLIENT_ENTRY_PLUGIN = 'true' + process.env.BUILTIN_APP_LOADER = 'true' + process.env.BUILTIN_SWC_LOADER = 'true' + return config +} diff --git a/packages/next-plugin-rspack/package.json b/packages/next-plugin-rspack/package.json new file mode 100644 index 0000000000000..804e1618063c6 --- /dev/null +++ b/packages/next-plugin-rspack/package.json @@ -0,0 +1,12 @@ +{ + "name": "@next/plugin-rspack", + "version": "15.2.0-canary.65", + "repository": { + "url": "vercel/next.js", + "directory": "packages/next-plugin-rspack" + }, + "dependencies": { + "@rspack/core": "npm:@rspack-canary/core@1.2.0-canary-37cc738d-20250207113050", + "@rspack/plugin-react-refresh": "1.0.1" + } +} diff --git a/packages/next/errors.json b/packages/next/errors.json index bea5b6cd1c768..0ddb334d4793f 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -644,5 +644,7 @@ "643": "Invalid \"devIndicator.position\" provided, expected one of %s, received %s", "644": "@rspack/core is not available. Please make sure the appropriate Next.js plugin is installed.", "645": "@rspack/plugin-react-refresh is not available. Please make sure the appropriate Next.js plugin is installed.", - "646": "No span found for compilation" + "646": "No span found for compilation", + "647": "@rspack/core is not available. Please make sure `@next/plugin-rspack` is correctly installed.", + "648": "@rspack/plugin-react-refresh is not available. Please make sure `@next/plugin-rspack` is correctly installed." } diff --git a/packages/next/src/shared/lib/get-rspack.ts b/packages/next/src/shared/lib/get-rspack.ts index 86741c24cadce..9ba62d1ad4277 100644 --- a/packages/next/src/shared/lib/get-rspack.ts +++ b/packages/next/src/shared/lib/get-rspack.ts @@ -5,7 +5,7 @@ export function getRspackCore() { } catch (e) { if (e instanceof Error && 'code' in e && e.code === 'MODULE_NOT_FOUND') { throw new Error( - '@rspack/core is not available. Please make sure the appropriate Next.js plugin is installed.' + '@rspack/core is not available. Please make sure `@next/plugin-rspack` is correctly installed.' ) } @@ -20,7 +20,7 @@ export function getRspackReactRefresh() { } catch (e) { if (e instanceof Error && 'code' in e && e.code === 'MODULE_NOT_FOUND') { throw new Error( - '@rspack/plugin-react-refresh is not available. Please make sure the appropriate Next.js plugin is installed.' + '@rspack/plugin-react-refresh is not available. Please make sure `@next/plugin-rspack` is correctly installed.' ) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d8cb1881c174..750e3cebe99a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1631,6 +1631,15 @@ importers: specifier: ^0.7.0 version: 0.7.3 + packages/next-plugin-rspack: + dependencies: + '@rspack/core': + specifier: npm:@rspack-canary/core@1.2.0-canary-37cc738d-20250207113050 + version: '@rspack-canary/core@1.2.0-canary-37cc738d-20250207113050(@swc/helpers@0.5.15)' + '@rspack/plugin-react-refresh': + specifier: 1.0.1 + version: 1.0.1(react-refresh@0.12.0) + packages/next-plugin-storybook: {} packages/next-polyfill-module: diff --git a/test/lib/create-next-install.js b/test/lib/create-next-install.js index 8d4ff54b82726..fd83206d4aecb 100644 --- a/test/lib/create-next-install.js +++ b/test/lib/create-next-install.js @@ -8,6 +8,7 @@ const { linkPackages } = require('../../.github/actions/next-stats-action/src/prepare/repo-setup')() const PREFER_OFFLINE = process.env.NEXT_TEST_PREFER_OFFLINE === '1' +const useRspack = process.env.NEXT_TEST_USE_RSPACK === '1' async function installDependencies(cwd, tmpDir) { const args = [ @@ -122,6 +123,7 @@ async function createNextInstall({ }) ) } + const combinedDependencies = { next: pkgPaths.get('next'), ...Object.keys(dependencies).reduce((prev, pkg) => { @@ -131,6 +133,12 @@ async function createNextInstall({ }, {}), } + if (useRspack) { + combinedDependencies['@next/plugin-rspack'] = pkgPaths = pkgPaths.get( + '@next/plugin-rspack' + ) + } + const scripts = { debug: `NEXT_PRIVATE_SKIP_CANARY_CHECK=1 NEXT_TELEMETRY_DISABLED=1 NEXT_TEST_NATIVE_DIR=${process.env.NEXT_TEST_NATIVE_DIR} node --inspect --trace-deprecation --enable-source-maps node_modules/next/dist/bin/next`, ...packageJson.scripts, @@ -183,6 +191,15 @@ async function createNextInstall({ .traceAsyncFn(() => installDependencies(installDir, tmpDir)) } + if (useRspack) { + // This is what the @next/plugin-rspack plugin does. + // TODO: Load the plugin properly during test + process.env.NEXT_RSPACK = 'true' + process.env.BUILTIN_FLIGHT_CLIENT_ENTRY_PLUGIN = 'true' + process.env.BUILTIN_APP_LOADER = 'true' + process.env.BUILTIN_SWC_LOADER = 'true' + } + return { installDir, pkgPaths,