import pify from 'pify' import type { NodeMiddleware } from 'h3' import { resolve } from 'pathe' import { defineEventHandler, fromNodeMiddleware } from 'h3' import type { MultiWatching } 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, useNitro, useNuxt } from '@nuxt/kit' import type { InputPluginOption } from 'rollup' 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 { client, server } from './configs' import { applyPresets, createWebpackConfigContext, getWebpackConfig } from './utils/config' import { dynamicRequire } from './nitro/plugins/dynamic-require' import { builder, webpack } from '#builder' // TODO: Support plugins // const plugins: string[] = [] export const bundle: NuxtBuilder['bundle'] = async (nuxt) => { const webpackConfigs = await Promise.all([client, ...nuxt.options.ssr ? [server] : []].map(async (preset) => { const ctx = createWebpackConfigContext(nuxt) ctx.userConfig = defu(nuxt.options.webpack[`$${preset.name as 'client' | 'server'}`], ctx.userConfig) await applyPresets(ctx, preset) return getWebpackConfig(ctx) })) /** Inject rollup plugin for Nitro to handle dynamic imports from webpack chunks */ if (!nuxt.options.dev) { const nitro = useNitro() const dynamicRequirePlugin = dynamicRequire({ dir: resolve(nuxt.options.buildDir, 'dist/server'), inline: nitro.options.node === false || nitro.options.inlineDynamicImports, ignore: [ 'client.manifest.mjs', 'server.js', 'server.cjs', 'server.mjs', 'server.manifest.mjs', ], }) const prerenderRollupPlugins = nitro.options._config.rollupConfig!.plugins as InputPluginOption[] const rollupPlugins = nitro.options.rollupConfig!.plugins as InputPluginOption[] prerenderRollupPlugins.push(dynamicRequirePlugin) rollupPlugins.push(dynamicRequirePlugin) } await nuxt.callHook(`${builder}: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 && nuxt.options.builder !== '@nuxt/rspack-builder') { 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(`${builder}: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 Compiler['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(`${builder}:compile`, { name: compiler.options.name!, compiler }) // Load renderer resources after build compiler.hooks.done.tap('load-resources', async (stats) => { await nuxt.callHook(`${builder}:compiled`, { name: compiler.options.name!, compiler, stats }) }) // --- Dev Build --- if (nuxt.options.dev) { const compilersWatching: Array = [] 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 } }