diff --git a/packages/nuxt/src/app/components/nuxt-root-test.vue b/packages/nuxt/src/app/components/nuxt-root-test.vue
new file mode 100644
index 0000000000..39da3f0021
--- /dev/null
+++ b/packages/nuxt/src/app/components/nuxt-root-test.vue
@@ -0,0 +1,13 @@
+
+ Nuxt 3 ❤️ rspack
+
+
+
+
+
diff --git a/packages/nuxt/src/app/entry.ts b/packages/nuxt/src/app/entry.ts
index 4424ae4a4d..9ed973d2da 100644
--- a/packages/nuxt/src/app/entry.ts
+++ b/packages/nuxt/src/app/entry.ts
@@ -1,4 +1,3 @@
-// We set __webpack_public_path via this import with webpack builder
import { createSSRApp, createApp, nextTick } from 'vue'
import { $fetch } from 'ofetch'
// @ts-ignore
@@ -9,7 +8,8 @@ import '#build/css'
// @ts-ignore
import _plugins from '#build/plugins'
// @ts-ignore
-import RootComponent from '#build/root-component.mjs'
+// import RootComponent from '#build/root-component.mjs'
+import RootComponent from './components/nuxt-root-test.vue'
// @ts-ignore
import { appRootId } from '#build/nuxt.config.mjs'
diff --git a/packages/nuxt/src/core/app.ts b/packages/nuxt/src/core/app.ts
index 236f6cbc51..c62194ad3a 100644
--- a/packages/nuxt/src/core/app.ts
+++ b/packages/nuxt/src/core/app.ts
@@ -47,7 +47,8 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
nuxt.vfs[fullPath.replace(/\//g, '\\')] = contents
}
- if (template.write) {
+ // TODO: remove when we have support for virtual modules in rspack
+ if (template.write || nuxt.options.builder === '@nuxt/rspack-builder') {
await fsp.mkdir(dirname(fullPath), { recursive: true })
await fsp.writeFile(fullPath, contents, 'utf8')
}
diff --git a/packages/rspack/build.config.ts b/packages/rspack/build.config.ts
new file mode 100644
index 0000000000..84615d8e8b
--- /dev/null
+++ b/packages/rspack/build.config.ts
@@ -0,0 +1,23 @@
+import { defineBuildConfig } from 'unbuild'
+
+export default defineBuildConfig({
+ declaration: true,
+ entries: [
+ 'src/index'
+ ],
+ dependencies: [
+ '@nuxt/kit',
+ 'unplugin',
+ 'webpack-virtual-modules',
+ 'postcss',
+ 'postcss-loader',
+ 'vue-loader',
+ 'style-resources-loader',
+ 'url-loader',
+ 'vue'
+ ],
+ externals: [
+ '@nuxt/schema',
+ 'h3'
+ ]
+})
diff --git a/packages/rspack/package.json b/packages/rspack/package.json
new file mode 100644
index 0000000000..e5cb79d1b2
--- /dev/null
+++ b/packages/rspack/package.json
@@ -0,0 +1,76 @@
+{
+ "name": "@nuxt/rspack-builder",
+ "version": "3.3.1",
+ "repository": "nuxt/nuxt",
+ "license": "MIT",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": "./dist/index.mjs",
+ "./dist/*": "./dist/*"
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "prepack": "unbuild"
+ },
+ "dependencies": {
+ "@babel/core": "^7.21.0",
+ "@nuxt/friendly-errors-webpack-plugin": "^2.5.2",
+ "@nuxt/kit": "3.3.1",
+ "@rspack/core": "^0.1.1",
+ "@rspack/dev-client": "^0.1.1",
+ "@rspack/dev-middleware": "^0.1.1",
+ "@rspack/dev-server": "0.1.1",
+ "@rspack/postcss-loader": "^0.1.1",
+ "autoprefixer": "^10.4.14",
+ "css-minimizer-webpack-plugin": "^4.2.2",
+ "cssnano": "^5.1.15",
+ "esbuild-loader": "^3.0.1",
+ "escape-string-regexp": "^5.0.0",
+ "estree-walker": "^3.0.3",
+ "fork-ts-checker-webpack-plugin": "^8.0.0",
+ "fs-extra": "^11.1.0",
+ "hash-sum": "^2.0.0",
+ "lodash-es": "^4.17.21",
+ "magic-string": "^0.30.0",
+ "memfs": "^3.4.13",
+ "mini-css-extract-plugin": "^2.7.3",
+ "mlly": "^1.2.0",
+ "ohash": "^1.0.0",
+ "pathe": "^1.1.0",
+ "pify": "^6.1.0",
+ "postcss": "^8.4.21",
+ "postcss-import": "^15.1.0",
+ "postcss-loader": "^7.0.2",
+ "postcss-url": "^10.1.3",
+ "style-resources-loader": "^1.5.0",
+ "time-fix-plugin": "^2.0.7",
+ "ufo": "^1.1.1",
+ "unplugin": "^1.3.0",
+ "url-loader": "^4.1.1",
+ "vue-bundle-renderer": "^1.0.2",
+ "vue-loader": "^17.0.1",
+ "webpack-bundle-analyzer": "^4.8.0",
+ "webpack-hot-middleware": "^2.25.3",
+ "webpack-virtual-modules": "^0.5.0",
+ "webpackbar": "^5.0.2"
+ },
+ "devDependencies": {
+ "@nuxt/schema": "3.3.1",
+ "@types/lodash-es": "^4.17.6",
+ "@types/pify": "^5.0.1",
+ "@types/webpack-bundle-analyzer": "^4.6.0",
+ "@types/webpack-hot-middleware": "^2.25.6",
+ "@types/webpack-virtual-modules": "^0",
+ "unbuild": "latest",
+ "vue": "3.2.47"
+ },
+ "peerDependencies": {
+ "vue": "^3.2.47"
+ },
+ "engines": {
+ "node": "^14.18.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+}
diff --git a/packages/rspack/src/configs/client.ts b/packages/rspack/src/configs/client.ts
new file mode 100644
index 0000000000..1f0e0690bb
--- /dev/null
+++ b/packages/rspack/src/configs/client.ts
@@ -0,0 +1,112 @@
+import querystring from 'node:querystring'
+import { resolve } from 'pathe'
+import webpack from '@rspack/core'
+import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
+import { logger } from '@nuxt/kit'
+import { joinURL } from 'ufo'
+import ForkTSCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
+
+import type { RspackConfigContext } from '../utils/config'
+import { applyPresets } from '../utils/config'
+import { nuxt } from '../presets/nuxt'
+
+export function client (ctx: RspackConfigContext) {
+ ctx.name = 'client'
+ ctx.isClient = true
+
+ applyPresets(ctx, [
+ nuxt,
+ clientPlugins,
+ clientOptimization
+ // clientDevtool,
+ // clientPerformance,
+ // clientHMR
+ ])
+}
+
+function clientDevtool (ctx: RspackConfigContext) {
+ if (!ctx.nuxt.options.sourcemap.client) {
+ ctx.config.devtool = false
+ return
+ }
+
+ if (!ctx.isDev) {
+ ctx.config.devtool = 'source-map'
+ return
+ }
+
+ ctx.config.devtool = 'eval-cheap-module-source-map'
+}
+
+function clientPerformance (ctx: RspackConfigContext) {
+ ctx.config.performance = {
+ maxEntrypointSize: 1000 * 1024,
+ hints: ctx.isDev ? false : 'warning',
+ ...ctx.config.performance
+ }
+}
+
+function clientHMR (ctx: RspackConfigContext) {
+ const { options, config } = ctx
+
+ if (!ctx.isDev) {
+ return
+ }
+
+ const clientOptions = options.webpack.hotMiddleware?.client || {}
+ const hotMiddlewareClientOptions = {
+ reload: true,
+ timeout: 30000,
+ path: joinURL(options.app.baseURL, '__webpack_hmr', ctx.name),
+ ...clientOptions,
+ ansiColors: JSON.stringify(clientOptions.ansiColors || {}),
+ overlayStyles: JSON.stringify(clientOptions.overlayStyles || {}),
+ name: ctx.name
+ }
+ const hotMiddlewareClientOptionsStr = querystring.stringify(hotMiddlewareClientOptions)
+
+ // Add HMR support
+ const app = (config.entry as any).app as any
+ app.unshift(
+ // https://github.com/glenjamin/webpack-hot-middleware#config
+ `webpack-hot-middleware/client?${hotMiddlewareClientOptionsStr}`
+ )
+
+ // TODO: HMR
+ // config.plugins = config.plugins || []
+ // config.plugins.push(new webpack.HotModuleReplacementPlugin())
+}
+
+function clientOptimization (_ctx: RspackConfigContext) {
+ // TODO: Improve optimization.splitChunks.cacheGroups
+}
+
+function clientPlugins (ctx: RspackConfigContext) {
+ const { options, config } = ctx
+
+ // webpack Bundle Analyzer
+ // https://github.com/webpack-contrib/webpack-bundle-analyzer
+ if (!ctx.isDev && ctx.name === 'client' && options.webpack.analyze) {
+ const statsDir = resolve(options.buildDir, 'stats')
+
+ config.plugins!.push(new BundleAnalyzerPlugin({
+ analyzerMode: 'static',
+ defaultSizes: 'gzip',
+ generateStatsFile: true,
+ openAnalyzer: true,
+ reportFilename: resolve(statsDir, `${ctx.name}.html`),
+ statsFilename: resolve(statsDir, `${ctx.name}.json`),
+ ...options.webpack.analyze === true ? {} : options.webpack.analyze
+ }))
+ }
+
+ // Normally type checking runs in server config, but in `ssr: false` there is
+ // no server build, so we inject here instead.
+ if (!ctx.nuxt.options.ssr) {
+ if (ctx.nuxt.options.typescript.typeCheck === true || (ctx.nuxt.options.typescript.typeCheck === 'build' && !ctx.nuxt.options.dev)) {
+ config.plugins!.push(new ForkTSCheckerWebpackPlugin({
+ logger
+ }))
+ }
+ }
+}
diff --git a/packages/rspack/src/configs/index.ts b/packages/rspack/src/configs/index.ts
new file mode 100644
index 0000000000..ec76385105
--- /dev/null
+++ b/packages/rspack/src/configs/index.ts
@@ -0,0 +1,2 @@
+export { client } from './client'
+export { server } from './server'
diff --git a/packages/rspack/src/configs/server.ts b/packages/rspack/src/configs/server.ts
new file mode 100644
index 0000000000..c18266c1a8
--- /dev/null
+++ b/packages/rspack/src/configs/server.ts
@@ -0,0 +1,98 @@
+import { isAbsolute } from 'pathe'
+import webpack from '@rspack/core'
+import ForkTSCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
+import { logger } from '@nuxt/kit'
+import type { RspackConfigContext } from '../utils/config'
+import { applyPresets, getRspackConfig } from '../utils/config'
+import { nuxt } from '../presets/nuxt'
+import { node } from '../presets/node'
+
+const assetPattern = /\.(css|s[ca]ss|png|jpe?g|gif|svg|woff2?|eot|ttf|otf|webp|webm|mp4|ogv)(\?.*)?$/i
+
+export function server (ctx: RspackConfigContext) {
+ ctx.name = 'server'
+ ctx.isServer = true
+
+ applyPresets(ctx, [
+ nuxt,
+ node,
+ serverStandalone,
+ serverPreset,
+ serverPlugins
+ ])
+
+ return getRspackConfig(ctx)
+}
+
+function serverPreset (ctx: RspackConfigContext) {
+ const { config } = ctx
+
+ config.output!.filename = 'server.mjs'
+
+ config.devtool = ctx.nuxt.options.sourcemap.server ? ctx.isDev ? 'cheap-module-source-map' : 'source-map' : false
+
+ config.optimization = {
+ splitChunks: false,
+ minimize: false
+ }
+}
+
+function serverStandalone (ctx: RspackConfigContext) {
+ // TODO: Refactor this out of webpack
+ const inline = [
+ 'src/',
+ '#app',
+ 'nuxt',
+ 'nuxt3',
+ '!',
+ '-!',
+ '~',
+ '@/',
+ '#',
+ ...ctx.options.build.transpile
+ ]
+ const external = ['#internal/nitro']
+
+ if (!Array.isArray(ctx.config.externals)) { return }
+ ctx.config.externals.push(({ request }, cb) => {
+ if (!request) {
+ return cb(undefined, false)
+ }
+ if (external.includes(request)) {
+ return cb(undefined, true)
+ }
+ if (
+ request[0] === '.' ||
+ isAbsolute(request) ||
+ inline.find(prefix => typeof prefix === 'string' && request.startsWith(prefix)) ||
+ assetPattern.test(request)
+ ) {
+ // console.log('Inline', request)
+ return cb(undefined, false)
+ }
+ // console.log('Ext', request)
+ return cb(undefined, true)
+ })
+}
+
+function serverPlugins (ctx: RspackConfigContext) {
+ const { config, options } = ctx
+
+ config.plugins = config.plugins || []
+
+ // Server polyfills
+ // TODO:
+ // if (options.webpack.serverURLPolyfill) {
+ // config.plugins.push(new webpack.ProvidePlugin({
+ // URL: [options.webpack.serverURLPolyfill, 'URL'],
+ // URLSearchParams: [options.webpack.serverURLPolyfill, 'URLSearchParams']
+ // }))
+ // }
+
+ // Add type-checking
+ if (ctx.nuxt.options.typescript.typeCheck === true || (ctx.nuxt.options.typescript.typeCheck === 'build' && !ctx.nuxt.options.dev)) {
+ config.plugins!.push(new ForkTSCheckerWebpackPlugin({
+ logger
+ }))
+ }
+}
diff --git a/packages/rspack/src/index.ts b/packages/rspack/src/index.ts
new file mode 100644
index 0000000000..a80612cf62
--- /dev/null
+++ b/packages/rspack/src/index.ts
@@ -0,0 +1 @@
+export * from './rspack'
diff --git a/packages/rspack/src/loaders/vue.cjs b/packages/rspack/src/loaders/vue.cjs
new file mode 100644
index 0000000000..0916ea24d3
--- /dev/null
+++ b/packages/rspack/src/loaders/vue.cjs
@@ -0,0 +1,61 @@
+const util = require('node:util')
+const { compileTemplate, compileScript, parse } = require('@vue/compiler-sfc')
+const qs = require('qs')
+// const type { LoaderContext } = require('@rspack/core')
+
+const descCache = new Map()
+
+module.exports = function vueLoader (content) {
+ const query = qs.parse(this.resourceQuery, { ignoreQueryPrefix: true })
+
+ const callback = this.async()
+ const inner = async () => {
+ if (query.vue === 'true') {
+ const cache = descCache.get(this.resourcePath)
+ const { script, styles, template } = cache
+ if (query.type === 'script') {
+ return script.content
+ } else if (query.type === 'style') {
+ const lang = styles[0]?.lang
+ return styles[0]?.content || 'export default {}'
+ } else if (query.type === 'template') {
+ if (!template) {
+ return ''
+ } else {
+ const result = compileTemplate({
+ source: template.content,
+ id: '1234'
+ })
+ return result.code
+ }
+ }
+ } else {
+ const parsed = parse(content, {
+ sourceMap: false,
+ filename: this.resourcePath
+ })
+ const descriptor = parsed.descriptor
+ const { script, scriptSetup, styles, template, customBlocks } = descriptor
+ descCache.set(this.resource, {
+ ...parsed.descriptor,
+ script: compileScript(descriptor, {
+ id: Math.random().toString(),
+ isProd: false
+ })
+ })
+ const jsPath = this.resourcePath + '?vue=true&type=script'
+ // const jscode = await util.promisify(this.loadModule)(jsPath);
+ const cssPath = this.resourcePath + '?vue=true&type=style'
+ const templatePath = this.resourcePath + '?vue=true&type=template'
+ // const csscode = await util.promisify(this.loadModule)(cssPath);
+ // console.log('csscode:', csscode);
+ return `import obj from ${JSON.stringify(jsPath)};
+ require(${
+ JSON.stringify(cssPath)});const { render } = require(${
+ JSON.stringify(templatePath)});export default { ...obj, render}`
+ }
+ }
+ return util.callbackify(inner)((err, data) => {
+ callback(err, data)
+ })
+}
diff --git a/packages/rspack/src/plugins/chunk.ts b/packages/rspack/src/plugins/chunk.ts
new file mode 100644
index 0000000000..2a5f49b809
--- /dev/null
+++ b/packages/rspack/src/plugins/chunk.ts
@@ -0,0 +1,28 @@
+import type { Compiler } from '@rspack/core'
+import webpack from '@rspack/core'
+
+const pluginName = 'ChunkErrorPlugin'
+
+const script = `
+if (typeof ${webpack.RuntimeGlobals.require} !== "undefined") {
+ var _ensureChunk = ${webpack.RuntimeGlobals.ensureChunk};
+ ${webpack.RuntimeGlobals.ensureChunk} = function (chunkId) {
+ return Promise.resolve(_ensureChunk(chunkId)).catch(err => {
+ const e = new Event("nuxt.preloadError");
+ e.payload = err;
+ window.dispatchEvent(e);
+ throw err;
+ });
+ };
+};`
+
+export class ChunkErrorPlugin {
+ apply (compiler: Compiler) {
+ compiler.hooks.thisCompilation.tap(pluginName, compilation =>
+ compilation.mainTemplate.hooks.localVars.tap(
+ { name: pluginName, stage: 1 },
+ source => source + script
+ )
+ )
+ }
+}
diff --git a/packages/rspack/src/plugins/dynamic-base.ts b/packages/rspack/src/plugins/dynamic-base.ts
new file mode 100644
index 0000000000..a5f89492b2
--- /dev/null
+++ b/packages/rspack/src/plugins/dynamic-base.ts
@@ -0,0 +1,33 @@
+import { createUnplugin } from 'unplugin'
+import MagicString from 'magic-string'
+
+interface DynamicBasePluginOptions {
+ globalPublicPath?: string
+ sourcemap?: boolean
+}
+
+const defaults: DynamicBasePluginOptions = {
+ globalPublicPath: '__rspack_public_path__',
+ sourcemap: true
+}
+
+export const DynamicBasePlugin = createUnplugin((options: DynamicBasePluginOptions = {}) => {
+ options = { ...defaults, ...options }
+ return {
+ name: 'nuxt:dynamic-base-path',
+ enforce: 'post',
+ transform (code, id) {
+ if (!id.includes('paths.mjs') || !code.includes('const appConfig = ')) {
+ return
+ }
+ const s = new MagicString(code)
+ s.append(`\n${options.globalPublicPath} = buildAssetsURL();\n`)
+ return {
+ code: s.toString(),
+ map: options.sourcemap
+ ? s.generateMap({ source: id, includeContent: true })
+ : undefined
+ }
+ }
+ }
+})
diff --git a/packages/rspack/src/plugins/vue/client.ts b/packages/rspack/src/plugins/vue/client.ts
new file mode 100644
index 0000000000..1bc8b658e8
--- /dev/null
+++ b/packages/rspack/src/plugins/vue/client.ts
@@ -0,0 +1,137 @@
+/**
+ * This file is based on Vue.js (MIT) webpack plugins
+ * https://github.com/vuejs/vue/blob/dev/src/server/webpack-plugin/client.js
+ */
+
+import { normalizeWebpackManifest } from 'vue-bundle-renderer'
+import { dirname } from 'pathe'
+import hash from 'hash-sum'
+import { uniq } from 'lodash-es'
+import fse from 'fs-extra'
+
+import type { Nuxt } from '@nuxt/schema'
+import type { Compilation, Compiler } from '@rspack/core'
+import { isJS, isCSS, isHotUpdate } from './util'
+
+interface PluginOptions {
+ filename: string
+ nuxt: Nuxt
+}
+
+export default class VueSSRClientPlugin {
+ options: PluginOptions
+
+ constructor (options: PluginOptions) {
+ this.options = Object.assign({
+ filename: null
+ }, options)
+ }
+
+ apply (compiler: Compiler) {
+ compiler.hooks.afterEmit.tap('VueSSRClientPlugin', async (compilation: Compilation) => {
+ const stats = compilation.getStats().toJson()
+
+ const allFiles = uniq(stats.assets!
+ .map(a => a.name))
+ .filter(file => !isHotUpdate(file))
+
+ const initialFiles = uniq(Object.keys(stats.entrypoints!)
+ .map(name => stats.entrypoints![name].assets!)
+ .reduce((files, entryAssets) => files.concat(entryAssets.map(entryAsset => entryAsset.name)), [] as string[])
+ .filter(file => isJS(file) || isCSS(file)))
+ .filter(file => !isHotUpdate(file))
+
+ const asyncFiles = allFiles
+ .filter(file => isJS(file) || isCSS(file))
+ .filter(file => !initialFiles.includes(file))
+ .filter(file => !isHotUpdate(file))
+
+ const assetsMapping: Record = {}
+ stats.assets!
+ .filter(({ name }) => isJS(name))
+ .filter(({ name }) => !isHotUpdate(name))
+ .forEach(({ name, chunkNames = [] }) => {
+ const componentHash = hash(chunkNames.join('|'))
+ if (!assetsMapping[componentHash]) {
+ assetsMapping[componentHash] = []
+ }
+ assetsMapping[componentHash].push(name)
+ })
+
+ const webpackManifest = {
+ publicPath: stats.publicPath,
+ all: allFiles,
+ initial: initialFiles,
+ async: asyncFiles,
+ modules: { /* [identifier: string]: Array */ } as Record,
+ assetsMapping
+ }
+
+ // console.log(stats.modules)
+
+ const { entrypoints = {}, namedChunkGroups = {} } = stats
+ const assetModules = stats.modules!.filter(m => m.assets?.length)
+ const fileToIndex = (file: string) => webpackManifest.all.indexOf(file)
+ stats.modules!.forEach((m) => {
+ // Ignore modules duplicated in multiple chunks
+ if (m.chunks!.length === 1) {
+ const [cid] = m.chunks!
+ const chunk = stats.chunks!.find(c => c.id === cid)
+ if (!chunk || !chunk.files) {
+ return
+ }
+ const id = m.identifier!.replace(/\s\w+$/, '') // remove appended hash
+ const filesSet = new Set(chunk.files.map(fileToIndex).filter(i => i !== -1))
+
+ for (const chunkName of chunk.names!) {
+ if (!entrypoints[chunkName]) {
+ const chunkGroup = namedChunkGroups[chunkName]
+ if (chunkGroup) {
+ for (const asset of chunkGroup.assets!) {
+ filesSet.add(fileToIndex(asset.name))
+ }
+ }
+ }
+ }
+
+ const files = Array.from(filesSet)
+ webpackManifest.modules[hash(id)] = files
+
+ // In production mode, modules may be concatenated by scope hoisting
+ // Include ConcatenatedModule for not losing module-component mapping
+ if (Array.isArray(m.modules)) {
+ for (const concatenatedModule of m.modules) {
+ const id = hash(concatenatedModule.identifier!.replace(/\s\w+$/, ''))
+ if (!webpackManifest.modules[id]) {
+ webpackManifest.modules[id] = files
+ }
+ }
+ }
+
+ // Find all asset modules associated with the same chunk
+ assetModules.forEach((m) => {
+ if (m.chunks!.includes(cid)) {
+ files.push(...(m.assets as string[])?.map(fileToIndex))
+ }
+ })
+ }
+ })
+
+ const manifest = normalizeWebpackManifest(webpackManifest as any)
+ await this.options.nuxt.callHook('build:manifest', manifest)
+
+ const src = JSON.stringify(manifest, null, 2)
+
+ await fse.mkdirp(dirname(this.options.filename))
+ await fse.writeFile(this.options.filename, src)
+
+ const mjsSrc = 'export default ' + src
+ await fse.writeFile(this.options.filename.replace('.json', '.mjs'), mjsSrc)
+
+ // assets[this.options.filename] = {
+ // source: () => src,
+ // size: () => src.length
+ // }
+ })
+ }
+}
diff --git a/packages/rspack/src/plugins/vue/server.ts b/packages/rspack/src/plugins/vue/server.ts
new file mode 100644
index 0000000000..9dd788385e
--- /dev/null
+++ b/packages/rspack/src/plugins/vue/server.ts
@@ -0,0 +1,90 @@
+import type { Compilation, Compiler } from '@rspack/core'
+import webpack from '@rspack/core'
+import { validate, isJS, extractQueryPartJS } from './util'
+
+export interface VueSSRServerPluginOptions {
+ filename: string
+}
+
+export default class VueSSRServerPlugin {
+ options: VueSSRServerPluginOptions
+
+ constructor (options: Partial = {}) {
+ this.options = Object.assign({
+ filename: null
+ }, options) as VueSSRServerPluginOptions
+ }
+
+ apply (compiler: Compiler) {
+ validate(compiler)
+ compiler.hooks.make.tap('VueSSRServerPlugin', (compilation: Compilation) => {
+ compilation.hooks.processAssets.tapAsync({
+ name: 'VueSSRServerPlugin',
+ stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
+ }, (assets: any, cb: any) => {
+ const stats = compilation.getStats().toJson()
+ const [entryName] = Object.keys(stats.entrypoints!)
+ const entryInfo = stats.entrypoints![entryName]
+
+ if (!entryInfo) {
+ // #5553
+ return cb()
+ }
+
+ const entryAssets = entryInfo.assets!.filter((asset: { name: string }) => isJS(asset.name))
+
+ if (entryAssets.length > 1) {
+ throw new Error(
+ 'Server-side bundle should have one single entry file. ' +
+ 'Avoid using CommonsChunkPlugin in the server config.'
+ )
+ }
+
+ const [entry] = entryAssets
+ if (!entry || typeof entry.name !== 'string') {
+ throw new Error(
+ `Entry "${entryName}" not found. Did you specify the correct entry option?`
+ )
+ }
+
+ const bundle = {
+ entry: entry.name,
+ files: {} as Record,
+ maps: {} as Record
+ }
+
+ stats.assets!.forEach((asset: any) => {
+ if (isJS(asset.name)) {
+ const queryPart = extractQueryPartJS(asset.name)
+ if (queryPart !== undefined) {
+ bundle.files[asset.name] = asset.name.replace(queryPart, '')
+ } else {
+ bundle.files[asset.name] = asset.name
+ }
+ } else if (asset.name.match(/\.js\.map$/)) {
+ bundle.maps[asset.name.replace(/\.map$/, '')] = asset.name
+ } else {
+ // Do not emit non-js assets for server
+ delete assets[asset.name]
+ }
+ })
+
+ const src = JSON.stringify(bundle, null, 2)
+
+ assets[this.options.filename] = {
+ source: () => src,
+ size: () => src.length
+ }
+
+ const mjsSrc = 'export default ' + src
+ assets[this.options.filename.replace('.json', '.mjs')] = {
+ source: () => mjsSrc,
+ map: () => null,
+ size: () => mjsSrc.length
+ }
+
+ cb()
+ })
+ })
+ }
+}
diff --git a/packages/rspack/src/plugins/vue/util.ts b/packages/rspack/src/plugins/vue/util.ts
new file mode 100644
index 0000000000..276636f3d5
--- /dev/null
+++ b/packages/rspack/src/plugins/vue/util.ts
@@ -0,0 +1,30 @@
+/**
+ * This file is based on Vue.js (MIT) webpack plugins
+ * https://github.com/vuejs/vue/blob/dev/src/server/webpack-plugin/util.js
+ */
+
+import { logger } from '@nuxt/kit'
+import type { Compiler } from '@rspack/core'
+
+export const validate = (compiler: Compiler) => {
+ if (compiler.options.target !== 'node') {
+ logger.warn('webpack config `target` should be "node".')
+ }
+
+ if (!compiler.options.externals) {
+ logger.info(
+ 'It is recommended to externalize dependencies in the server build for ' +
+ 'better build performance.'
+ )
+ }
+}
+
+const isJSRegExp = /\.[cm]?js(\?[^.]+)?$/
+
+export const isJS = (file: string) => isJSRegExp.test(file)
+
+export const extractQueryPartJS = (file: string) => isJSRegExp.exec(file)?.[1]
+
+export const isCSS = (file: string) => /\.css(\?[^.]+)?$/.test(file)
+
+export const isHotUpdate = (file: string) => file.includes('hot-update')
diff --git a/packages/rspack/src/plugins/warning-ignore.ts b/packages/rspack/src/plugins/warning-ignore.ts
new file mode 100644
index 0000000000..f90fba1173
--- /dev/null
+++ b/packages/rspack/src/plugins/warning-ignore.ts
@@ -0,0 +1,17 @@
+import type { Compiler, WebpackError } from '@rspack/core'
+
+export type WarningFilter = (warn: WebpackError) => boolean
+
+export default class WarningIgnorePlugin {
+ filter: WarningFilter
+
+ constructor (filter: WarningFilter) {
+ this.filter = filter
+ }
+
+ apply (compiler: Compiler) {
+ compiler.hooks.done.tap('warnfix-plugin', (stats) => {
+ stats.compilation.warnings = stats.compilation.warnings.filter(this.filter)
+ })
+ }
+}
diff --git a/packages/rspack/src/presets/assets.ts b/packages/rspack/src/presets/assets.ts
new file mode 100644
index 0000000000..4064828bab
--- /dev/null
+++ b/packages/rspack/src/presets/assets.ts
@@ -0,0 +1,18 @@
+import type { RspackConfigContext } from '../utils/config'
+
+export function assets (ctx: RspackConfigContext) {
+ ctx.config.module!.rules!.push(
+ {
+ test: /\.(png|jpe?g|gif|svg|webp)$/i,
+ type: 'asset/resource'
+ },
+ {
+ test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
+ type: 'asset/resource'
+ },
+ {
+ test: /\.(webm|mp4|ogv)$/i,
+ type: 'asset/resource'
+ }
+ )
+}
diff --git a/packages/rspack/src/presets/base.ts b/packages/rspack/src/presets/base.ts
new file mode 100644
index 0000000000..a792ed26cf
--- /dev/null
+++ b/packages/rspack/src/presets/base.ts
@@ -0,0 +1,260 @@
+import { resolve, normalize } from 'pathe'
+// @ts-expect-error missing types
+import TimeFixPlugin from 'time-fix-plugin'
+import WebpackBar from 'webpackbar'
+import type { Configuration } from '@rspack/core'
+import type webpack from '@rspack/core'
+import { logger } from '@nuxt/kit'
+// @ts-expect-error missing types
+import FriendlyErrorsWebpackPlugin from '@nuxt/friendly-errors-webpack-plugin'
+import escapeRegExp from 'escape-string-regexp'
+import { joinURL } from 'ufo'
+import type { NuxtOptions } from '@nuxt/schema'
+import type { WarningFilter } from '../plugins/warning-ignore'
+import WarningIgnorePlugin from '../plugins/warning-ignore'
+import type { RspackConfigContext } from '../utils/config'
+import { applyPresets, fileName } from '../utils/config'
+
+export function base (ctx: RspackConfigContext) {
+ applyPresets(ctx, [
+ baseAlias,
+ baseConfig,
+ basePlugins,
+ baseResolve,
+ baseTranspile
+ ])
+}
+
+function baseConfig (ctx: RspackConfigContext) {
+ const { options } = ctx
+
+ ctx.config = {
+ name: ctx.name,
+ entry: { app: [resolve(options.appDir, options.experimental.asyncEntry ? 'entry.async' : 'entry')] },
+ module: { rules: [] },
+ plugins: [],
+ // TODO:
+ // externals: [],
+ // optimization: {
+ // ...options.webpack.optimization,
+ // minimizer: []
+ // },
+ experiments: {},
+ mode: ctx.isDev ? 'development' : 'production',
+ cache: getCache(ctx),
+ output: getOutput(ctx),
+ stats: statsMap[ctx.nuxt.options.logLevel] ?? statsMap.info,
+ ...ctx.config
+ }
+}
+
+function basePlugins (ctx: RspackConfigContext) {
+ const { config, options, nuxt } = ctx
+
+ config.plugins = config.plugins || []
+
+ // Add timefix-plugin before other plugins
+ if (options.dev) {
+ // config.plugins.push(new TimeFixPlugin())
+ }
+
+ // User plugins
+ config.plugins.push(...(options.webpack.plugins || []))
+
+ // Ignore empty warnings
+ // config.plugins.push(new WarningIgnorePlugin(getWarningIgnoreFilter(ctx)))
+
+ // Provide env
+ config.builtins = config.builtins || {}
+ config.builtins.noEmitAssets = false
+ config.builtins.define = {
+ ...getEnv(ctx),
+ ...config.builtins.define
+ }
+
+ // Friendly errors
+ // if (ctx.isServer || (ctx.isDev && options.webpack.friendlyErrors)) {
+ // config.plugins.push(
+ // new FriendlyErrorsWebpackPlugin({
+ // clearConsole: false,
+ // reporter: 'consola',
+ // logLevel: 'ERROR' // TODO
+ // })
+ // )
+ // }
+
+ // if (nuxt.options.webpack.profile) {
+ // // Webpackbar
+ // const colors = {
+ // client: 'green',
+ // server: 'orange',
+ // modern: 'blue'
+ // }
+ // config.plugins.push(new WebpackBar({
+ // name: ctx.name,
+ // color: colors[ctx.name as keyof typeof colors],
+ // reporters: ['stats'],
+ // stats: !ctx.isDev,
+ // reporter: {
+ // // @ts-ignore
+ // change: (_, { shortPath }) => {
+ // if (!ctx.isServer) {
+ // nuxt.callHook('rspack:change', shortPath)
+ // }
+ // },
+ // // @ts-ignore
+ // done: ({ state }) => {
+ // if (state.hasErrors) {
+ // nuxt.callHook('rspack:error')
+ // } else {
+ // logger.success(`${state.name} ${state.message}`)
+ // }
+ // },
+ // allDone: () => {
+ // nuxt.callHook('rspack:done')
+ // },
+ // // @ts-ignore
+ // progress ({ statesArray }) {
+ // nuxt.callHook('rspack:progress', statesArray)
+ // }
+ // }
+ // }))
+ // }
+}
+
+function baseAlias (ctx: RspackConfigContext) {
+ const { options } = ctx
+
+ ctx.alias = {
+ '#app': options.appDir,
+ '#build/plugins': resolve(options.buildDir, 'plugins', ctx.isClient ? 'client' : 'server'),
+ '#build': options.buildDir,
+ ...options.alias,
+ ...ctx.alias
+ }
+ if (ctx.isClient) {
+ ctx.alias['#internal/nitro'] = resolve(ctx.nuxt.options.buildDir, 'nitro.client.mjs')
+ }
+}
+
+function baseResolve (ctx: RspackConfigContext) {
+ const { options, config } = ctx
+
+ // Prioritize nested node_modules in webpack search path (#2558)
+ // TODO: this might be refactored as default modulesDir?
+ const webpackModulesDir = ['node_modules'].concat(options.modulesDir)
+
+ config.resolve = {
+ extensions: ['.wasm', '.mjs', '.js', '.ts', '.json', '.vue', '.jsx', '.tsx'],
+ alias: ctx.alias,
+ modules: webpackModulesDir,
+ // fullySpecified: false,
+ ...config.resolve
+ }
+
+ // config.resolveLoader = {
+ // modules: webpackModulesDir,
+ // ...config.resolveLoader
+ // }
+}
+
+export function baseTranspile (ctx: RspackConfigContext) {
+ const { options } = ctx
+
+ const transpile = [
+ /\.vue\.js/i, // include SFCs in node_modules
+ /consola\/src/,
+ /vue-demi/
+ ]
+
+ for (let pattern of options.build.transpile) {
+ if (typeof pattern === 'function') {
+ const result = pattern(ctx)
+ if (result) { pattern = result }
+ }
+ if (typeof pattern === 'string') {
+ transpile.push(new RegExp(escapeRegExp(normalize(pattern))))
+ } else if (pattern instanceof RegExp) {
+ transpile.push(pattern)
+ }
+ }
+
+ // TODO: unique
+ ctx.transpile = [...transpile, ...ctx.transpile]
+}
+
+function getCache (ctx: RspackConfigContext): Configuration['cache'] {
+ const { options } = ctx
+
+ if (!options.dev) {
+ return false
+ }
+
+ // TODO: Disable for nuxt internal dev due to inconsistencies
+ // return {
+ // name: ctx.name,
+ // type: 'filesystem',
+ // cacheDirectory: resolve(ctx.options.rootDir, 'node_modules/.cache/webpack'),
+ // managedPaths: [
+ // ...ctx.options.modulesDir
+ // ],
+ // buildDependencies: {
+ // config: [
+ // ...ctx.options._nuxtConfigFiles
+ // ]
+ // }
+ // }
+}
+
+function getOutput (ctx: RspackConfigContext): Configuration['output'] {
+ const { options } = ctx
+
+ return {
+ path: resolve(options.buildDir, 'dist', ctx.isServer ? 'server' : joinURL('client', options.app.buildAssetsDir)),
+ filename: fileName(ctx, 'app'),
+ chunkFilename: fileName(ctx, 'chunk'),
+ publicPath: joinURL(options.app.baseURL, options.app.buildAssetsDir)
+ }
+}
+
+function getWarningIgnoreFilter (ctx: RspackConfigContext): WarningFilter {
+ const { options } = ctx
+
+ const filters: WarningFilter[] = [
+ // Hide warnings about plugins without a default export (#1179)
+ warn => warn.name === 'ModuleDependencyWarning' &&
+ warn.message.includes('export \'default\'') &&
+ warn.message.includes('nuxt_plugin_'),
+ ...(options.webpack.warningIgnoreFilters || [])
+ ]
+
+ return warn => !filters.some(ignoreFilter => ignoreFilter(warn))
+}
+
+function getEnv (ctx: RspackConfigContext) {
+ const { options } = ctx
+
+ const _env: Record = {
+ 'process.env.NODE_ENV': JSON.stringify(ctx.config.mode),
+ 'process.mode': JSON.stringify(ctx.config.mode),
+ 'process.dev': options.dev,
+ __NUXT_VERSION__: JSON.stringify(ctx.nuxt._version),
+ 'process.env.VUE_ENV': JSON.stringify(ctx.name),
+ 'process.browser': ctx.isClient,
+ 'process.client': ctx.isClient,
+ 'process.server': ctx.isServer
+ }
+
+ if (options.webpack.aggressiveCodeRemoval) {
+ _env['typeof process'] = JSON.stringify(ctx.isServer ? 'object' : 'undefined')
+ _env['typeof window'] = _env['typeof document'] = JSON.stringify(!ctx.isServer ? 'object' : 'undefined')
+ }
+
+ return _env
+}
+
+const statsMap: Record = {
+ silent: 'none',
+ info: 'normal',
+ verbose: 'verbose'
+}
diff --git a/packages/rspack/src/presets/esbuild.ts b/packages/rspack/src/presets/esbuild.ts
new file mode 100644
index 0000000000..9c7205f1b3
--- /dev/null
+++ b/packages/rspack/src/presets/esbuild.ts
@@ -0,0 +1,45 @@
+import { EsbuildPlugin } from 'esbuild-loader'
+import type { RspackConfigContext } from '../utils/config'
+
+export function esbuild (ctx: RspackConfigContext) {
+ const { config } = ctx
+
+ // https://esbuild.github.io/getting-started/#bundling-for-the-browser
+ // https://gs.statcounter.com/browser-version-market-share
+ // https://nodejs.org/en/
+ const target = ctx.isServer ? 'es2019' : 'chrome85'
+
+ // https://github.com/nuxt/nuxt/issues/13052
+ // config.optimization!.minimizer!.push(new EsbuildPlugin())
+
+ config.module!.rules!.push(
+ // {
+ // test: /\.m?[jt]s$/i,
+ // type: 'javascript/auto'
+ // // loader: 'esbuild-loader',
+ // // exclude: (file) => {
+ // // // Not exclude files outside node_modules
+ // // file = file.split('node_modules', 2)[1]
+ // // if (!file) { return false }
+
+ // // return !ctx.transpile.some(module => module.test(file))
+ // // },
+ // // resolve: {
+ // // // fullySpecified: false
+ // // },
+ // // options: {
+ // // loader: 'ts',
+ // // target
+ // // }
+ // },
+ // {
+ // test: /\.m?[jt]sx$/,
+ // // loader: 'esbuild-loader',
+ // type: 'jsx'
+ // // options: {
+ // // loader: 'tsx',
+ // // target
+ // // }
+ // }
+ )
+}
diff --git a/packages/rspack/src/presets/node.ts b/packages/rspack/src/presets/node.ts
new file mode 100644
index 0000000000..7d512bd528
--- /dev/null
+++ b/packages/rspack/src/presets/node.ts
@@ -0,0 +1,37 @@
+import type { RspackConfigContext } from '../utils/config'
+
+export function node (ctx: RspackConfigContext) {
+ const { config } = ctx
+
+ config.target = 'node'
+ config.node = false
+
+ config.experiments!.outputModule = true
+
+ config.output = {
+ ...config.output,
+ chunkFilename: '[name].mjs',
+ chunkFormat: 'module',
+ chunkLoading: 'import',
+ module: true,
+ environment: {
+ module: true,
+ arrowFunction: true,
+ bigIntLiteral: true,
+ const: true,
+ destructuring: true,
+ dynamicImport: true,
+ forOf: true
+ },
+ library: {
+ type: 'module'
+ }
+ }
+
+ config.performance = {
+ ...config.performance,
+ hints: false,
+ maxEntrypointSize: Infinity,
+ maxAssetSize: Infinity
+ }
+}
diff --git a/packages/rspack/src/presets/nuxt.ts b/packages/rspack/src/presets/nuxt.ts
new file mode 100644
index 0000000000..6fb9897a1b
--- /dev/null
+++ b/packages/rspack/src/presets/nuxt.ts
@@ -0,0 +1,20 @@
+import type { RspackConfigContext } from '../utils/config'
+import { applyPresets } from '../utils/config'
+
+import { assets } from './assets'
+import { base } from './base'
+import { esbuild } from './esbuild'
+import { pug } from './pug'
+import { style } from './style'
+import { vue } from './vue'
+
+export function nuxt (ctx: RspackConfigContext) {
+ applyPresets(ctx, [
+ base,
+ assets,
+ esbuild,
+ pug,
+ style,
+ vue
+ ])
+}
diff --git a/packages/rspack/src/presets/pug.ts b/packages/rspack/src/presets/pug.ts
new file mode 100644
index 0000000000..7cacd5893d
--- /dev/null
+++ b/packages/rspack/src/presets/pug.ts
@@ -0,0 +1,25 @@
+import type { RspackConfigContext } from '../utils/config'
+
+export function pug (ctx: RspackConfigContext) {
+ ctx.config.module!.rules!.push({
+ test: /\.pug$/i,
+ oneOf: [
+ {
+ resourceQuery: /^\?vue/i,
+ use: [{
+ loader: 'pug-plain-loader',
+ options: ctx.options.webpack.loaders.pugPlain
+ }]
+ },
+ {
+ use: [
+ 'raw-loader',
+ {
+ loader: 'pug-plain-loader',
+ options: ctx.options.webpack.loaders.pugPlain
+ }
+ ]
+ }
+ ]
+ })
+}
diff --git a/packages/rspack/src/presets/style.ts b/packages/rspack/src/presets/style.ts
new file mode 100644
index 0000000000..a1b14c2c96
--- /dev/null
+++ b/packages/rspack/src/presets/style.ts
@@ -0,0 +1,148 @@
+import MiniCssExtractPlugin from 'mini-css-extract-plugin'
+import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
+import type { RspackConfigContext } from '../utils/config'
+import { fileName, applyPresets } from '../utils/config'
+import { getPostcssConfig } from '../utils/postcss'
+
+export function style (ctx: RspackConfigContext) {
+ applyPresets(ctx, [
+ loaders,
+ extractCSS,
+ minimizer
+ ])
+}
+
+function minimizer (ctx: RspackConfigContext) {
+ const { options, config } = ctx
+
+ // if (options.webpack.optimizeCSS && Array.isArray(config.optimization!.minimizer)) {
+ // config.optimization!.minimizer.push(new CssMinimizerPlugin({
+ // ...options.webpack.optimizeCSS
+ // }))
+ // }
+}
+
+function extractCSS (ctx: RspackConfigContext) {
+ const { options, config } = ctx
+
+ // CSS extraction
+ // if (options.webpack.extractCSS) {
+ // config.plugins!.push(new MiniCssExtractPlugin({
+ // filename: fileName(ctx, 'css'),
+ // chunkFilename: fileName(ctx, 'css'),
+ // ...options.webpack.extractCSS === true ? {} : options.webpack.extractCSS
+ // }))
+ // }
+}
+
+function loaders (ctx: RspackConfigContext) {
+ const { config, options } = ctx
+
+ // CSS
+ config.module!.rules!.push(createdStyleRule('css', /\.css$/i, null, ctx))
+
+ // PostCSS
+ config.module!.rules!.push(createdStyleRule('postcss', /\.p(ost)?css$/i, null, ctx))
+
+ // Less
+ const lessLoader = { loader: 'less-loader', options: options.webpack.loaders.less }
+ config.module!.rules!.push(createdStyleRule('less', /\.less$/i, lessLoader, ctx))
+
+ // Sass (TODO: optional dependency)
+ const sassLoader = { loader: 'sass-loader', options: options.webpack.loaders.sass }
+ config.module!.rules!.push(createdStyleRule('sass', /\.sass$/i, sassLoader, ctx))
+
+ const scssLoader = { loader: 'sass-loader', options: options.webpack.loaders.scss }
+ config.module!.rules!.push(createdStyleRule('scss', /\.scss$/i, scssLoader, ctx))
+
+ // Stylus
+ const stylusLoader = { loader: 'stylus-loader', options: options.webpack.loaders.stylus }
+ config.module!.rules!.push(createdStyleRule('stylus', /\.styl(us)?$/i, stylusLoader, ctx))
+}
+
+function createdStyleRule (lang: string, test: RegExp, processorLoader: any, ctx: RspackConfigContext) {
+ const { options } = ctx
+
+ const styleLoaders = [
+ createPostcssLoadersRule(ctx),
+ processorLoader
+ ].filter(Boolean)
+
+ options.webpack.loaders.css.importLoaders =
+ options.webpack.loaders.cssModules.importLoaders =
+ styleLoaders.length
+
+ const cssLoaders = createCssLoadersRule(ctx, options.webpack.loaders.css)
+ const cssModuleLoaders = createCssLoadersRule(ctx, options.webpack.loaders.cssModules)
+
+ return {
+ test,
+ oneOf: [
+ // This matches