UNPKG

@jpapini/webpack-config

Version:
606 lines (590 loc) 20.2 kB
'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