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