import pify from 'pify' import webpack from 'webpack' import type { NodeMiddleware } from 'h3' import { defineEventHandler, fromNodeMiddleware } from 'h3' import type { OutputFileSystem } from 'webpack-dev-middleware' import webpackDevMiddleware from 'webpack-dev-middleware' import webpackHotMiddleware from 'webpack-hot-middleware' import type { Compiler, Stats, Watching } from 'webpack' import { defu } from 'defu' import type { NuxtBuilder } from '@nuxt/schema' import { joinURL } from 'ufo' import { logger, useNuxt } from '@nuxt/kit' 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 { applyPresets, createWebpackConfigContext, getWebpackConfig } from './utils/config' // TODO: Support plugins // const plugins: string[] = [] export const bundle: NuxtBuilder['bundle'] = async (nuxt) => { registerVirtualModules() const webpackConfigs = [client, ...nuxt.options.ssr ? [server] : []].map((preset) => { const ctx = createWebpackConfigContext(nuxt) ctx.userConfig = defu(nuxt.options.webpack[`$${preset.name as 'client' | 'server'}`], ctx.userConfig) applyPresets(ctx, preset) return getWebpackConfig(ctx) }) await nuxt.callHook('webpack:config', webpackConfigs) // Initialize shared MFS for dev const mfs = nuxt.options.dev ? createMFS() : null for (const config of webpackConfigs) { config.plugins!.push(DynamicBasePlugin.webpack({ sourcemap: !!nuxt.options.sourcemap[config.name as 'client' | 'server'] })) // 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.webpack({ sourcemap: !!nuxt.options.sourcemap[config.name as 'client' | 'server'], rootDir: nuxt.options.rootDir, composables: nuxt.options.optimization.keyedComposables })) } await nuxt.callHook('webpack:configResolved', webpackConfigs) // Configure compilers const compilers = webpackConfigs.map((config) => { // Create compiler const compiler = webpack(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(resolve => compiler.close(resolve)) } }) // Start Builds if (nuxt.options.dev) { await Promise.all(compilers.map(c => compile(c))) return } 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 = webpackDevMiddleware(compiler, { publicPath: joinURL(nuxt.options.app.baseURL, nuxt.options.app.buildAssetsDir), outputFileSystem: compiler.outputFileSystem as any, stats: 'none', ...nuxt.options.webpack.devMiddleware }) // @ts-expect-error need better types for `pify` 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(devMiddleware as NodeMiddleware) 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() await nuxt.callHook('webpack:compile', { name: compiler.options.name!, compiler }) // Load renderer resources after build compiler.hooks.done.tap('load-resources', async (stats) => { await nuxt.callHook('webpack:compiled', { name: compiler.options.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 (compiler.options.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) => { if (devMiddleware.context.watching) { 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((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 } }