diff --git a/package.json b/package.json index 4001ed30b29777..d049fc7ba932b1 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,21 @@ "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-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", "test-start-turbo": "cross-env NEXT_TEST_MODE=start TURBOPACK=1 TURBOPACK_BUILD=1 pnpm testheadless", "test-deploy": "cross-env NEXT_TEST_MODE=deploy pnpm testheadless", "testonly-dev": "cross-env NEXT_TEST_MODE=dev pnpm testonly", + "testonly-dev-rspack": "cross-env NEXT_TEST_MODE=dev NEXT_RSPACK=1 pnpm testonly", "testonly-dev-turbo": "cross-env NEXT_TEST_MODE=dev TURBOPACK=1 TURBOPACK_DEV=1 pnpm testonly", "testonly-start": "cross-env NEXT_TEST_MODE=start pnpm testonly", + "testonly-start-rspack": "cross-env NEXT_TEST_MODE=start NEXT_RSPACK=1 pnpm testonly", "testonly-start-turbo": "cross-env NEXT_TEST_MODE=start TURBOPACK=1 TURBOPACK_BUILD=1 pnpm testonly", "testonly-deploy": "cross-env NEXT_TEST_MODE=deploy pnpm testonly", "test": "pnpm testheadless", + "test-rspack": "cross-env NEXT_RSPACK=1 pnpm testheadless", "test-turbo": "cross-env TURBOPACK=1 TURBOPACK_DEV=1 TURBOPACK_BUILD=1 pnpm testheadless", "testonly": "jest --runInBand", "testheadless": "cross-env HEADLESS=true pnpm testonly", diff --git a/packages/next/src/bin/next.ts b/packages/next/src/bin/next.ts index 957018f1219f8e..1d1a4f38bdb2c9 100755 --- a/packages/next/src/bin/next.ts +++ b/packages/next/src/bin/next.ts @@ -21,6 +21,11 @@ import type { NextInfoOptions } from '../cli/next-info.js' import type { NextDevOptions } from '../cli/next-dev.js' import type { NextBuildOptions } from '../cli/next-build.js' +if (process.env.NEXT_RSPACK) { + // silent rspack's schema check + process.env.RSPACK_CONFIG_VALIDATE = 'loose-silent' +} + if ( !semver.satisfies( process.versions.node, diff --git a/packages/next/src/build/webpack/config/blocks/base.ts b/packages/next/src/build/webpack/config/blocks/base.ts index d7b2ed05016685..9e8d19981ce629 100644 --- a/packages/next/src/build/webpack/config/blocks/base.ts +++ b/packages/next/src/build/webpack/config/blocks/base.ts @@ -34,6 +34,8 @@ export const base = curry(function base( if (ctx.isDevelopment) { if (process.env.__NEXT_TEST_MODE && !process.env.__NEXT_TEST_WITH_DEVTOOL) { config.devtool = false + } else if (process.env.NEXT_RSPACK) { + config.devtool = 'source-map' } else { // `eval-source-map` provides full-fidelity source maps for the // original source, including columns and original variable names. @@ -73,7 +75,7 @@ export const base = curry(function base( shouldIgnorePath, }) ) - } else if (config.devtool === 'eval-source-map') { + } else if (config.devtool === 'eval-source-map' && !process.env.NEXT_RSPACK) { // We're using a fork of `eval-source-map` config.devtool = false config.plugins.push( diff --git a/packages/next/src/build/webpack/config/blocks/css/index.ts b/packages/next/src/build/webpack/config/blocks/css/index.ts index c62e1e5a27548d..a30b680c54d56f 100644 --- a/packages/next/src/build/webpack/config/blocks/css/index.ts +++ b/packages/next/src/build/webpack/config/blocks/css/index.ts @@ -14,6 +14,7 @@ import { import { getPostCssPlugins } from './plugins' import { nonNullable } from '../../../../../lib/non-nullable' import { WEBPACK_LAYERS } from '../../../../../lib/constants' +import { getRspackCore } from '../../../../../shared/lib/get-rspack' // RegExps for all Style Sheet variants export const regexLikeCss = /\.(css|scss|sass)$/ @@ -147,6 +148,7 @@ export const css = curry(async function css( ctx: ConfigurationContext, config: webpack.Configuration ) { + const isRspack = Boolean(process.env.NEXT_RSPACK) const { prependData: sassPrependData, additionalData: sassAdditionalData, @@ -592,8 +594,10 @@ export const css = curry(async function css( // Enable full mini-css-extract-plugin hmr for prod mode pages or app dir if (ctx.isClient && (ctx.isProduction || ctx.hasAppDir)) { // Extract CSS as CSS file(s) in the client-side production bundle. - const MiniCssExtractPlugin = - require('../../../plugins/mini-css-extract-plugin').default + const MiniCssExtractPlugin = isRspack + ? getRspackCore().CssExtractRspackPlugin + : require('../../../plugins/mini-css-extract-plugin').default + fns.push( plugin( // @ts-ignore webpack 5 compat diff --git a/packages/next/src/build/webpack/config/blocks/css/loaders/client.ts b/packages/next/src/build/webpack/config/blocks/css/loaders/client.ts index c668f28aff5675..8b298e49744d3f 100644 --- a/packages/next/src/build/webpack/config/blocks/css/loaders/client.ts +++ b/packages/next/src/build/webpack/config/blocks/css/loaders/client.ts @@ -1,4 +1,5 @@ import type { webpack } from 'next/dist/compiled/webpack/webpack' +import { getRspackCore } from '../../../../../../shared/lib/get-rspack' export function getClientStyleLoader({ hasAppDir, @@ -11,6 +12,7 @@ export function getClientStyleLoader({ isDevelopment: boolean assetPrefix: string }): webpack.RuleSetUseItem { + const isRspack = Boolean(process.env.NEXT_RSPACK) const shouldEnableApp = typeof isAppDir === 'boolean' ? isAppDir : hasAppDir // Keep next-style-loader for development mode in `pages/` @@ -41,8 +43,10 @@ export function getClientStyleLoader({ } } - const MiniCssExtractPlugin = - require('../../../../plugins/mini-css-extract-plugin').default + const MiniCssExtractPlugin = isRspack + ? getRspackCore().rspack.CssExtractRspackPlugin + : require('../../../../plugins/mini-css-extract-plugin').default + return { loader: MiniCssExtractPlugin.loader, options: { diff --git a/packages/next/src/build/webpack/loaders/utils.ts b/packages/next/src/build/webpack/loaders/utils.ts index 7752018d50d912..a9c49c76d77182 100644 --- a/packages/next/src/build/webpack/loaders/utils.ts +++ b/packages/next/src/build/webpack/loaders/utils.ts @@ -35,6 +35,8 @@ export function isCSSMod(mod: { mod.loaders?.some( ({ loader }) => loader.includes('next-style-loader/index.js') || + (process.env.NEXT_RSPACK && + loader.includes('rspack.CssExtractRspackPlugin.loader')) || loader.includes('mini-css-extract-plugin/loader.js') || loader.includes('@vanilla-extract/webpack-plugin/loader/') ) diff --git a/packages/next/src/build/webpack/plugins/middleware-plugin.ts b/packages/next/src/build/webpack/plugins/middleware-plugin.ts index a780cc1e966dc5..a69e7b394c9280 100644 --- a/packages/next/src/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/src/build/webpack/plugins/middleware-plugin.ts @@ -791,9 +791,13 @@ export default class MiddlewarePlugin { compiler, compilation, }) - hooks.parser.for('javascript/auto').tap(NAME, codeAnalyzer) - hooks.parser.for('javascript/dynamic').tap(NAME, codeAnalyzer) - hooks.parser.for('javascript/esm').tap(NAME, codeAnalyzer) + + // parser hooks aren't available in rspack + if (!process.env.NEXT_RSPACK) { + hooks.parser.for('javascript/auto').tap(NAME, codeAnalyzer) + hooks.parser.for('javascript/dynamic').tap(NAME, codeAnalyzer) + hooks.parser.for('javascript/esm').tap(NAME, codeAnalyzer) + } /** * Extract all metadata for the entry points in a Map object. diff --git a/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts b/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts index b4efaa31167fc6..eb130e00dab536 100644 --- a/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts +++ b/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts @@ -190,9 +190,9 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { for (const entrypoint of compilation.entrypoints.values()) { const entryFiles = new Set() - for (const chunk of entrypoint - .getEntrypointChunk() - .getAllReferencedChunks()) { + for (const chunk of process.env.NEXT_RSPACK + ? entrypoint.chunks + : entrypoint.getEntrypointChunk().getAllReferencedChunks()) { for (const file of chunk.files) { if (isTraceable(file)) { const filePath = nodePath.join(outputPath, file) @@ -580,6 +580,23 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { apply(compiler: webpack.Compiler) { compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { + compilation.hooks.processAssets.tapAsync( + { + name: PLUGIN_NAME, + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE, + }, + (_assets: any, callback: any) => { + this.createTraceAssets(compilation, traceEntrypointsPluginSpan) + .then(() => callback()) + .catch((err) => callback(err)) + } + ) + + // rspack doesn't support all API below so only create trace assets + if (process.env.NEXT_RSPACK) { + return + } + const readlink = async (path: string): Promise => { try { return await new Promise((resolve, reject) => { diff --git a/packages/next/src/build/webpack/plugins/nextjs-require-cache-hot-reloader.ts b/packages/next/src/build/webpack/plugins/nextjs-require-cache-hot-reloader.ts index c66019a762910b..aa96783c19bf53 100644 --- a/packages/next/src/build/webpack/plugins/nextjs-require-cache-hot-reloader.ts +++ b/packages/next/src/build/webpack/plugins/nextjs-require-cache-hot-reloader.ts @@ -37,7 +37,7 @@ export class NextJsRequireCacheHotReloader implements WebpackPluginInstance { // we need to make sure to clear all server entries from cache // since they can have a stale webpack-runtime cache // which needs to always be in-sync - const entries = [...compilation.entries.keys()].filter((entry) => { + const entries = [...compilation.entrypoints.keys()].filter((entry) => { const isAppPath = entry.toString().startsWith('app/') return entry.toString().startsWith('pages/') || isAppPath }) diff --git a/packages/next/src/build/webpack/utils.ts b/packages/next/src/build/webpack/utils.ts index 0ea11a65bb52d5..5e0585c335456a 100644 --- a/packages/next/src/build/webpack/utils.ts +++ b/packages/next/src/build/webpack/utils.ts @@ -95,6 +95,12 @@ export function getModuleReferencesInOrder( module: Module, moduleGraph: ModuleGraph ): ModuleGraphConnection[] { + if ( + 'getOutgoingConnectionsInOrder' in moduleGraph && + typeof moduleGraph.getOutgoingConnectionsInOrder === 'function' + ) { + return moduleGraph.getOutgoingConnectionsInOrder(module) + } const connections = [] for (const connection of moduleGraph.getOutgoingConnections(module)) { if (connection.dependency && connection.module) { diff --git a/packages/next/src/bundles/webpack/packages/webpack.js b/packages/next/src/bundles/webpack/packages/webpack.js index 1783a3675c53e6..13e4602b34bc1b 100644 --- a/packages/next/src/bundles/webpack/packages/webpack.js +++ b/packages/next/src/bundles/webpack/packages/webpack.js @@ -3,7 +3,14 @@ exports.__esModule = true exports.default = undefined exports.init = function () { - if (process.env.NEXT_PRIVATE_LOCAL_WEBPACK) { + if (process.env.NEXT_RSPACK) { + // eslint-disable-next-line + Object.assign(exports, getRspackCore()) + Object.assign(exports, { + // eslint-disable-next-line import/no-extraneous-dependencies + StringXor: require('webpack/lib/util/StringXor'), + }) + } else if (process.env.NEXT_PRIVATE_LOCAL_WEBPACK) { Object.assign(exports, { // eslint-disable-next-line import/no-extraneous-dependencies BasicEvaluatedExpression: require('webpack/lib/javascript/BasicEvaluatedExpression'), @@ -33,3 +40,18 @@ exports.init = function () { Object.assign(exports, require('./bundle5')()) } } + +function getRspackCore() { + try { + // eslint-disable-next-line import/no-extraneous-dependencies + return require('@rspack/core') + } 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.' + ) + } + + throw e + } +} diff --git a/packages/next/src/client/app-dir/link.tsx b/packages/next/src/client/app-dir/link.tsx index dab7140d35eb1d..241abfa00d771d 100644 --- a/packages/next/src/client/app-dir/link.tsx +++ b/packages/next/src/client/app-dir/link.tsx @@ -245,6 +245,9 @@ function mountLinkInstance( router: AppRouterInstance, kind: PrefetchKind.AUTO | PrefetchKind.FULL ) { + // element can be falsy which can break WeakMap and observing + if (!element) return + let prefetchUrl: URL | null = null try { prefetchUrl = createPrefetchURL(href) diff --git a/packages/next/src/compiled/webpack/webpack.js b/packages/next/src/compiled/webpack/webpack.js index 1783a3675c53e6..13e4602b34bc1b 100644 --- a/packages/next/src/compiled/webpack/webpack.js +++ b/packages/next/src/compiled/webpack/webpack.js @@ -3,7 +3,14 @@ exports.__esModule = true exports.default = undefined exports.init = function () { - if (process.env.NEXT_PRIVATE_LOCAL_WEBPACK) { + if (process.env.NEXT_RSPACK) { + // eslint-disable-next-line + Object.assign(exports, getRspackCore()) + Object.assign(exports, { + // eslint-disable-next-line import/no-extraneous-dependencies + StringXor: require('webpack/lib/util/StringXor'), + }) + } else if (process.env.NEXT_PRIVATE_LOCAL_WEBPACK) { Object.assign(exports, { // eslint-disable-next-line import/no-extraneous-dependencies BasicEvaluatedExpression: require('webpack/lib/javascript/BasicEvaluatedExpression'), @@ -33,3 +40,18 @@ exports.init = function () { Object.assign(exports, require('./bundle5')()) } } + +function getRspackCore() { + try { + // eslint-disable-next-line import/no-extraneous-dependencies + return require('@rspack/core') + } 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.' + ) + } + + throw e + } +}