feat(rspack): initial commit (poc)

This commit is contained in:
Daniel Roe 2023-03-21 09:54:29 +00:00
parent 1c323a8810
commit df5162d694
32 changed files with 3123 additions and 23 deletions

View File

@ -0,0 +1,13 @@
<template>
<div>Nuxt 3 rspack</div>
</template>
<script setup lang="ts">
console.log('hey there!')
</script>
<!-- <style>
:root {
color: blue;
}
</style> -->

View File

@ -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'

View File

@ -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')
}

View File

@ -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'
]
})

View File

@ -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"
}
}

View File

@ -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
}))
}
}
}

View File

@ -0,0 +1,2 @@
export { client } from './client'
export { server } from './server'

View File

@ -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
}))
}
}

View File

@ -0,0 +1 @@
export * from './rspack'

View File

@ -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)
})
}

View File

@ -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
)
)
}
}

View File

@ -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
}
}
}
})

View File

@ -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<string, string[]> = {}
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<index: number> */ } as Record<string, number[]>,
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
// }
})
}
}

View File

@ -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<VueSSRServerPluginOptions> = {}) {
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<string, string>,
maps: {} as Record<string, string>
}
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()
})
})
}
}

View File

@ -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')

View File

@ -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)
})
}
}

View File

@ -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'
}
)
}

View File

@ -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<string, string | boolean> = {
'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<NuxtOptions['logLevel'], Configuration['stats']> = {
silent: 'none',
info: 'normal',
verbose: 'verbose'
}

View File

@ -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
// // }
// }
)
}

View File

@ -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
}
}

View File

@ -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
])
}

View File

@ -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
}
]
}
]
})
}

View File

@ -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 <style module>
{
resourceQuery: /module/,
use: cssModuleLoaders.concat(styleLoaders)
},
// This matches plain <style> or <style scoped>
{
use: cssLoaders.concat(styleLoaders)
}
]
}
}
function createCssLoadersRule (ctx: RspackConfigContext, cssLoaderOptions: any) {
// const { options } = ctx
// const cssLoader = { loader: 'css-loader', options: cssLoaderOptions }
// if (options.webpack.extractCSS) {
// if (ctx.isServer) {
// // https://webpack.js.org/loaders/css-loader/#exportonlylocals
// if (cssLoader.options.modules) {
// cssLoader.options.modules.exportOnlyLocals = cssLoader.options.modules.exportOnlyLocals ?? true
// }
// return [cssLoader]
// }
// return [
// {
// loader: MiniCssExtractPlugin.loader
// },
// cssLoader
// ]
// }
return [
// https://github.com/vuejs/vue-style-loader/issues/56
// {
// loader: 'vue-style-loader',
// options: options.webpack.loaders.vueStyle
// },
// {
// test: /\.css$/i,
// type: 'css'
// },
// {
// test: /\.module\.css$/i,
// type: 'css/module' // this is enabled by default for module.css, so you don't need to specify it
// }
]
}
function createPostcssLoadersRule (ctx: RspackConfigContext) {
const { options, nuxt } = ctx
if (!options.postcss) { return }
const config = getPostcssConfig(nuxt)
if (!config) {
return
}
return {
loader: 'postcss-loader',
options: config
}
}

View File

@ -0,0 +1,42 @@
import { resolve } from 'pathe'
import VueLoaderPlugin from 'vue-loader/dist/pluginWebpack5.js'
import VueSSRClientPlugin from '../plugins/vue/client'
import VueSSRServerPlugin from '../plugins/vue/server'
import type { RspackConfigContext } from '../utils/config'
export function vue (ctx: RspackConfigContext) {
const { options, config } = ctx
// @ts-ignore
// config.plugins.push(new (VueLoaderPlugin.default || VueLoaderPlugin)())
config.module!.rules!.push({
test: /\.vue$/i,
use: ['/Users/daniel/code/nuxt.js/packages/rspack/src/loaders/vue.cjs']
// options: {
// reactivityTransform: ctx.nuxt.options.experimental.reactivityTransform,
// ...options.webpack.loaders.vue
// }
})
if (ctx.isClient) {
config.plugins!.push(new VueSSRClientPlugin({
filename: resolve(options.buildDir, 'dist/server', `${ctx.name}.manifest.json`),
nuxt: ctx.nuxt
}))
} else {
config.plugins!.push(new VueSSRServerPlugin({
filename: `${ctx.name}.manifest.json`
}))
}
// Feature flags
// https://github.com/vuejs/vue-next/tree/master/packages/vue#bundler-build-feature-flags
// TODO: Provide options to toggle
config.builtins = config.builtins || {}
config.builtins.define = {
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false',
...config.builtins.define
}
}

View File

@ -0,0 +1,176 @@
import pify from 'pify'
import { createCompiler } from '@rspack/core'
import type { NodeMiddleware } from 'h3'
import { fromNodeMiddleware, defineEventHandler, useBase } from 'h3'
import type { OutputFileSystem } from '@rspack/dev-middleware'
import rspackDevMiddleware, { getRspackMemoryAssets } from '@rspack/dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import type { Compiler, Watching } from '@rspack/core'
import type { Nuxt } from '@nuxt/schema'
import { joinURL } from 'ufo'
import { logger, useNuxt } from '@nuxt/kit'
import { createUnplugin } from 'unplugin'
import { composableKeysPlugin } from '../../vite/src/plugins/composable-keys'
import { DynamicBasePlugin } from './plugins/dynamic-base'
import { ChunkErrorPlugin } from './plugins/chunk'
import { createMFS } from './utils/mfs'
import { registerVirtualModules } from './virtual-modules'
import { client, server } from './configs'
import { createRspackConfigContext, applyPresets, getRspackConfig } from './utils/config'
// TODO: Support plugins
// const plugins: string[] = []
export async function bundle (nuxt: Nuxt) {
// TODO: remove when we have support for virtual modules in rspack
nuxt.hook('app:templates', (app) => {
for (const template of app.templates) {
template.write = true
}
})
const webpackConfigs = [client, ...nuxt.options.ssr ? [server] : []].map((preset) => {
const ctx = createRspackConfigContext(nuxt)
applyPresets(ctx, preset)
return getRspackConfig(ctx)
})
await nuxt.callHook('rspack:config', webpackConfigs)
// console.log(webpackConfigs[0])
// // Initialize shared MFS for dev
const mfs = nuxt.options.dev ? createMFS() : null
// Configure compilers
const compilers = webpackConfigs.map((config) => {
// TODO: need support for runtime __webpack_public_path__
// config.plugins!.push(DynamicBasePlugin.rspack({
// sourcemap: nuxt.options.sourcemap[config.name as 'client' | 'server']
// }))
// TODO: Emit chunk errors if the user has opted in to `experimental.emitRouteChunkError`
// if (config.name === 'client' && nuxt.options.experimental.emitRouteChunkError) {
// config.plugins!.push(new ChunkErrorPlugin())
// }
// config.plugins!.push(composableKeysPlugin.rspack({
// sourcemap: nuxt.options.sourcemap[config.name as 'client' | 'server'],
// rootDir: nuxt.options.rootDir,
// composables: nuxt.options.optimization.keyedComposables
// }))
// Create compiler
const compiler = createCompiler(config)
// In dev, write files in memory FS
if (nuxt.options.dev) {
compiler.outputFileSystem = mfs as unknown as OutputFileSystem
}
return compiler
})
nuxt.hook('close', async () => {
for (const compiler of compilers) {
await new Promise<void>(resolve => compiler.close(resolve))
}
})
// Start Builds
if (nuxt.options.dev) {
return Promise.all(compilers.map(c => compile(c)))
}
for (const c of compilers) {
await compile(c)
}
}
async function createDevMiddleware (compiler: Compiler) {
const nuxt = useNuxt()
logger.debug('Creating webpack middleware...')
// Create webpack dev middleware
const devMiddleware = rspackDevMiddleware(compiler as any, {
publicPath: joinURL(nuxt.options.app.baseURL, nuxt.options.app.buildAssetsDir),
outputFileSystem: compiler.outputFileSystem as any,
stats: 'none',
serverSideRender: true,
...nuxt.options.webpack.devMiddleware
})
// @ts-ignore
nuxt.hook('close', () => pify(devMiddleware.close.bind(devMiddleware))())
// const { client: _client, ...hotMiddlewareOptions } = nuxt.options.webpack.hotMiddleware || {}
// const hotMiddleware = webpackHotMiddleware(compiler, {
// log: false,
// heartbeat: 10000,
// path: joinURL(nuxt.options.app.baseURL, '__webpack_hmr', compiler.options.name!),
// ...hotMiddlewareOptions
// })
// Register devMiddleware on server
const devHandler = fromNodeMiddleware(getRspackMemoryAssets(compiler, devMiddleware))
// const hotHandler = fromNodeMiddleware(hotMiddleware as NodeMiddleware)
await nuxt.callHook('server:devHandler', defineEventHandler(async (event) => {
await devHandler(event)
// await hotHandler(event)
}))
return devMiddleware
}
async function compile (compiler: Compiler) {
const nuxt = useNuxt()
const { name } = compiler.options
await nuxt.callHook('rspack:compile', { name: name!, compiler })
// Load renderer resources after build
compiler.hooks.done.tap('load-resources', async (stats) => {
await nuxt.callHook('rspack:compiled', { name: name!, compiler, stats })
})
// --- Dev Build ---
if (nuxt.options.dev) {
const compilersWatching: Watching[] = []
nuxt.hook('close', async () => {
await Promise.all(compilersWatching.map(watching => pify(watching.close.bind(watching))()))
})
// Client build
if (name === 'client') {
return new Promise((resolve, reject) => {
compiler.hooks.done.tap('nuxt-dev', () => { resolve(null) })
compiler.hooks.failed.tap('nuxt-errorlog', (err) => { reject(err) })
// Start watch
createDevMiddleware(compiler).then((devMiddleware) => {
compilersWatching.push(devMiddleware.context.watching)
})
})
}
// Server, build and watch for changes
return new Promise((resolve, reject) => {
const watching = compiler.watch(nuxt.options.watchers.webpack, (err) => {
if (err) { return reject(err) }
resolve(null)
})
compilersWatching.push(watching)
})
}
// --- Production Build ---
const stats = await new Promise<webpack.Stats>((resolve, reject) => compiler.run((err, stats) => err ? reject(err) : resolve(stats!)))
if (stats.hasErrors()) {
const error = new Error('Nuxt build error')
error.stack = stats.toString('errors-only')
throw error
}
}

View File

@ -0,0 +1,87 @@
import { cloneDeep } from 'lodash-es'
import type { Configuration } from '@rspack/core'
import type { Nuxt } from '@nuxt/schema'
import { logger } from '@nuxt/kit'
export interface RspackConfigContext extends ReturnType<typeof createRspackConfigContext>{ }
type RspackConfigPreset = (ctx: RspackConfigContext, options?: object) => void
type RspackConfigPresetItem = RspackConfigPreset | [RspackConfigPreset, any]
export function createRspackConfigContext (nuxt: Nuxt) {
return {
nuxt,
options: nuxt.options,
config: {} as Configuration,
name: 'base',
isDev: nuxt.options.dev,
isServer: false,
isClient: false,
alias: {} as { [index: string]: string | false | string[] },
transpile: [] as RegExp[]
}
}
export function applyPresets (ctx: RspackConfigContext, presets: RspackConfigPresetItem | RspackConfigPresetItem[]) {
if (!Array.isArray(presets)) {
presets = [presets]
}
for (const preset of presets) {
if (Array.isArray(preset)) {
preset[0](ctx, preset[1])
} else {
preset(ctx)
}
}
}
export function fileName (ctx: RspackConfigContext, key: string) {
const { options } = ctx
let fileName = options.webpack.filenames[key as keyof typeof options.webpack.filenames] as ((ctx: RspackConfigContext) => string) | string
if (typeof fileName === 'function') {
fileName = fileName(ctx)
}
if (typeof fileName === 'string' && options.dev) {
const hash = /\[(chunkhash|contenthash|hash)(?::(\d+))?]/.exec(fileName)
if (hash) {
logger.warn(`Notice: Please do not use ${hash[1]} in dev mode to prevent memory leak`)
}
}
return fileName
}
export function getRspackConfig (ctx: RspackConfigContext): Configuration {
const { options, config } = ctx
// TODO
const builder = {}
const loaders: any[] = []
// @ts-ignore
const { extend } = options.build
if (typeof extend === 'function') {
const extendedConfig = extend.call(
builder,
config,
{ loaders, ...ctx }
) || config
const pragma = /@|#/
const { devtool } = extendedConfig
if (typeof devtool === 'string' && pragma.test(devtool)) {
extendedConfig.devtool = devtool.replace(pragma, '')
logger.warn(`devtool has been normalized to ${extendedConfig.devtool} as webpack documented value`)
}
return extendedConfig
}
// Clone deep avoid leaking config between Client and Server
return cloneDeep(config)
}

View File

@ -0,0 +1,24 @@
import { join } from 'pathe'
import pify from 'pify'
import { Volume, createFsFromVolume } from 'memfs'
import type { IFs } from 'memfs'
export function createMFS () {
// Create a new volume
const fs = createFsFromVolume(new Volume())
// Clone to extend
const _fs: IFs & { join?(...paths: string[]): string } = { ...fs } as any
// fs.join method is (still) expected by @rspack/dev-middleware
// There might be differences with https://github.com/webpack/memory-fs/blob/master/lib/join.js
_fs.join = join
// Used by vue-renderer
_fs.exists = p => Promise.resolve(_fs.existsSync(p))
// @ts-ignore
_fs.readFile = pify(_fs.readFile)
return _fs as IFs & { join?(...paths: string[]): string }
}

View File

@ -0,0 +1,84 @@
import { createCommonJS } from 'mlly'
import { defaults, merge, cloneDeep } from 'lodash-es'
import { requireModule } from '@nuxt/kit'
import type { Nuxt } from '@nuxt/schema'
const isPureObject = (obj: unknown): obj is Object => obj !== null && !Array.isArray(obj) && typeof obj === 'object'
export const orderPresets = {
cssnanoLast (names: string[]) {
const nanoIndex = names.indexOf('cssnano')
if (nanoIndex !== names.length - 1) {
names.push(names.splice(nanoIndex, 1)[0])
}
return names
},
autoprefixerLast (names: string[]) {
const nanoIndex = names.indexOf('autoprefixer')
if (nanoIndex !== names.length - 1) {
names.push(names.splice(nanoIndex, 1)[0])
}
return names
},
autoprefixerAndCssnanoLast (names: string[]) {
return orderPresets.cssnanoLast(orderPresets.autoprefixerLast(names))
}
}
export const getPostcssConfig = (nuxt: Nuxt) => {
function defaultConfig () {
return {
sourceMap: nuxt.options.webpack.cssSourceMap,
plugins: nuxt.options.postcss.plugins,
// Array, String or Function
order: 'autoprefixerAndCssnanoLast'
}
}
function sortPlugins ({ plugins, order }: any) {
const names = Object.keys(plugins)
if (typeof order === 'string') {
order = orderPresets[order as keyof typeof orderPresets]
}
return typeof order === 'function' ? order(names, orderPresets) : (order || names)
}
function loadPlugins (config: any) {
if (!isPureObject(config.plugins)) { return }
// Map postcss plugins into instances on object mode once
const cjs = createCommonJS(import.meta.url)
config.plugins = sortPlugins(config).map((pluginName: string) => {
const pluginFn = requireModule(pluginName, { paths: [cjs.__dirname] })
const pluginOptions = config.plugins[pluginName]
if (!pluginOptions || typeof pluginFn !== 'function') { return null }
return pluginFn(pluginOptions)
}).filter(Boolean)
}
if (!nuxt.options.webpack.postcss || !nuxt.options.postcss) {
return false
}
let postcssOptions = cloneDeep(nuxt.options.postcss)
// Apply default plugins
if (isPureObject(postcssOptions)) {
if (Array.isArray(postcssOptions.plugins)) {
defaults(postcssOptions, defaultConfig())
} else {
// Keep the order of default plugins
postcssOptions = merge({}, defaultConfig(), postcssOptions)
loadPlugins(postcssOptions)
}
// @ts-expect-error
delete nuxt.options.webpack.postcss.order
return {
// @ts-expect-error
sourceMap: nuxt.options.webpack.cssSourceMap,
...nuxt.options.webpack.postcss,
postcssOptions
}
}
}

View File

@ -0,0 +1,28 @@
import { useNuxt } from '@nuxt/kit'
import VirtualModulesPlugin from 'webpack-virtual-modules'
export function registerVirtualModules () {
const nuxt = useNuxt()
// Initialize virtual modules instance
const virtualModules = new VirtualModulesPlugin(nuxt.vfs)
const writeFiles = () => {
console.log('writing files')
for (const filePath in nuxt.vfs) {
virtualModules.writeModule(filePath, nuxt.vfs[filePath])
}
}
// Workaround to initialize virtual modules
nuxt.hook('rspack:compile', ({ compiler }) => {
writeFiles()
if (compiler.name === 'server') { writeFiles() }
})
// Update virtual modules when templates are updated
nuxt.hook('app:templatesGenerated', writeFiles)
nuxt.hook('rspack:config', configs => configs.forEach((config) => {
// Support virtual modules (input)
config.plugins!.push(virtualModules)
}))
}

View File

@ -34,7 +34,6 @@ export default defineBuildConfig({
'mini-css-extract-plugin',
'terser-webpack-plugin',
'css-minimizer-webpack-plugin',
'webpack-dev-middleware',
'h3',
'webpack-hot-middleware',
'postcss',

View File

@ -1,3 +1,10 @@
export default defineNuxtConfig({
builder: 'rspack'
// TODO: Cannot create property 'global' on boolean 'false'
ssr: false,
builder: 'rspack',
app: {
head: {
link: [{ rel: 'stylesheet', href: 'https://unpkg.com/@picocss/pico@1.*/css/pico.min.css' }]
}
}
})

File diff suppressed because it is too large Load Diff