@jpapini/webpack-config
Version:
Custom Webpack configuration for bundling projects.
606 lines (590 loc) • 20.2 kB
JavaScript
'use strict';
var logger = require('@jpapini/logger');
var path = require('node:path');
var fs2 = require('node:fs');
var memoizeOne = require('memoize-one');
var dotenv = require('dotenv');
var node_module = require('node:module');
var TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
var webpack = require('webpack');
var webpackBundleAnalyzer = require('webpack-bundle-analyzer');
var runScriptWebpackPlugin = require('run-script-webpack-plugin');
var nodeExternals = require('webpack-node-externals');
var ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var webpackMerge = require('webpack-merge');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var path__default = /*#__PURE__*/_interopDefault(path);
var fs2__default = /*#__PURE__*/_interopDefault(fs2);
var memoizeOne__default = /*#__PURE__*/_interopDefault(memoizeOne);
var TsconfigPathsPlugin__default = /*#__PURE__*/_interopDefault(TsconfigPathsPlugin);
var webpack__default = /*#__PURE__*/_interopDefault(webpack);
var nodeExternals__default = /*#__PURE__*/_interopDefault(nodeExternals);
var ReactRefreshWebpackPlugin__default = /*#__PURE__*/_interopDefault(ReactRefreshWebpackPlugin);
var HtmlWebpackPlugin__default = /*#__PURE__*/_interopDefault(HtmlWebpackPlugin);
var MiniCssExtractPlugin__default = /*#__PURE__*/_interopDefault(MiniCssExtractPlugin);
// src/create-build-configuration.ts
function _findProjectRoot(currentDir = process.cwd()) {
let dir = currentDir;
while (dir !== "/") {
if (fs2__default.default.existsSync(path__default.default.join(dir, ".git"))) return dir;
dir = path__default.default.dirname(dir);
}
throw new Error(`Project root not found for directory: ${currentDir}`);
}
var findProjectRoot = memoizeOne__default.default(_findProjectRoot);
// src/utils/shortern-path.util.ts
function shorternPath(absolutePath, currentDir) {
const rootDir = findProjectRoot(currentDir);
return absolutePath.replace(rootDir, "<root>");
}
// src/utils/find-file-path.util.ts
function findFilePath(currentDir, filePath) {
const resolvedPath = path__default.default.join(currentDir, filePath);
return fs2__default.default.existsSync(resolvedPath) ? resolvedPath : null;
}
function findFilePathOrThrow(currentDir, filePath) {
const resolvedPath = path__default.default.join(currentDir, filePath);
if (!fs2__default.default.existsSync(resolvedPath))
throw new Error(`File not found: ${shorternPath(resolvedPath, currentDir)}`);
return resolvedPath;
}
// src/contexts/base.context.ts
var BaseContext = class {
_buildType;
_id;
_projectDir;
_rootDir;
_outDir;
_cacheDir;
_pkgJsonFile;
_tsconfigFile;
_entryFile;
_nodeEnv;
_envVars;
_isProduction;
_isWatchMode;
_isDevServer;
_publicUrl;
_useSWC;
_swcLoaderConfig;
_tsLoaderConfig;
constructor(options) {
this._buildType = options.buildType;
this._id = options.id;
this._projectDir = findProjectRoot(options.rootDir);
this._rootDir = options.rootDir;
this._outDir = path__default.default.join(options.rootDir, options.outDir ?? "dist");
this._cacheDir = path__default.default.join(
options.rootDir,
options.cacheDir ?? "node_modules/.cache/webpack"
);
this._pkgJsonFile = findFilePathOrThrow(options.rootDir, "package.json");
this._tsconfigFile = findFilePathOrThrow(options.rootDir, "tsconfig.json");
this._entryFile = findFilePathOrThrow(options.rootDir, options.entryFile);
this._nodeEnv = options.nodeEnv;
this._envVars = options.envVars ?? {};
this._isProduction = options.isProduction;
this._isWatchMode = options.isWatchMode;
this._isDevServer = options.isDevServer;
this._publicUrl = options.publicUrl;
this._publicUrl.pathname += this._publicUrl.pathname.endsWith("/") ? "" : "/";
this._useSWC = options.useSWC ?? false;
this._swcLoaderConfig = options.swcLoaderConfig ?? {};
this._tsLoaderConfig = options.tsLoaderConfig ?? {};
}
get buildType() {
return this._buildType;
}
get id() {
return this._id;
}
get projectDir() {
return this._projectDir;
}
get rootDir() {
return this._rootDir;
}
get outDir() {
return this._outDir;
}
get cacheDir() {
return this._cacheDir;
}
get pkgJsonFile() {
return this._pkgJsonFile;
}
get tsconfigFile() {
return this._tsconfigFile;
}
get entryFile() {
return this._entryFile;
}
get nodeEnv() {
return this._nodeEnv;
}
get envVars() {
return this._envVars;
}
get isProduction() {
return this._isProduction;
}
get isWatchMode() {
return this._isWatchMode;
}
get isDevServer() {
return this._isDevServer;
}
get publicUrl() {
return this._publicUrl;
}
get useSWC() {
return this._useSWC;
}
get swcLoaderConfig() {
return this._swcLoaderConfig;
}
get tsLoaderConfig() {
return this._tsLoaderConfig;
}
print() {
logger.logger.info("ID:", logger.colors.blue(this.id));
logger.logger.info("Node environment:", logger.colors.blue(this.nodeEnv));
logger.logger.info("Public URL:", logger.colors.blue(this.publicUrl.toString()));
logger.logger.info(
"Production build:",
this.isProduction ? logger.colors.green("YES") : logger.colors.yellow("NO")
);
logger.logger.info(
"Watch mode:",
this.isWatchMode ? logger.colors.green("ENABLED") : logger.colors.yellow("DISABLED")
);
logger.logger.info(
"Dev server:",
this.isDevServer ? logger.colors.green("ENABLED") : logger.colors.yellow("DISABLED")
);
logger.logger.info("Project dir:", logger.colors.blue(this.projectDir));
logger.logger.info("Root dir:", logger.colors.blue(shorternPath(this.rootDir, this.rootDir)));
logger.logger.info("Output dir:", logger.colors.blue(shorternPath(this.outDir, this.rootDir)));
logger.logger.info("Cache dir:", logger.colors.blue(shorternPath(this.cacheDir, this.rootDir)));
logger.logger.info("Manifest file:", logger.colors.blue(shorternPath(this.pkgJsonFile, this.rootDir)));
logger.logger.info("TSConfig file:", logger.colors.blue(shorternPath(this.tsconfigFile, this.rootDir)));
logger.logger.info("Entry file:", logger.colors.blue(shorternPath(this.entryFile, this.rootDir)));
logger.logger.info("Use SWC:", this.useSWC ? logger.colors.green("YES") : logger.colors.yellow("NO"));
}
};
var NestAppContext = class extends BaseContext {
_outFilename;
constructor({ outFilename, ...options }) {
super(options);
this._outFilename = outFilename ?? "main.js";
}
get outFilename() {
return this._outFilename;
}
print() {
super.print();
logger.logger.info("Output filename:", logger.colors.blue(shorternPath(this.outFilename, this.rootDir)));
}
};
var ReactAppContext = class extends BaseContext {
_htmlTemplateFile;
constructor({ htmlTemplateFile, ...options }) {
super(options);
this._htmlTemplateFile = findFilePath(
options.rootDir,
htmlTemplateFile ?? "src/index.html"
);
}
get htmlTemplateFile() {
return this._htmlTemplateFile;
}
print() {
super.print();
logger.logger.info(
"HTML template file:",
this.htmlTemplateFile ? logger.colors.blue(shorternPath(this.htmlTemplateFile, this.rootDir)) : logger.colors.yellow("Not found")
);
}
};
// src/enums/build-type.enum.ts
var BuildType = {
REACT_APP: "REACT_APP",
NEST_APP: "NEST_APP"
};
// src/enums/node-env.enum.ts
var NodeEnv = {
DEVELOPMENT: "development",
PRODUCTION: "production",
TEST: "test"
};
// src/context.factory.ts
function contextFactory(options) {
switch (true) {
case options.buildType === BuildType.NEST_APP:
return new NestAppContext(options);
case options.buildType === BuildType.REACT_APP:
return new ReactAppContext(options);
default:
throw new Error("Invalid context options");
}
}
function loadDotenv(rootDir, dotenvFiles = [".env"]) {
const loadedEnvFiles = [];
dotenvFiles.map((name) => path__default.default.join(rootDir, name)).filter((file) => fs2__default.default.existsSync(file)).forEach((file) => {
dotenv.config({ path: file });
loadedEnvFiles.push(file);
});
return loadedEnvFiles;
}
// src/load-env-vars.ts
function loadEnvVars(rootDir, env) {
const loadedEnvFiles = loadDotenv(rootDir);
logger.logger.info("Loading environment variables from:");
if (loadedEnvFiles.length === 0) {
logger.logger.log(" -", logger.colors.red("No environment files found"));
} else {
loadedEnvFiles.forEach((file) => {
logger.logger.log(" -", logger.colors.blue(shorternPath(file, rootDir)));
});
}
process.env.NODE_ENV ??= NodeEnv.PRODUCTION;
if (!Object.values(NodeEnv).includes(process.env.NODE_ENV)) {
throw new Error(
`Invalid NODE_ENV: ${process.env.NODE_ENV}. Valid values are: ${Object.values(NodeEnv).join(", ")}`
);
}
if (!process.env.PUBLIC_URL) throw new Error("PUBLIC_URL environment variable is required");
let publicUrl;
try {
publicUrl = new URL(process.env.PUBLIC_URL);
} catch {
throw new Error(`Invalid PUBLIC_URL: ${process.env.PUBLIC_URL}. Must be a valid URL`);
}
return {
nodeEnv: process.env.NODE_ENV,
publicUrl
};
}
// src/constants.ts
var TS_RULE_TEST = /\.(?:ts|tsx|cts|mts)$/iu;
var ASSET_RULE_TEST = /\.(?:jpe?g|png|gif|svg)$/iu;
var CSS_RULE_TEST = /\.css$/iu;
var HASHED_JS_FILENAME_PATTERN = "[name].[contenthash:8].js";
var HASHED_CSS_FILENAME_PATTERN = "[name].[contenthash:8].css";
// src/presets/base.preset.ts
var { EnvironmentPlugin, SourceMapDevToolPlugin } = webpack__default.default;
var require2 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
var createBasePreset = (context, config2) => {
return {
presetName: "base",
context: context.rootDir,
mode: context.isProduction ? "production" : "development",
resolve: {
extensions: [".js", ".ts"],
extensionAlias: {
".js": [".js", ".ts"],
".cjs": [".cjs", ".cts"],
".mjs": [".mjs", ".mts"]
},
plugins: [new TsconfigPathsPlugin__default.default({ configFile: context.tsconfigFile })]
},
output: {
uniqueName: config2.name ?? context.id,
devtoolNamespace: config2.name ?? context.id,
path: context.outDir,
clean: true,
chunkFilename: context.isProduction ? HASHED_JS_FILENAME_PATTERN : "[name].js"
},
optimization: { minimize: false },
stats: { errorDetails: true },
module: {
rules: [
{
test: TS_RULE_TEST,
exclude: [/dist\//u, /node_modules\//u],
use: [
context.useSWC ? {
loader: require2.resolve("swc-loader"),
options: {
...context.swcLoaderConfig,
minify: false,
module: {
...context.swcLoaderConfig.module,
type: "nodenext"
},
jsc: {
...context.swcLoaderConfig.jsc,
parser: {
...context.swcLoaderConfig.jsc?.parser,
syntax: "typescript"
},
transform: {
...context.swcLoaderConfig.jsc?.transform,
useDefineForClassFields: true
},
keepClassNames: true,
externalHelpers: false
}
}
} : {
loader: require2.resolve("ts-loader"),
options: {
...context.tsLoaderConfig
}
}
]
}
]
},
plugins: [
new EnvironmentPlugin({
...context.envVars,
NODE_ENV: context.nodeEnv,
PUBLIC_URL: context.publicUrl.toString()
}),
...context.isProduction ? [
new SourceMapDevToolPlugin({
namespace: config2.name ?? context.id,
filename: "[file].map",
noSources: false
}),
new webpackBundleAnalyzer.BundleAnalyzerPlugin({
analyzerMode: "static",
generateStatsFile: true,
reportFilename: path__default.default.join(context.outDir, "analyzer", "report.html"),
statsFilename: path__default.default.join(context.outDir, "analyzer", "stats.json"),
openAnalyzer: false
})
] : []
].filter(Boolean),
cache: !context.isProduction ? {
type: "filesystem",
cacheDirectory: context.cacheDir
} : false
};
};
var { HotModuleReplacementPlugin } = webpack__default.default;
var require3 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
var createNestAppPreset = (context) => {
if (!(context instanceof NestAppContext)) throw new Error("Invalid context");
return {
presetName: "nest-app",
devtool: context.isProduction ? false : "inline-source-map",
target: "node",
node: {
__dirname: false,
__filename: false
},
externalsPresets: { node: true },
externals: [
nodeExternals__default.default(
context.isWatchMode ? { allowlist: ["webpack/hot/poll?100"] } : void 0
)
],
entry: [...context.isWatchMode ? ["webpack/hot/poll?100"] : [], context.entryFile],
output: {
filename: context.outFilename
},
module: {
rules: [
{
test: TS_RULE_TEST,
use: [
context.useSWC ? {
loader: require3.resolve("swc-loader"),
options: {
jsc: {
target: "es2022",
parser: {
syntax: "typescript",
decorators: true,
dynamicImport: true
},
transform: {
legacyDecorator: true,
decoratorMetadata: true
}
}
}
} : {
loader: require3.resolve("ts-loader"),
options: {}
}
]
}
]
},
plugins: context.isWatchMode ? [
new HotModuleReplacementPlugin(),
new runScriptWebpackPlugin.RunScriptWebpackPlugin({
name: context.outFilename,
autoRestart: false
})
] : []
};
};
var require4 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
var createReactAppPreset = (context) => {
if (!(context instanceof ReactAppContext)) throw new Error("Invalid context");
return {
presetName: "react-app",
devtool: context.isProduction ? false : "eval-source-map",
target: "web",
node: {
__dirname: true,
__filename: true
},
resolve: {
extensions: [".jsx", ".tsx"]
},
externalsPresets: { web: true },
optimization: {
minimize: context.isProduction,
moduleIds: "deterministic",
runtimeChunk: "single",
splitChunks: {
chunks: "all"
}
},
entry: [context.entryFile],
output: {
filename: context.isProduction ? HASHED_JS_FILENAME_PATTERN : "[name].js",
publicPath: context.publicUrl.toString()
},
module: {
rules: [
{
test: TS_RULE_TEST,
use: [
context.useSWC ? {
loader: require4.resolve("swc-loader"),
options: {
minify: context.isProduction,
jsc: {
target: "es5",
parser: {
syntax: "typescript",
tsx: true
},
transform: {
react: {
runtime: "automatic",
refresh: context.isDevServer
}
}
}
}
} : {
loader: require4.resolve("ts-loader"),
options: {}
}
]
},
{
test: CSS_RULE_TEST,
use: [
MiniCssExtractPlugin__default.default.loader,
{
loader: require4.resolve("css-loader")
},
{
loader: require4.resolve("postcss-loader"),
options: {
implementation: require4.resolve("postcss"),
postcssOptions: {
plugins: [require4.resolve("@tailwindcss/postcss")]
}
}
}
]
},
{
test: ASSET_RULE_TEST,
use: [
{
loader: require4.resolve("url-loader")
}
]
}
]
},
plugins: [
new MiniCssExtractPlugin__default.default({
filename: context.isProduction ? HASHED_CSS_FILENAME_PATTERN : "[name].css"
}),
...context.htmlTemplateFile ? [new HtmlWebpackPlugin__default.default({ template: context.htmlTemplateFile })] : [],
...context.isDevServer ? [new ReactRefreshWebpackPlugin__default.default({ overlay: { sockIntegration: "whm" } })] : []
],
...context.isDevServer ? {
devServer: {
historyApiFallback: true,
host: context.publicUrl.hostname || void 0,
port: context.publicUrl.port || void 0,
hot: true,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
"Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization"
}
}
} : {}
};
};
var mergeConfig = webpackMerge.mergeWithRules({
module: {
rules: {
test: "match",
use: {
loader: "match",
options: "merge"
}
}
}
});
// src/utils/merge-presets.util.ts
function mergePresets(context, presets) {
const loadedPresets = [];
const config2 = presets.reduce((acc, presetFunc) => {
const { presetName, ...preset } = presetFunc(context, acc);
loadedPresets.push(presetName);
return mergeConfig(acc, preset);
}, {});
return { config: config2, loadedPresets };
}
// src/create-build-configuration.ts
function createBuildConfiguration(rootDir, getOptions) {
return async function(env) {
const { nodeEnv, publicUrl } = loadEnvVars(rootDir);
const options = await getOptions();
const contextOptions = {
...options,
rootDir,
nodeEnv,
isProduction: nodeEnv !== NodeEnv.DEVELOPMENT,
isDevServer: env?.WEBPACK_SERVE ?? false,
isWatchMode: env?.WEBPACK_WATCH ?? false,
publicUrl
};
const context = contextFactory(contextOptions);
context.print();
const { loadedPresets, config: config2 } = mergePresets(context, [
createBasePreset,
...context instanceof NestAppContext ? [createNestAppPreset] : [],
...context instanceof ReactAppContext ? [createReactAppPreset] : []
]);
logger.logger.info("Loaded presets:");
loadedPresets.forEach((presetName) => {
logger.logger.log(" -", logger.colors.blue(presetName));
});
return config2;
};
}
exports.BuildType = BuildType;
exports.NodeEnv = NodeEnv;
exports.createBuildConfiguration = createBuildConfiguration;
exports.mergeConfig = mergeConfig;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map