exports_optimization.js

/**
 * @file exports/optimization.js
 * @copyright @spmhome @_2025
 * @author Scott Meesseman @spmeesseman
 * @description @see {@link https://webpack.js.org/configuration/optimization webpack.js.org/optimization}
 *//** */

const WpwWebpackExport = require("./base");
const Terser = require("terser-webpack-plugin");


 /**
  * @augments WpwWebpackExport
  */
 class WpwOptimizationExport extends WpwWebpackExport
 {
	// /**
    //  * @private
    //  * @static
    //  */
    // static regex = {
    //     dotPath: /(?:^|[\\/])\.[\w\-_]+$/,
    //     nodeModulePath: /[\\/]node_modules/,
    //     nodeModulesPathBaseAbs: /^.+?[/\\]node_modules/,
    //     nodeModulesPathBaseAbsTrSep: /^.*?[\\/]node_modules[\\/]/,
    //     relativeImportPath: /^\.\.?\//,
    //     scopedPackage: /^@[\w\-_]+[\\/][\w\-_]+$/,
    //     scopedPackageBaseDir: /^@[\w\-_]+$/
    // };
	// /**
    //  * @private
    //  * @type {string[]}
    //  */
	// isAddedToGroupCache = [];


	/**
     * @param {WpwExportOptions} options
     */
	constructor(options)
	{
		super(options);  // reset for typings
        this.buildOptions = /** @type {WpwBuildOptionsExportConfig<"optimization">} */(this.buildOptions);
	}


	/**
     * @override
     */
	static create = WpwOptimizationExport.wrap.bind(this);


	/**
	 * @private
	 * @param {WpwBuild} build
	 */
	addRuntimeChunk(build)
	{
		if (this.buildOptions.runtime !== "none")
		{
			build.logger.write("add runtime chunk", 1);
			build.wpc.optimization.runtimeChunk = this.buildOptions.runtime || "single"; // "multiple";
		}
	}


	isRelativeToChunk(mod, name)
	{
		if (mod.identifier().includes(name)) return true;
		if (mod.issuer) return this.isRelativeToChunk(mod.issuer, name);
	}
	// getSplitChunksRuleForWidget(name)
	// {
	// 	return {
	// 		test: (mod) => this.isRelativeToBuild(mod, name),
	// 		name: widgetName,
	// 		chunks: 'async',
	// 		enforce: true,
	// 	}
	// }
	/**
	 * @private
	 * @param {WpwBuild} build
	 * @param {IWpwExportConfigOptimizationCacheGroup} cacheGroup
	 */
	addSplitChunk(build, cacheGroup)
	{
		if (build.wpc.optimization.splitChunks === false) { return; }
		this._obj_.applyIf(cacheGroup, { maxSize: undefined, minSize: undefined });
		this.logger.values([
			[ "pattern regex", cacheGroup.test ], [ "chunks", cacheGroup.chunks, 3 ],
			[ "min chunk size", cacheGroup.minSize, 3 ], [ "max chunk size", cacheGroup.maxSize, 3 ]
		], 2, "   ", false, `configure '${cacheGroup.name}' split chunk cache group`);
		build.wpc.optimization.splitChunks.cacheGroups[cacheGroup.name] = {
			name: cacheGroup.name,
			chunks: cacheGroup.chunks,
			layer: cacheGroup.layer,
			maxSize: cacheGroup.maxSize,
			minSize: cacheGroup.minSize,
			test: new RegExp(cacheGroup.test)
		};
	}


	/**
	 * @private
	 * @param {WpwBuild} build
	 */
	addSplitChunks(build)
	{
		this.configureSplitChunks(build);
		build.wpc.optimization.splitChunks = {
		   chunks: "all", cacheGroups: { vendors: false, default: false  }
		};
		this.addSplitChunk(build, this.buildOptions.eslintCacheGroup);
		this.addSplitChunk(build, this.buildOptions.spmhCacheGroup);
		this.addSplitChunk(build, this.buildOptions.spmhCacheGroupApp);
		this.addSplitChunk(build, this.buildOptions.spmhCacheGroupLib);
		this.addSplitChunk(build, this.buildOptions.vendorCacheGroup);
		if (build.isReact) {
			this.addSplitChunk(build, this.buildOptions.reactCacheGroup);
		}
		this._arr_.asArray(this.buildOptions.customCacheGroup).forEach((c) => this.addSplitChunk(build, c));
	}


	/**
	 * @override
	 * @param {WpwBuild} build
	 */
    app(build)
    {
		this._obj_.apply(build.wpc.optimization,
		{
			moduleIds: "named",
			chunkIds: "named",
			usedExports: true,
			// sideEffects: true,
			concatenateModules: build.isProdMode
		});
		this.addRuntimeChunk(build);
		this.addSplitChunks(build);
	}


	/**
	 * @override
	 * @param {WpwBuild} build
	 */
	base(build)
	{
		if (build.isAnyAppOrLib)
		{
			this._obj_.apply(build.wpc.optimization,
			{
				minimize: false,
				// sideEffects: true,
				// usedExports: true,
				splitChunks: false,
				// emitOnErrors: false,
				runtimeChunk: false,
				chunkIds: "natural",
				moduleIds: "natural",
				minimizer: undefined
				// providedExports: true, //  build.mode === "production"
				// removeEmptyChunks: true,
				// mergeDuplicateChunks: true,
				// removeAvailableModules: false,
				// checkWasmType: build.isProdMode,
				// avoidEntryIife: build.isProdMode,
				// flagIncludedChunks: build.isProdMode,
				// concatenateModules: build.isProdMode
			});
			this.configureMinification(build);
		}
	}


	/**
	 * @private
	 * @param {WpwBuild} build
	 */
	configureMinification(build)
	{
		const b= build,
		      bo = b.options,
			  minifyForce = b.options.optimization?.minify === true,
			  enabled = (minifyForce && !b.isDocs && !b.isResource) ||
			            (b.isProdMode && (b.isAnyAppOrLib || (b.isScript && bo.script.minify === true) ||
						(b.isSchema && bo.schema.minify === true)));

		b.logger.write("configure terser minimizer", 1);
		if (b.isProdMode) {
			b.logger.write("   minify, license extraction, and comment removal", 1);
		}
		else {
			b.logger.write("   comment removal", 1);
		}

		const co = b.tsc.compilerOptions,
				// Terser = require("terser-webpack-plugin"),
				lib = (this._arr_.asArray(b.target).find((t) => t.startsWith("es")) ||
				       co.target || co.lib[0] || "es2015").toLowerCase().replace("es", ""),
				num = (lib !== "next" && this._types_.isNumeric(lib) ? parseInt(lib, 10) : 2020),
				ecma = /** @type {TerserECMA} */(num <= 2020 ? num : 2020);

		const minimizerOptions = this._obj_.apply({},
		{
			parallel: true,
			extractComments: enabled ?
			{   banner: false,
				filename: "vendor.LICENSE",
				// filename: (fileData) => `${fileData.filename}.LICENSE${fileData.query}`,
				condition: /^ *(?=\/\/|\*).*?licens(?:e|ing|ed|)\s.+$/i
			} : false,
			/** @type {MinifyOptions} */
			terserOptions:
			{   ecma,
				sourceMap: false,
				module: b.isModule,
				keep_classnames: true,
				parse: { shebang: true },
				safari10: b.loader === "babel",
				compress: enabled ? {drop_debugger: true, passes: 2, module: b.isModule } : false,
				mangle: enabled ? { keep_classnames: true, module: b.isModule } : false,
				format: { comments: /(?:.*?copyright|^#!\/).+$/i, shebang: true }
			}
		}, b.loader !== "esbuild" || !enabled ? {} : Terser.esbuildMinify);

		this._obj_.apply(b.wpc.optimization,
		{
			minimize: enabled,
			minimizer: [{
				apply: (/** @type {WebpackCompiler} */compiler) => { new Terser(minimizerOptions).apply(compiler); }
			}]
		});
	}


	/**
	 * @private
	 * @param {WpwBuild} build
	 */
	configureSplitChunks(build)
	{
		let cstPriority = 10;
		const bo = this.buildOptions,
			  nmPat = "[\\\\/]node_modules[\\\\/]";

		this._arr_.asArray(bo.customCacheGroup).filter((cg) => !this._types_.isDefined(cg.priority))
		.forEach((cg) => {
			cg.priority = cstPriority--;
		});

		this._obj_.merge(bo,
		{
			eslintCacheGroup:
			{
				name: "eslint",
				priority: 1,
				test: `${nmPat}.*eslint(?:\\\\|\\/||$|\\-[a-z]{3,})`
			},
			reactCacheGroup: !build.isReact ? {} :
			{
				name: "react",
				priority: 1,
				test: `${nmPat}react(?:\\\\|\\/||$|\\-[a-z]{3,})`
			},
			spmhCacheGroup:
			{
				name: "spmhc",
				priority: 1,
				test: `${nmPat}@spmhome[\\\\/](?!(svr|exec|type|app|log|cli|arg|vscode))`
			},
			spmhCacheGroupApp:
			{
				name: "spmha",
				priority: 2,
				test: `${nmPat}@spmhome[\\\\/](?:app|log|cli|arg|vscode|exec)`
			},
			spmhCacheGroupLib:
			{
				name: "spmhl",
				priority: 3,
				test: `${nmPat}@spmhome[\\\\/](?:svr|type)`
			},
			vendorCacheGroup:
			{
				name: "vendor",
				priority: 10,
				test: `${nmPat}(?!(@spmhome|react(?:\\\\|\\/|$||\\-[a-z]{3,})|(?:@|.+?\\-)eslint(?:$|\\\\|\\/|\\-)))`
			}
		});
	}


    // /**
    //  * @private
    //  * @param {string} ctx
    //  * @returns {string}
    //  */
    // moduleNameFromCtx(ctx)
    // {
    //     if (WpwOptimizationExport.regex.nodeModulePath.test(ctx))
    //     {
    //         const name = ctx.replace(WpwOptimizationExport.regex.nodeModulesPathBaseAbsTrSep, "")
    //                         .replace(WpwOptimizationExport.regex.scopedPackage, "");
    //         return !name.includes("/") ? name : name.split("/").slice(0, 2).join("/");
    //     }
    //     return "_relative_";
    // }


	/**
	 * @override
	 * @param {WpwBuild} _build
	 */
	doxygen(_build) {}


    /**
     * @override
	 * @param {WpwBuild} _build
     */
    jsdoc(_build) {}


    /**
     * @override
	 * @param {WpwBuild} build
     */
    lib(build)
	{
		this._obj_.apply(build.wpc.optimization,
		{
			moduleIds: "named",
			chunkIds: "named",
			usedExports: true,
			// sideEffects: true,
			removeAvailableModules: false,
			concatenateModules: build.isProdMode
		});
	}


    /**
     * @override
     * @param {WpwBuild} build
     */
    plugin(build) { this.app(build); }


	/**
	 * @override
	 * @param {WpwBuild} _build
	 */
	resource(_build) {}


	/**
	 * @override
	 * @param {WpwBuild} _build
	 */
	schema(_build) {}


    /**
     * @override
	 * @param {WpwBuild} _build
     */
    script(_build) {}


    /**
     * @override
	 * @param {WpwBuild} _build
     */
    tests(_build) {}


    /**
     * @override
	 * @param {WpwBuild} _build
     */
    types(_build) {}


    /**
     * @override
	 * @param {WpwBuild} build
     */
    webapp(build)
	{
		this._obj_.apply(this.build.wpc.optimization,
		{
			moduleIds: "named",
			chunkIds: "named",
			usedExports: true,
			// sideEffects: true,
			removeAvailableModules: false,
			concatenateModules: build.isProdMode
		});
		this.addRuntimeChunk(build);
		this.addSplitChunks(build);
	}

 }


module.exports = WpwOptimizationExport.create;