weldable
Version:
A webpack configuration tool.
432 lines (416 loc) • 12.5 kB
JavaScript
const fs = require('fs');
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlReplaceWebpackPlugin = require('html-replace-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const SvgToMiniDataURI = require('mini-svg-data-uri');
const TerserJSPlugin = require('terser-webpack-plugin');
const { babelLoaderResolve, babelPresetEnvResolve, cssLoaderResolve, tsLoaderResolve } = require('../lib/packages');
const { jsFileExtensions, OPTIONS, tsFileExtensions } = require('./global');
const { consoleMessage } = require('./logger');
const { setupWebpackDotenvFilesForEnv } = require('./dotenv');
/**
* @module webpackConfigs
*/
/**
* Assumption based preprocess loader for JS
*
* @param {object} dotenv
* @param {string} dotenv._BUILD_SRC_DIR
* @param {object} settings
* @param {Array<string>} settings.jsFileExtensions
* @returns {{module: {rules: Array}}}
*/
const preprocessLoaderJs = (
{ _BUILD_SRC_DIR: SRC_DIR = '' } = OPTIONS.dotenv || {},
{ jsFileExtensions: aliasJsFileExtensions = jsFileExtensions } = {}
) => ({
module: {
rules: [
{
test: new RegExp(`\\.(${aliasJsFileExtensions.join('|')})?$`),
include: [SRC_DIR],
resolve: {
// Dependent on loader resolutions this may, or may not, be necessary
extensions: aliasJsFileExtensions.map(ext => `.${ext}`)
},
use: [
{
loader: babelLoaderResolve,
options: {
presets: [babelPresetEnvResolve]
}
}
]
}
]
}
});
/**
* Assumption based preprocess loader for Typescript
*
* @param {object} dotenv
* @param {string} dotenv._BUILD_SRC_DIR
* @param {object} settings
* @param {Array<string>} settings.jsFileExtensions
* @param {Array<string>} settings.tsFileExtensions
* @returns {{module: {rules: Array}}}
*/
const preprocessLoaderTs = (
{ _BUILD_SRC_DIR: SRC_DIR = '' } = OPTIONS.dotenv || {},
{
jsFileExtensions: aliasJsFileExtensions = jsFileExtensions,
tsFileExtensions: aliasTsFileExtensions = tsFileExtensions
} = {}
) => ({
module: {
rules: [
{
test: new RegExp(`\\.(${[...aliasTsFileExtensions, ...aliasJsFileExtensions].join('|')})?$`),
include: [SRC_DIR],
resolve: {
// Dependent on loader resolutions this may, or may not, be necessary
extensions: [...aliasTsFileExtensions, ...aliasJsFileExtensions].map(ext => `.${ext}`)
},
use: [
{
loader: tsLoaderResolve
}
]
}
]
}
});
/**
* Assumption based preprocess loader for none
*
* @returns {{module: {rules: Array}}}
*/
const preprocessLoaderNone = () => ({
module: {
rules: []
}
});
/**
* Assumption based preprocess loader
*
* @param {object} options
* @param {string} options.loader
* @returns {{module: {rules: Array}}}
*/
const preprocessLoader = ({ loader } = OPTIONS) => {
switch (loader) {
case 'js':
return preprocessLoaderJs();
case 'ts':
return preprocessLoaderTs();
default:
return preprocessLoaderNone();
}
};
/**
* Common webpack settings between environments.
*
* @param {object} dotenv
* @param {string} dotenv._BUILD_APP_INDEX_PREFIX
* @param {string} dotenv._BUILD_DIST_DIR
* @param {string} dotenv._BUILD_HTML_INDEX_DIR
* @param {string} dotenv._BUILD_PUBLIC_PATH
* @param {string} dotenv._BUILD_RELATIVE_DIRNAME
* @param {string} dotenv._BUILD_SRC_DIR
* @param {string} dotenv._BUILD_STATIC_DIR
* @param {string} dotenv._BUILD_UI_NAME
* @param {object} settings
* @param {object} settings.consoleMessage
* @param {Array<string>} settings.jsFileExtensions
* @param {Function} settings.setupWebpackDotenvFilesForEnv
* @param {Array<string>} settings.tsFileExtensions
* @returns {{output: {path: string, filename: string, publicPath: string, clean: boolean}, entry: {app: string},
* resolve: {cacheWithContext: boolean, symlinks: boolean}, plugins: any[], module: {rules: Array}}}
*/
const common = (
{
_BUILD_APP_INDEX_PREFIX: APP_INDEX_PREFIX,
_BUILD_DIST_DIR: DIST_DIR,
_BUILD_HTML_INDEX_DIR: HTML_INDEX_DIR = '',
_BUILD_PUBLIC_PATH: PUBLIC_PATH,
_BUILD_RELATIVE_DIRNAME: RELATIVE_DIRNAME,
_BUILD_SRC_DIR: SRC_DIR = '',
_BUILD_STATIC_DIR: STATIC_DIR = '',
_BUILD_UI_NAME: UI_NAME
} = OPTIONS.dotenv || {},
{
consoleMessage: aliasConsoleMessage = consoleMessage,
jsFileExtensions: aliasJsFileExtensions = jsFileExtensions,
setupWebpackDotenvFilesForEnv: aliasSetupWebpackDotenvFilesForEnv = setupWebpackDotenvFilesForEnv,
tsFileExtensions: aliasTsFileExtensions = tsFileExtensions
} = {}
) => ({
context: RELATIVE_DIRNAME,
entry: {
app: (() => {
let entryFiles;
try {
const fileExtensions = [...aliasTsFileExtensions, ...aliasJsFileExtensions];
const entryFilesSet = new Set([...fileExtensions.map(ext => path.join(SRC_DIR, `${APP_INDEX_PREFIX}.${ext}`))]);
entryFiles = Array.from(entryFilesSet).filter(file => fs.existsSync(file));
if (!entryFiles.length) {
aliasConsoleMessage.warn(
`webpack app entry file error: Missing entry/app file. Expected an index file! ${APP_INDEX_PREFIX}.(${fileExtensions.join('|')})`
);
}
} catch (e) {
aliasConsoleMessage.error(`webpack app entry file error: ${e.message}`);
}
return entryFiles;
})()
},
output: {
filename: '[name].bundle.js',
path: DIST_DIR,
publicPath: PUBLIC_PATH,
clean: true
},
module: {
rules: [
{
test: /\.(svg|ttf|eot|woff|woff2)$/,
include: input => input.indexOf('fonts') > -1 || input.indexOf('icon') > -1,
type: 'asset/resource',
generator: {
filename: 'fonts/[hash][ext][query]'
}
},
{
test: /\.svg$/i,
include: input => input.indexOf('fonts') === -1 || input.indexOf('icon') === -1,
type: 'asset',
parser: {
dataUrlCondition: { maxSize: 5000 }
},
generator: {
dataUrl: content => SvgToMiniDataURI(content.toString()),
filename: 'images/[hash][ext][query]'
}
},
{
test: /\.(jpg|jpeg|png|gif)$/i,
type: 'asset',
parser: {
dataUrlCondition: { maxSize: 5000 }
},
generator: {
filename: 'images/[hash][ext][query]'
}
},
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, cssLoaderResolve]
}
]
},
plugins: [
...aliasSetupWebpackDotenvFilesForEnv({
directory: RELATIVE_DIRNAME
}),
...(() => {
const staticFile = path.join(HTML_INDEX_DIR, 'index.html');
if (fs.existsSync(staticFile)) {
return [
new HtmlWebpackPlugin({
...(UI_NAME && { title: UI_NAME }),
template: staticFile
}),
new HtmlReplaceWebpackPlugin([
{
pattern: /%([A-Z_]+)%/g,
replacement: (match, $1) => process.env?.[$1] || match
}
])
];
}
return [
new HtmlWebpackPlugin({
...(UI_NAME && { title: UI_NAME })
})
];
})(),
...(() => {
try {
let fileResults;
if (fs.existsSync(STATIC_DIR)) {
fileResults = fs
.readdirSync(STATIC_DIR)
?.filter(fileDir => !/^(\.|index)/.test(fileDir))
?.map(fileDir => ({
from: path.join(STATIC_DIR, fileDir),
to: path.join(DIST_DIR, fileDir)
}));
}
return (
(fileResults?.length > 0 && [
new CopyPlugin({
patterns: fileResults
})
]) ||
[]
);
} catch (e) {
aliasConsoleMessage.error(`webpack.common.js copy plugin error: ${e.message}`);
return [];
}
})()
],
resolve: {
symlinks: false,
cacheWithContext: false
}
});
/**
* Development webpack configuration.
*
* @param {object} dotenv
* @param {string} dotenv.NODE_ENV
* @param {string} dotenv._BUILD_DIST_DIR
* @param {string} dotenv._BUILD_HOST
* @param {string} dotenv._BUILD_HTML_INDEX_DIR
* @param {string} dotenv._BUILD_OPEN_PATH
* @param {string} dotenv._BUILD_RELATIVE_DIRNAME
* @param {string} dotenv._BUILD_PORT
* @param {string} dotenv._BUILD_SRC_DIR
* @param {string} dotenv._BUILD_STATIC_DIR
* @param {object} settings
* @param {Function} settings.setupWebpackDotenvFilesForEnv
* @returns {{mode: string, devtool: string, devServer: {historyApiFallback: boolean, static: {directory: string},
* port: string, compress: boolean, host: string, devMiddleware: {writeToDisk: boolean, stats: string|object},
* client: {overlay: boolean, progress: boolean}, hot: boolean, watchFiles: {paths: string[]}}, plugins: any[]}}
*/
const development = (
{
NODE_ENV: MODE,
_BUILD_DIST_DIR: DIST_DIR,
_BUILD_HOST: HOST,
_BUILD_HTML_INDEX_DIR: HTML_INDEX_DIR,
_BUILD_OPEN_PATH: OPEN_PATH,
_BUILD_RELATIVE_DIRNAME: RELATIVE_DIRNAME,
_BUILD_PORT: PORT,
_BUILD_SRC_DIR: SRC_DIR,
_BUILD_STATIC_DIR: STATIC_DIR
} = OPTIONS.dotenv || {},
{ setupWebpackDotenvFilesForEnv: aliasSetupWebpackDotenvFilesForEnv = setupWebpackDotenvFilesForEnv } = {}
) => ({
mode: MODE,
devtool: 'eval-source-map',
devServer: {
...((OPEN_PATH && { open: OPEN_PATH }) || {}),
host: HOST,
port: PORT,
compress: true,
historyApiFallback: true,
hot: true,
devMiddleware: {
stats: 'errors-warnings',
writeToDisk: false
},
client: {
overlay: false,
progress: true
},
static: {
directory: DIST_DIR
},
watchFiles: {
paths: (() => {
const updatedPaths = new Set();
if (SRC_DIR) {
updatedPaths.add(path.basename(SRC_DIR));
}
if (HTML_INDEX_DIR) {
updatedPaths.add(path.basename(HTML_INDEX_DIR));
}
if (STATIC_DIR) {
updatedPaths.add(path.basename(STATIC_DIR));
}
return Array.from(updatedPaths).map(value => path.join(value, '**', '*'));
})()
}
},
plugins: [
...aliasSetupWebpackDotenvFilesForEnv({
directory: RELATIVE_DIRNAME,
env: MODE
}),
new MiniCssExtractPlugin({
filename: '[name].bundle.css'
})
]
});
/**
* Production webpack configuration.
*
* @param {object} dotenv
* @param {string} dotenv.NODE_ENV
* @param {string} dotenv._BUILD_RELATIVE_DIRNAME
* @param {object} settings
* @param {Function} settings.setupWebpackDotenvFilesForEnv
* @returns {{mode: string, devtool: undefined, output: {chunkFilename: string, filename: string},
* optimization: {minimize: boolean, runtimeChunk: string, minimizer: Array<any|CssMinimizerPlugin>,
* splitChunks: {chunks: string, cacheGroups: {vendor: {test: RegExp, chunks: string, name: string}}}},
* plugins: Array}}
*/
const production = (
{ NODE_ENV: MODE, _BUILD_RELATIVE_DIRNAME: RELATIVE_DIRNAME } = OPTIONS.dotenv || {},
{ setupWebpackDotenvFilesForEnv: aliasSetupWebpackDotenvFilesForEnv = setupWebpackDotenvFilesForEnv } = {}
) => ({
mode: MODE,
devtool: undefined,
output: {
chunkFilename: '[name].[contenthash:8].chunk.js',
filename: '[name].[contenthash:8].js'
},
optimization: {
minimize: true,
minimizer: [
new TerserJSPlugin({
parallel: true
}),
new CssMinimizerPlugin({
minimizerOptions: {
preset: ['default', { mergeLonghand: false }]
}
})
],
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
chunks: 'all',
name: 'vendor',
test: /[\\/]node_modules[\\/]/
}
}
}
},
plugins: [
...aliasSetupWebpackDotenvFilesForEnv({
directory: RELATIVE_DIRNAME,
env: MODE
}),
new MiniCssExtractPlugin({
chunkFilename: '[name].[contenthash:8].chunk.css',
filename: '[name].[contenthash:8].css'
})
]
});
module.exports = {
common,
development,
preprocessLoader,
preprocessLoaderNone,
preprocessLoaderJs,
preprocessLoaderTs,
production
};