UNPKG

webpack-v5-chain

Version:
621 lines (484 loc) 18.9 kB
# webpack-v5-chain [![codecov](https://codecov.io/gh/Mister-Hope/webpack-v5-chain/graph/badge.svg?token=Zq5yZGNjjA)](https://codecov.io/gh/Mister-Hope/webpack-v5-chain) A chaining API to generate and simplify webpack 5 configurations. ## Why `webpack-v5-chain` - [`webpack-chain`](https://github.com/neutrinojs/webpack-chain) is archived and unmaintained. - [`webpack-5-chain`](https://github.com/nicholasxuu/webpack-5-chain) is a fork with some fixes, but has many API mismatches with the latest webpack and is not actively maintained. `webpack-v5-chain` keeps the API **up to date with the latest webpack**, is rewritten in TypeScript, and has **100% test coverage**. > The source uses ESM syntax, so update your require: > > ```diff > - const Config = require('webpack-chain'); > + const { Config } = require('webpack-v5-chain'); > ``` ## Introduction Webpack's core configuration is a plain JavaScript object. While fine for single projects, sharing and modifying these objects across projects requires deep knowledge of the internal structure. `webpack-v5-chain` provides a chainable API for creating and modifying webpack configurations. Named references let you standardize modifications across projects. ## Getting Started ```js // imports the webpack-v5-chain module. const { Config } = require('webpack-v5-chain'); // Instantiate the configuration with a new API const config = new Config(); // Make configuration changes using the chain API. // Every API call tracks a change to the stored configuration. config // Interact with entry points .entry('index') .add('src/index.js') .end() // Modify output settings .output .path('dist') .filename('[name].bundle.js'); // Create named rules which can be modified later config.module .rule('lint') .test(/\.js$/) .pre() .include .add('src') .end() // Even create named uses (loaders) .use('eslint') .loader('eslint-loader') .options({ rules: { semi: 'off' } }); config.module .rule('compile') .test(/\.js$/) .include .add('src') .add('test') .end() .use('babel') .loader('babel-loader') .options({ presets: [ ['@babel/preset-env', { modules: false }] ] }); // Create named plugins too! config .plugin('clean') .use(CleanPlugin, [['dist'], { root: '/dir' }]); // Export the completed configuration object to be consumed by webpack module.exports = config.toConfig(); ``` ### Shared Configurations Export the config and call `.toConfig()` before passing to webpack: ```js // webpack.core.js const { Config } = require('webpack-v5-chain'); const config = new Config(); // ...shared configuration module.exports = config; // webpack.dev.js const config = require('./webpack.core'); // ...dev-specific configuration module.exports = config.toConfig(); // webpack.prod.js const config = require('./webpack.core'); // ...prod-specific configuration module.exports = config.toConfig(); ``` ## Core Interfaces ### ChainedMap A `ChainedMap` behaves like a JavaScript `Map` with chaining conveniences. Unless stated otherwise, all methods return the instance for chaining. | Method | Description | | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `clear()` | Remove all entries. | | `delete(key)` | Remove a single entry by key. | | `get(key)``value` | Fetch the value at the given key. | | `getOrCompute(key, fn)``value` | Get the value, or compute and set it via `fn()` if missing. | | `set(key, value)` | Set a value at the given key. | | `has(key)``boolean` | Whether the map has a value at the key. | | `values()``Array` | All values in the map. | | `entries()``Object \| undefined` | All entries as an object. Returns `undefined` if empty. Keys are ordered by `.before()` / `.after()` when applicable. | | `merge(obj, omit?)` | Merge an object's properties into the map. The optional `omit` array skips specified keys. | | `batch(handler)` | Invoke `handler` with the current instance. | | `when(condition, truthy, falsy?)` | Conditionally invoke `truthy` or `falsy` handler with the current instance. | ### ChainedValueMap `ChainedValueMap` extends `ChainedMap` and is callable. Calling it as a function sets its value and clears all map entries. Conversely, setting any key clears the callable value. The call returns the instance for chaining. ```js // Callable — sets a direct value config.optimization.splitChunks(false); // ChainedMap usage config.optimization.splitChunks.set('amd', true); ``` ### ChainedSet A `ChainedSet` behaves like a JavaScript `Set` with chaining conveniences. Unless stated otherwise, all methods return the instance. | Method | Description | | --------------------------------- | --------------------------------------------------------- | | `add(value)` | Append a value. | | `prepend(value)` | Add a value to the beginning. | | `clear()` | Remove all values. | | `delete(value)` | Remove a specific value. | | `has(value)``boolean` | Whether the set contains the value. | | `values()``Array` | All values in the set. | | `merge(arr)` | Concatenate an array to the end. | | `batch(handler)` | Invoke `handler` with the current instance. | | `when(condition, truthy, falsy?)` | Conditionally invoke a handler with the current instance. | ## Shorthand Methods Many `ChainedMap` properties have shorthand methods named after the key. They are chainable and equivalent to calling `.set(key, value)`: ```js devServer.hot(true); // equivalent to: devServer.set('hot', true); ``` ## API Reference Navigate the API by chaining — use `.end()` to return to the parent context. For valid values, refer to the [webpack documentation](https://webpack.js.org/configuration/). ### Config ```js const config = new Config(); // extends ChainedMap ``` #### Shorthand methods ```js config .context(context) .mode(mode) .cache(cache) .devtool(devtool) .target(target) .watch(watch) .watchOptions(watchOptions) .externals(externals) .externalsType(externalsType) .externalsPresets(externalsPresets) .dotenv(dotenv) .node(node) .stats(stats) .experiments(experiments) .infrastructureLogging(infrastructureLogging) .amd(amd) .bail(bail) .dependencies(dependencies) .ignoreWarnings(ignoreWarnings) .loader(loader) .name(name) .parallelism(parallelism) .profile(profile) .recordsInputPath(recordsInputPath) .recordsOutputPath(recordsOutputPath) .recordsPath(recordsPath) .snapshot(snapshot) ``` #### Entry points ```js // Backed at config.entryPoints : ChainedMap config.entry(name) : ChainedSet config .entry(name) .add(value) .add(value) config .entry(name) .clear() // Using low-level config.entryPoints: config.entryPoints .get(name) .add(value) .add(value) config.entryPoints .get(name) .clear() ``` #### Output ```js config.output : ChainedMap config.output .assetModuleFilename(assetModuleFilename) .asyncChunks(asyncChunks) .auxiliaryComment(auxiliaryComment) .charset(charset) .chunkFilename(chunkFilename) .chunkFormat(chunkFormat) .chunkLoadTimeout(chunkLoadTimeout) .chunkLoadingGlobal(chunkLoadingGlobal) .chunkLoading(chunkLoading) .clean(clean) .compareBeforeEmit(compareBeforeEmit) .crossOriginLoading(crossOriginLoading) .cssChunkFilename(cssChunkFilename) .cssFilename(cssFilename) .devtoolFallbackModuleFilenameTemplate(devtoolFallbackModuleFilenameTemplate) .devtoolModuleFilenameTemplate(devtoolModuleFilenameTemplate) .devtoolNamespace(devtoolNamespace) .enabledChunkLoadingTypes(enabledChunkLoadingTypes) .enabledLibraryTypes(enabledLibraryTypes) .enabledWasmLoadingTypes(enabledWasmLoadingTypes) .environment(environment) .filename(filename) .globalObject(globalObject) .hashDigest(hashDigest) .hashDigestLength(hashDigestLength) .hashFunction(hashFunction) .hashSalt(hashSalt) .hotUpdateChunkFilename(hotUpdateChunkFilename) .hotUpdateGlobal(hotUpdateGlobal) .hotUpdateMainFilename(hotUpdateMainFilename) .html(html) .htmlChunkFilename(htmlChunkFilename) .htmlFilename(htmlFilename) .iife(iife) .ignoreBrowserWarnings(ignoreBrowserWarnings) .importFunctionName(importFunctionName) .importMetaName(importMetaName) .library(library) .libraryExport(libraryExport) .libraryTarget(libraryTarget) .module(module) .path(path) .pathinfo(pathinfo) .publicPath(publicPath) .scriptType(scriptType) .sourceMapFilename(sourceMapFilename) .sourcePrefix(sourcePrefix) .strictModuleErrorHandling(strictModuleErrorHandling) .strictModuleExceptionHandling(strictModuleExceptionHandling) .strictModuleResolution(strictModuleResolution) .trustedTypes(trustedTypes) .umdNamedDefine(umdNamedDefine) .uniqueName(uniqueName) .wasmLoading(wasmLoading) .webassemblyModuleFilename(webassemblyModuleFilename) .workerChunkFilename(workerChunkFilename) .workerChunkLoading(workerChunkLoading) .workerPublicPath(workerPublicPath) .workerWasmLoading(workerWasmLoading) ``` #### Resolve `config.resolve : ChainedMap` Shorthand: `.cache .cachePredicate .cacheWithContext .enforceExtension .fullySpecified .preferAbsolute .preferRelative .symlinks .tsconfig .unsafeCache .useSyncFileSystemCalls` **ChainedSet properties:** `.aliasFields .byDependency .conditionNames .descriptionFields .exportsFields .extensionAlias .extensions .importsFields .mainFields .mainFiles .modules .restrictions .roots` **ChainedMap properties:** `.alias .fallback` ```js config.resolve.alias.set(key, value); config.resolve.extensions.add('.js').prepend('.ts'); config.resolve.fallback.set('path', 'path-browserify'); ``` #### Resolve plugins ```js config.resolve.plugin(name) → Plugin // Adding — do not use `new` config.resolve.plugin(name).use(Plugin, args); config.resolve.plugin(name).use(require.resolve('plugin-path'), args); // Modify arguments config.resolve.plugin(name).tap(args => newArgs); // Modify instantiation config.resolve.plugin(name).init((Plugin, args) => new Plugin(...args)); // Remove config.resolve.plugins.delete(name); // Ordering config.resolve.plugin(name).before(otherName); config.resolve.plugin(name).after(otherName); ``` #### ResolveLoader Identical to `config.resolve` with additional **ChainedSet** properties: `.modules .moduleExtensions .packageMains` #### Performance `config.performance : ChainedValueMap` ```js config.performance(false); // callable — disables performance hints config.performance .assetFilter(fn) .hints('warning') .maxEntrypointSize(250000) .maxAssetSize(250000); ``` #### Optimization `config.optimization : ChainedMap` Shorthand: `.checkWasmTypes .chunkIds .concatenateModules .emitOnErrors .avoidEntryIife .flagIncludedChunks .inlineExports .innerGraph .mangleExports .mangleWasmImports .mergeDuplicateChunks .minimize .moduleIds .nodeEnv .portableRecords .providedExports .realContentHash .removeAvailableModules .removeEmptyChunks .runtimeChunk .sideEffects .usedExports` **SplitChunks:** `config.optimization.splitChunks : ChainedValueMap` ```js config.optimization.splitChunks({ chunks: 'all' }); config.optimization.splitChunks.set('minSize', 10000); ``` **Minimizers:** ```js config.optimization.minimizer(name) → Plugin config.optimization.minimizer(name).use(Plugin, args); config.optimization.minimizer(name).tap(args => newArgs); config.optimization.minimizer(name).init((Plugin, args) => new Plugin(...args)); config.optimization.minimizers.delete(name); ``` #### Plugins ```js config.plugin(name) → Plugin config.plugin(name).use(Plugin, args); config.plugin(name).use(require.resolve('plugin-path'), args); config.plugin(name).tap(args => newArgs); config.plugin(name).init((Plugin, args) => new Plugin(...args)); config.plugins.delete(name); config.plugin(name).before(otherName); // cannot also use .after() config.plugin(name).after(otherName); // cannot also use .before() ``` #### DevServer `config.devServer : ChainedMap` Shorthand: `.after .app .bonjour .client .compress .devMiddleware .headers .historyApiFallback .host .hot .ipc .liveReload .onListening .open .port .server .setupExitSignals .setupMiddlewares .static .watchFiles .webSocketServer` **ChainedSet:** `.allowedHosts` #### Module `config.module : ChainedMap` Shorthand: `.noParse .unsafeCache .exprContextCritical .exprContextRecursive .exprContextRegExp .exprContextRequest .unknownContextCritical .unknownContextRecursive .unknownContextRegExp .unknownContextRequest .wrappedContextCritical .wrappedContextRecursive .wrappedContextRegExp .strictExportPresence .strictThisContextOnImports` #### Module rules ```js config.module.rule(name) → Rule config.module.rule(name) .test(/\.js$/) .pre() // enforce: 'pre' .post() // enforce: 'post' .enforce(value) // enforce: value // Nested rules config.module.rule(name).rule(name) → Rule config.module.rule(name).rule(name).before(otherName); config.module.rule(name).rule(name).after(otherName); // OneOf rules config.module.rule(name).oneOf(name) → Rule config.module.rule(name).oneOf(name).before(otherName); config.module.rule(name).oneOf(name).after(otherName); // Resolve override (webpack ≥ 4.36.1) config.module.rule(name).resolve.symlinks(true); ``` #### Rule uses (loaders) ```js config.module.rule(name).use(name) → Use config.module.rule(name).use(name) .loader('babel-loader') .options({ presets: ['@babel/preset-env'] }); // Modify options config.module.rule(name).use(name).tap(options => mergedOptions); ``` ## Merging Config Merge an object matching the chain schema layout. Note: this schema uses named keys (not webpack's raw config format). ```js config.merge({ devtool: 'source-map' }); config.get('devtool'); // "source-map" ``` <details> <summary>Full merge schema</summary> ```js config.merge({ amd, bail, cache, context, devtool, externals, loader, mode, parallelism, profile, recordsPath, recordsInputPath, recordsOutputPath, stats, target, watch, watchOptions, node: {}, entry: { name: [...] }, plugin: { name: { plugin, args, before, after } }, devServer: { /* shorthand keys */ }, optimization: { /* shorthand keys */, minimizer: { name: { plugin, args, before, after } }, splitChunks: {} }, performance: { hints, maxEntrypointSize, maxAssetSize, assetFilter }, resolve: { /* shorthand keys */, alias: { key: value }, aliasFields: [...], byDependency: [...], conditionNames: [...], descriptionFields: [...], exportsFields: [...], extensionAlias: [...], extensions: [...], fallback: {}, importsFields: [...], mainFields: [...], mainFiles: [...], modules: [...], restrictions: [...], roots: [...], plugin: { name: { plugin, args, before, after } } }, resolveLoader: { /* same as resolve */, moduleExtensions: [...], packageMains: [...] }, module: { /* shorthand keys */, rule: { name: { test, enforce, issuer, parser, resource, resourceQuery, dependency, descriptionData, loader, options, phase, realResource, resourceFragment, include: [...], exclude: [...], rules: { name: Rule }, oneOf: { name: Rule }, use: { name: { loader, options, before, after } } } } } }) ``` </details> ## Conditional Configuration Use `.when()` on any `ChainedMap` or `ChainedSet`: ```js config.when( process.env.NODE_ENV === 'production', config => config.plugin('minify').use(BabiliWebpackPlugin), config => config.devtool('source-map') ); ``` ## Inspecting Config Use `config.toString()` to generate a commented, stringified config: ```js config.module .rule('compile') .test(/\.js$/) .use('babel') .loader('babel-loader'); config.toString(); { module: { rules: [ /* config.module.rule('compile') */ { test: /\.js$/, use: [ /* config.module.rule('compile').use('babel') */ { loader: 'babel-loader' } ] } ] } } ``` ### Stringifying objects & plugins By default the output is not directly usable as webpack config. Set a `__expression` property on objects to control how they are stringified: ```js const sass = require('sass'); sass.__expression = `require('sass')`; class MyPlugin {} MyPlugin.__expression = `require('my-plugin')`; function myFunction() {} myFunction.__expression = `require('my-function')`; config.plugin('example').use(MyPlugin, [{ fn: myFunction, implementation: sass }]); config.toString(); /* { plugins: [ new (require('my-plugin'))({ fn: require('my-function'), implementation: require('sass') }) ] } */ ``` Plugins specified by path auto-generate `require()`: ```js config.plugin('env').use(require.resolve('webpack/lib/ProvidePlugin'), [{ jQuery: 'jquery' }]); config.toString(); /* { plugins: [ new (require('/foo/bar/node_modules/webpack/lib/EnvironmentPlugin.js'))({ jQuery: 'jquery' }) ] } */ ``` ### Static `Config.toString()` Call `Config.toString()` statically to modify the config before stringifying: ```js Config.toString({ ...config.toConfig(), module: { defaultRules: [{ use: [{ loader: 'banner-loader', options: { prefix: 'banner-prefix.txt' } }] }] } }) ```