utils_vendormod.js

/**
 * @file utils/vendormod.js
 * @copyright @spmhome @_2025
 * @author Scott Meesseman @spmeesseman
 */

const {
	absPath, existsSync, readFileSync, writeFileSync, readFileAsync, resolve, writeFileAsync
} = require("@spmhome/cmn-utils");

const nodeModulesDir = absPath("node_modules");


/**
 * @param {any} opts
 * @param {WpwLogger} logger
 * @returns {boolean}
 */
const angular = (opts, logger) =>
{
	if (!opts.all && !opts.srcmap) { return false; }
	const ngWpCfg = resolve(nodeModulesDir, "@angular-devkit/build-angular/src/tools/webpack/configs/common.js");
	if (existsSync(ngWpCfg))
	{
		let content = readFileSync(ngWpCfg);
		const rgx = /topLevelAwait: *false/;
		if (rgx.test(content))
		{
			logger.write("apply angular webpack.config top-level await modification", 1);
			logger.write("   " + ngWpCfg, 1);
			content = content.replace(rgx, "topLevelAwait: true");
			writeFileSync(ngWpCfg, content);
			return true;
		}
	}
	return false;
}


/**
 * @param {any} opts
 * @param {WpwLogger} logger
 * @returns {Promise<boolean>}
 */
const jsdoc = async(opts, logger) =>
{   //
	// JSDOC/PUBLISH.JS
	// file:///D:\Projects\ci\webpack-wrap\node_modules\clean-jsdoc-theme\publish.js ~ line 764
	// file:///D:\Projects\ci\webpack-wrap\node_modules\jsdoc\templates\default\publish.js ~ line 493
	//
	// replace some dumb shit in some of the templates with the implementation used in docdash
	//
	if (!opts.all && !opts.jsdoc) { return false; }
	const rplRgx = new RegExp(" +if \\( *packageInfo +&& +packageInfo\\.name *\\)\\s+{\\s+outdir = path\\.join\\" +
							  "( *outdir, packageInfo\\.name, \\(? *packageInfo\\.version \\|\\| '' *\\)? *\\);\\s+\\}");
	const rplTxt = `    if (packageInfo) {
		const subdirs = [outdir];
		if (packageInfo.name) {
			const packageName = packageInfo.name.split('/');
			if (packageName.length > 1 && conf.scopeInOutputPath !== false) {
				subdirs.push(packageName[0]);
			}
			if (conf.nameInOutputPath !== false) {
				subdirs.push((packageName.length > 1 ? packageName[1] : packageName[0]));
			}
			if (packageInfo.version && conf.versionInOutputPath !== false) {
				subdirs.push(packageInfo.version);
			}
			if (subdirs.length > 1) {
				outdir = path.join.apply(null, subdirs);
			}
		}
	}`;
	const _ = async(/** @type {string} */ path) =>
	{
		const jsdoc = resolve(nodeModulesDir, path);
		if (existsSync(jsdoc))
		{   const content = await readFileAsync(jsdoc);
			if (!content.includes("packageName = packageInfo.name.split"))
			{   logger.write("apply jsdoc template modification", 1);
				logger.write("   " + jsdoc, 1);
				await writeFileAsync(jsdoc, content.replace(rplRgx, rplTxt));
				return true;
			}
		}
		return false;
	};
	const didApply = (await _("clean-jsdoc-theme/publish.js")) ||  (await _("jsdoc/templates/default/publish.js"));
	return didApply;
};


/**
 * @param {any} opts
 * @param {WpwLogger} logger
 * @returns {boolean}
 */
const nodefetch = (opts, logger) =>
{   //
	// Remove the async import and replace with top level import
	//
	if (!opts.all && !opts.nodefetch) { return false; }
	const imports = [],
		  nodefetch = resolve(nodeModulesDir, "node-fetch/src/body.js");
	if (existsSync(nodefetch))
	{
		let content = readFileSync(nodefetch);
		const rgx = /const +\{ *(.+?) *\} += +await +import\(["'](.+?)["']\) *;/gm;
		if (rgx.test(content))
		{
			logger.write("apply node-fetch async import modification", 1);
			logger.write("   " + nodefetch, 1);
			content = content.replace(rgx, (m, g1, g2) => { imports.push(`import { ${g1} } from "${g2}";`); return `// ${m}`; });
			if (imports.length) {
				writeFileSync(nodefetch, imports.join("\n") + "\n" + content);
				return true;
			}
		}
	}
	return false;
};


/**
 * @param {any} opts
 * @param {WpwLogger} logger
 * @returns {boolean}
 */
const nyc = (opts, logger) =>
{   //
	// Remove the referened non-existent required internal module nyc
	//
	if (!opts.all && !opts.nyc) { return false; }
	const nyc = resolve(nodeModulesDir, "nyc/index.js");
	if (existsSync(nyc))
	{
		let content = readFileSync(nyc);
		const rgx = /require\.resolve\("nyc\//gm;
		if (rgx.test(content))
		{
			logger.write("apply nyc se;f referenced coverage file modification", 1);
			logger.write("   " + nyc, 1);
			content = content.replace("require(mod)", "____require____(mod)")
				.replace(rgx, "____require.resolve____(\"nyc/")
				.replace("selfCoverageHelper = require('../self-coverage-helper')", "selfCoverageHelper = { onExit () {} }");
			writeFileSync(nyc, content);
			return true;
		}
	}
	return false;
};


/**
 * @param {any} opts
 * @param {WpwLogger} logger
 * @returns {boolean}
 */
const sourcemapPlugin= (opts, logger) =>
{
	// if (!(compilation instanceof Compilation)) {
	// 	throw new TypeError(
	// 		"The 'compilation' argument must be an instance of Compilation"
	// 	);
	// }
	//
	// WEBPACK.SOURCEMAPPLUGIN
	// file:///d:\Projects\ci\webpack-wrap\node_modules\webpack\lib\javascript\JavascriptModulesPlugin.js
	//
	// A hack to remove a check added in Webpack 5 using 'instanceof' to check the compilation parameter.
	// If multiple webpack installs are present, the following error occurs, regardless if Wp versions are the same:
	//
	// TypeError: The 'compilation' argument must be an instance of Compilation
	//    at Function.getCompilationHooks (...node_modules\webpack\lib\javascript\JavascriptModulesPlugin.js:164:10)
	//    at SourceMapDevToolModuleOptionsPlugin.apply (...\node_modules\webpack\lib\So...onsPlugin.js:54:27)
	//    at d:\Projects\ci\webpack-wrap\node_modules\webpack\lib\SourceMapDevToolPlugin.js:184:53
	//    at Hook.eval [as call] (eval at create (...\node_modules\tapable\lib\HookCodeFactory.js:19:10), <anon>:106:1)
	//    at Hook.CALL_DELEGATE [as _call] (d:\Projects\ci\webpack-wrap\node_modules\tapable\lib\Hook.js:14:14)
	//	  ....
	//
	// Seeing it's a module resolution issue, this was patched for this plugin using 'require.resolve'
	// in the cwd when importing this plugin in plugins/sourcemaps.js.
	//
	// This is a "in the worst case" fix, where we can "kind if" safely remove this check, and
	// consider it patched if redundant testing yields no side effects,
	//
	if (!opts.all && !opts.srcmap) { return false; }
	const sourceMapPlugin = resolve(nodeModulesDir, "webpack/lib/javascript/JavascriptModulesPlugin.js");
	if (existsSync(sourceMapPlugin))
	{
		let content = readFileSync(sourceMapPlugin);
		const rgx = /if \(!\(compilation instanceof Compilation\)\)/;
		if (rgx.test(content))
		{
			logger.write("apply sourcemap-plugin compilation type comparison modification", 1);
			logger.write("   " + sourceMapPlugin, 1);
			content = content.replace(rgx, "if (false)");
			writeFileSync(sourceMapPlugin, content);
			return true;
		}
	}
	return false;
};


/**
 * @param {any} opts
 * @param {WpwLogger} logger
 * @returns {boolean}
 */
const tsLoader = (opts, logger) =>
{   //
	// TS-LOADER
	// file:///d:\Projects\ci\webpack-wrap\node_modules\ts-loader\dist\index.js
	//
	// A hck to allow just a straight up types 'declarations only' build.
	//
	if (!opts.all && !opts.tsloader) { return false; }
	const tsLoader = resolve(nodeModulesDir, "ts-loader/dist/index.js");
	if (existsSync(tsLoader))
	{
		let content = readFileSync(tsLoader);
		const rgx = /if \(!\(compilation instanceof Compilation\)\)/;
		if (rgx.test(content))
		{
			logger.write("apply ts-loader extended options check modification", 1);
			logger.write("   " + tsLoader, 1);
			content = readFileSync(tsLoader)
			.replace(
				/if \(outputText === null \|\| outputText === undefined\)/,
				"if ((outputText === null || outputText === undefined) && (!instance.loaderOptions.compilerOptions " +
				"|| !instance.loaderOptions.compilerOptions.emitDeclarationsOnly))"
			).replace(
				"callback(null, output, sourceMap);",
				"callback(null, (!instance.loaderOptions.compilerOptions || " +
				"!instance.loaderOptions.compilerOptions.emitDeclarationsOnly ? output : \"\"), sourceMap);"
			);
			writeFileSync(tsLoader, content);
			return true;
		}
	}
	return false;
};


/**
 * @param {any} opts
 * @param {WpwLogger} logger
 * @returns {Promise<boolean>}
 */
async function applyVendorMods(opts, logger)
{
	opts.all ||= !!Object.keys(opts).every((o) => !o);
    if (!opts.quiet) {
		logger.write("check vendor node_module modification status for known required changes", 1);
	}
	let didRpl = await jsdoc(opts, logger);
	didRpl ||= angular(opts, logger);
	didRpl ||= nodefetch(opts, logger);
    didRpl ||= nyc(opts, logger);
    didRpl ||= sourcemapPlugin(opts, logger);
    didRpl ||= tsLoader(opts, logger);
	if (didRpl)
	{
		logger.sep();
		logger.start(" applied required modifications, a manual build restart is required");
		logger.sep();
	}
	else if (!opts.quiet && !didRpl) {
		logger.write("   all modifications have already been applied", 1);
	}
    return didRpl;
}


module.exports = { applyVendorMods, jsdoc, nodefetch, nyc, sourcemapPlugin, tsLoader };