exports_entry.js

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

const WpwWebpackExport = require("./base");
const { createEntryObjFromDir } = require("../utils/utils");
const { apply, isString, isObject, pluralize, asArray } = require("@spmhome/type-utils");
const {
	existsSync, findFilesSync, findExPathSync, isAbsPath, isDirectory, normalizePath, relativePath, resolvePath
} = require("@spmhome/cmn-utils");


/**
 * @augments WpwWebpackExport
 */
class WpwEntryExport extends WpwWebpackExport
{
	/**
	 * @private
	 * @type {string}
	 */
	globTestSuiteFiles= "**/*.{test,tests,spec,specs}";


    /**
	* @param {WpwExportOptions} options Plugin options to be applied
	*/
   constructor(options)
   {
	   super(options);
	   this.buildOptions = /** @type {WpwBuildOptionsExportConfig<"entry">} */(this.buildOptions);
   }


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


	/**
	 * @override
	 * @param {WpwBuild} build
	 */
	app(build)
	{
		const outputCfg = this.build.options.output || { name: undefined, ext: undefined },
			  entryName = outputCfg.name || build.name,
		      l = build.logger.write("construct entry export configuration", 1);

		if (build.entry)
		{
			l.write("   found rc-configured entry point configuration", 1);
			if (isObject(build.entry))
			{
				const entries = Object.entries(build.entry),
					  entryCtxTxt = `${pluralize("entry", entries.length)}`;
				l.write(`   found entry point 'object' type config with ${entries.length} ${entryCtxTxt}`, 1);
				entries.forEach(([ entryName, cfg ]) =>
				{
					if (isObject(cfg))
					{
						const ext = outputCfg.ext || (build.isModule ? ".mjs" : ".cjs"),
						      name = cfg.filename || "[name]";
						l.write("      found entry point 'descriptor' type value");
						apply(cfg, {
							filename: `${name}${build.options.hash?.enabled ? ".[contenthash]" : ""}${ext}`
						});
						this._obj_.merge(build.wpc.entry, { [entryName]: cfg });
						l.write("      " + JSON.stringify(cfg, null, l.level >= 3 ? 3 : undefined), 2);
					}
					else if (isString(cfg, true))
					{
						l.write("   found entry point 'object path' type config", 1);
						l.value("      configured entry path", cfg, 2);
						build.wpc.entry[entryName] = cfg;
					}
					else
					{   return build.addMessage({
							code: this.MsgCode.ERROR_CONFIG_PROPERTY,
							message: `could not determine ${build.type} entry point`,
							detail: "entry point contains empty or invalid rc-configuration"
						});
					}
				});
			}
			else
			{   l.write("   found entry point 'string/path' type config", 1);
				apply(build.wpc.entry,
				{
					[entryName]: {
						import: build.entry,
						layer: build.isApp && build.debug ? "release" : undefined
					}
				});
			}
		}
		else
		{
			l.write("    entry point not configured", 1);
			const entryPath = this.determineEntryPath(build);
			if (entryPath)
			{
				l.write("      auto-create entry point 'object import' type value", 1);
				apply(build.wpc.entry, {
					[entryName]: {
						import: entryPath,
						layer: build.debug ? "release" : undefined
					}
				});
				if (build.debug)
				{
					l.write("      auto-create layer.debug entry point 'object import' type value", 1);
					apply(build.wpc.entry,
					{
						[`${entryName}.debug`]: {
							import: entryPath,
							layer: "debug"
						}
					});
				}
			}
		}
	}


	/**
	 * @override
	 * @protected
     * @param {WpwBuild} build
	 */
	baseDone(build)
	{
		if (isString(build.entry))
		{
			build.entry = `./${this._path_.fwdSlash(build.entry).replace(/^\.\//, "")}`;
		}
		else if (isObject(build.entry))
		{
			const entry = this.build.entry;
			Object.keys(entry).forEach((name) =>
			{
				if (isString(entry[name]))
				{
					entry[name] = `./${this._path_.fwdSlash(entry[name]).replace(/^\.\//, "")}`;
				}
				else
				{   const imp = entry[name].import;
					if (this._types_.isArray(imp))
					{
						for (let i = 0; i < imp.length; i++) {
							entry[name].import[i] = `./${this._path_.fwdSlash(imp[i]).replace(/^\.\//, "")}`;
						}
					}
					else {
						entry[name].import = `./${this._path_.fwdSlash(imp).replace(/^\.\//, "")}`;
					}
				}
			});
		}
	}


	/**
	 * @private
	 * @param {WpwBuild} build
	 * @returns {string | string[] | undefined}
	 */
	determineEntryPath(build)
	{
		const entry = build.entry;
		let entryPath, entryPathAbs;

		build.logger.write("   determine unknown entry point", 2);

		if (isString(entry))
		{
			entryPathAbs = entry;
			if (!isAbsPath(entryPathAbs)) {
				entryPathAbs = resolvePath(build.getRootCtxPath(), entryPathAbs);
			}
			// entryPathAbs = normalize(`${entryPathAbs.replace(/\.(?:j|t)s$/, "")}.${build.source.ext}`);
			if (!existsSync(entryPathAbs)) {
				entryPathAbs = undefined;
			}
		}

		if (this._types_.isArray(entry))
		{
			entryPathAbs = entry.map((_e) =>  this._path_.absPath(entryPathAbs, true, build.getContextPath()));
			if (entryPathAbs.length === 0) {
				entryPathAbs = undefined;
			}
		}

		if (!entryPathAbs)
		{
			const rootLikeFilenames = [
				`index.${build.source.ext}`, `main.${build.source.ext}`, `${build.name}.${build.source.ext}`,
				`${build.type}.${build.source.ext}`, `${this._arr_.asArray(build.target).join("-")}.${build.source.ext}`
			];

			build.logger.write("      no entry point defined in wpwrc for this build", 3);
			build.logger.write("      attempt to auto-find entry point", 3);

			if (build.type !== "app")
			{
				const mainBuildConfig = build.getBuildConfigMain();
				if (mainBuildConfig)
				{
					build.logger.write("   get configured entry point from 'app' build config", 2);
					if (mainBuildConfig.entry)
					{
						if (isString(mainBuildConfig.entry)) {
							entryPathAbs = mainBuildConfig.entry;
						}
						else if (isObject(mainBuildConfig.entry))
						{
							const eValues = Object.values(mainBuildConfig.entry);
							entryPathAbs = eValues.map((e) => isObject(e) ? e.import : e);
						}
					}
					entryPathAbs = asArray(entryPathAbs).map(this._path_.absPath.bind(this)).filter((e) => existsSync(e));
					if (!entryPathAbs.length)
					{
						entryPathAbs.push(normalizePath(
							`${mainBuildConfig.paths.src}/${mainBuildConfig.name}.${build.source.ext}`
						));
						build.logger.write("   search entry point by build name for 'app' build", 2);
						build.logger.value("      constructed search value", entryPathAbs, 2);
						if (!existsSync(entryPathAbs[0]))
						{
							entryPathAbs = findFilesSync(
								`**/${mainBuildConfig.name}.${build.source.ext}`,
								{ cwd: mainBuildConfig.paths.src, absolute: true, maxDepth: 3 }
							);
							if (!entryPathAbs.length) {
								entryPathAbs = normalizePath(`${build.paths.src}/index.${build.source.ext}`);
								build.logger.write("   check entry point by name 'index'", 2);
								build.logger.value("      value", entryPathAbs, 2);
							}
						}
					}
				}
			}
			else
			{
				entryPathAbs = normalizePath(`${build.paths.src}/${build.name}.${build.source.ext}`);
				build.logger.write("      search entry point by build name", 2);
				build.logger.value("         constructed search value", entryPathAbs, 2);
				if (!existsSync(entryPathAbs))
				{
					entryPathAbs = findFilesSync(
						`**/${build.name}.${build.source.ext}`,
						{ cwd: build.paths.src, absolute: true, maxDepth: 3 }
					)[0];
					if (!entryPathAbs)
					{
						entryPathAbs = normalizePath(`${build.paths.src}/index.${build.source.ext}`);
						build.logger.write("      check entry point by name 'index'", 2);
						build.logger.value("         value", entryPathAbs, 2);
					}
				}
			}

			if (!entryPathAbs.length)
			{
				const srcPath = build.getRootSrcPath(),
					  file = findExPathSync(rootLikeFilenames, srcPath, true);
				if (file)
				{
					build.logger.write("   get configured entry point from 'root' paths config", 2);
					entryPathAbs = resolvePath(srcPath, file);
				}
			}

			if (!entryPathAbs.length)
			{
				const srcPath = build.tsc.compilerOptions.rootDir,
					  file = findExPathSync(rootLikeFilenames, srcPath, true);
				if (file)
				{
					build.logger.write("   get configured entry point from 'tsconfig.json' parsed config", 2);
					entryPathAbs = resolvePath(srcPath, file);
				}
			}

			if (!entryPathAbs.length)
			{
				const srcPath = build.tsc.compilerOptions.sourceRoot,
					  file = findExPathSync(rootLikeFilenames, srcPath, true);
				if (file)
				{
					build.logger.write("   get configured entry point from 'root' paths config", 2);
					entryPathAbs = resolvePath(srcPath, file);
				}
			}

			if (!entryPathAbs.length && ((build.pkgJson.main && !build.isModule) || (build.pkgJson.module && build.isModule)))
			{
				entryPathAbs = build.pkgJson.main;
				build.logger.write("   check configured 'main' property in package.json", 2);
				build.logger.value("      property value", entryPathAbs, 2);
				if (!isAbsPath(entryPathAbs)) {
					entryPathAbs = resolvePath(build.getBuildConfigMain().paths.base, entryPathAbs);
				}
				entryPathAbs = normalizePath(`${entryPathAbs.replace(/\.(?:j|t)s$/, "")}.${build.source.ext}`);
				if (!existsSync(entryPathAbs)) {
					entryPathAbs = undefined;
				}
			}
		}

		if (!entryPathAbs.length)
		{
			this.addMessage({
				lPad: "   ",
				code: this.MsgCode.ERROR_CONFIG_PROPERTY,
				message: "could not determine entry point",
				detail: `build details: [wpc.entry: name=${build.name}] [type=${build.type}] [mode=${build.mode}]`,
				suggest: [
					"name the build using the same name as that of the entry file",
					"set 'root.build.entry' in the wpwrc file ('(web)app', 'lib', and 'test' type builds only)",
					`create a root level index.${build.source.ext} that exports the current unknown entry file`
				]
			});
		}
		else
		{
			entryPath = relativePath(build.getContextPath(), asArray(entryPathAbs)[0]);
			entryPath = `./${this._path_.fwdSlash(entryPath.replace(/^\.[/\\]/, "")
				         .replace(/\.(?:j|t)s$/, ""))}.${build.source.ext}`;
			if (entryPathAbs.includes(build.getDistPath()))
			{
				entryPath = undefined;
				this.addMessage({
					code: this.MsgCode.ERROR_CONFIG_PROPERTY,
					message: "entry point cannot be a descendent of 'dist' path, output will overwrite input",
					suggest: "set a valid 'entry' property in the wpw build configuration ot js/tsconfig file",
					detail: `details: [entry=${entryPath}][name=${build.name}][type=${build.type}][mode=${build.mode}]`
				});
			}
		}

		if (entryPath) {
			build.logger.value("   using entry point", entryPath, 2);
		}
		else {
			build.logger.error("   unable to determine entry point");
		}
		return entryPath;
	}


	/**
	 * @override
	 * @param {WpwBuild} build
	 */
	doxygen(build) { this.task(build, build.options.doc.doxygen); }


	/**
	 * @override
	 * @param {WpwBuild} build
	 */
	extjsdoc(build) { this.task(build, build.options.doc.extjsdoc); }


	/**
	 * @override
	 * @param {WpwBuild} build
	 */
	jsdoc(build) { this.task(build, build.options.doc.jsdoc); }


	/**
	 * @override
	 * @param {WpwBuild} build
	 */
	lib(build)
	{
		this.app(build);
		// const build = this.build,
		// 	  entryPath = this.appEntryPath();
		// if (entryPath && !build.hasError)
		// {
		// 	apply(build.wpc.entry,
		// 	{
		// 		[build.name]: {
		// 			import: entryPath,
		// 			filename: `${build.name}.js`
		// 			// library: {
		// 			// 	type: "commonjs2"
		// 			// }
		// 		}
		// 	});
		// 	let binPath = resolvePath(build.getSrcPath(), `bin/${build.name}.js`);
		// 	if (!existsSync(binPath)) {
		// 		binPath = resolvePath(build.getSrcPath(), `cli/${build.name}.js`);
		// 	}
		// 	if (existsSync(binPath))
		// 	{
		// 		const binName = dirname(binPath);
		// 		apply(build.wpc.entry,
		// 		{
		// 			[`${build.name}-${binName}`]: {
		// 				import: `./${binName}/${build.name}.js`,
		// 				filename: `${build.name}-${binName}.js`,
		// 				dependOn: build.name
		// 			}
		// 		});
		//
		// 	}
		// }
	}


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


	/**
	 * @override
	 * @param {WpwBuild} build
	 */
	schema(build) { this.task(build, build.options.schema.scripts, true); }


	/**
	 * @override
	 * @param {WpwBuild} build
	 */
	script(build) { this.task(build, build.options.script, true); }


	/**
	 * @override
	 * @param {WpwBuild} build
	 */
    resource(build) { this.task(build, build.options.resource, true); }


	/**
	 * @private
	 * @param {WpwBuild} build
	 * @param {WpwBuildOptionsConfig<any> | WpwBuildOptionsPluginConfig<any>} [_config]
	 * @param {boolean | undefined} [_noEntry]
	 */
	task(build, _config, _noEntry)
	{
		// if (!noEntry)
		// {
		// 	const entry = build.entry || this.determineEntryPath();
		// 	if (isObject(entry))
		// 	{
		// 		let entryNum = 0;
		// 		build.logger.write("   apply dependency entry point path for file dependencies", 3);
		// 		apply(build.wpc.entry, entry);
		// 		Object.values(build.entry).forEach((value) => {
		// 			apply(build.wpc.entry, { [`entry${++entryNum}`]: value });
		// 		});
		// 	}
		// 	else if (isString(entry)) {
		// 		apply(build.wpc.entry, { entry1: `./${forwardSlash(entry.replace(/^\.\//, ""))}` });
		// 	}
		// }
		// else {
			apply(
				build.wpc.entry,
				{ [build.virtualEntry.file]: `./${this._path_.fwdSlash(build.virtualEntry.filePathRel)}${build.source.dotext}`}
			);
		// }
	}


	/**
	 * @override
	 * @param {WpwBuild} build
	 */
	tests(build)
	{
		const dotext = this.build.source.dotext,
		      testsPath = this.build.getSrcPath();
		apply(build.wpc.entry, {
			...createEntryObjFromDir(testsPath, dotext, true, [ this.globTestSuiteFiles + dotext ]),
			...createEntryObjFromDir(testsPath, dotext, true)
		});
	}


	/**
	 * @override
	 * @param {WpwBuild} build
	 */
	types(build)
	{
		if (build.options.types?.mode === "plugin") { this.task(build, build.options.types); }
	}


	/**
	 * @override
	 * @param {WpwBuild} build
	 */
	webapp(build)
	{
		if (build.entry)
		{
			this.app(build);
		}
		else
		{
			if (isDirectory(build.getSrcPath()))
			{
				const appPath = this._path_.resolve(build.getContextPath(), build.getSrcPath({ rel: true }));
				apply(build.wpc.entry, createEntryObjFromDir(appPath, build.source.dotext));
			}
		}
	}

}


module.exports = WpwEntryExport.create;