/**
* @file exports/externals.js
* @copyright @spmhome @_2025
* @author Scott Meesseman @spmeesseman
*
* @description @see {@link [externals](https://webpack.js.org/configuration/externals/)}
*//** */
const { readdirSync } = require("fs");
const WpwWebpackExport = require("./base");
const isBuiltinNodeJsModule = require("node:module").isBuiltin;
const { isWebpackExternalsType } = require("../types/constants");
class WpwExternalsExport extends WpwWebpackExport
{
/**
* @private
* @static
*/
static regex = {
dotPath: /(?:^|[\\/])\.[\w\-_]+$/,
nodeModulePath: /[\\/]node_modules/,
nodeModulesPathBaseAbs: /^.+?[/\\]node_modules/,
nodeModulesPathBaseAbsTrSep: /^.*?[\\/]node_modules[\\/]/,
relativeImportPath: /^\.\.?\//,
relativeSassCssPath: /\/(?:sa|c)ss-loader\//,
relativeImportTsConfigPath: /^[:#][\w-]+/,
scopedPackage: /^@.+?\//,
scopedPackageBaseDir: /^@[\w\-_]+$/,
spmhUtilsPackage: /@spmhome\/[a-z]+-utils/
};
/**
* @private
*/
_importStats = { bundled: 0, external: 0, relative: 0, relativeVendor: 0, vendor: 0 };
/**
* @private
* @type {string[]}
*/
installedModules = [];
/**
* @private
* @type {RegExp}
*/
externalRgx;
/**
* @private
* @type {RegExp | undefined}
*/
bundledRgx;
/**
* @private
* @type {RegExp | undefined}
*/
ignoredRgx;
/**
* @private
* @type {string[]}
*/
rtExternals = [
"append-transform", "chokidar", "esbuild", "eslint", "node-windows", "nyc", "schema-utils", "typescript", "webpack"
];
/**
* @private
* @type {string[]}
*/
alwaysExternal = [ // "^(?:fs)?events$",
"^append-transform$", "^caniuse.*$", "^chokidar$", "^esbuild$", "^eslint(?:\\/.+)?$", "^fsevents$",
"humanfs", "^jiti.*$", "^karma$", "^.+-loader$", "^mini-css", "^mysql2?.*?$", "^node-windows$", "^nyc$", "^rollup.*$",
"^sass$", "^schema-utils$", "^ts-node", "^typescript$", "^vscode$", "(?:^|-|\\/)webpack(?:$|\\/|-)"
];
// /**
// * @private
// */
// _externalInnerRgx = /node_modules[\\/]((?:(?:@.+?)[\\/])?([^ \\]+(?:$|[^\\/])|[^ \\/]+$|[^ \\/]))/;
/**
* @type {number | NodeJS.Timeout}
*/
timer;
/**
* @param {WpwExportOptions} options Plugin options to be applied
*/
constructor(options)
{
super({ ...options, hasPluginHooks: true });
this.buildOptions = /** @type {WpwBuildOptionsExportConfig<"externals">} */(this.buildOptions);
}
/**
* @override
*/
static create = WpwExternalsExport.wrap.bind(this);
/**
* @override
* @param {WpwBuild} build
*/
app(build)
{
if (build.pkgJson.scopedName.scope === "@spmhome")
{
if (this.buildOptions.bundled) {
this.buildOptions.bundled.push("^@spmhome\\/.+?-utils$");
}
else {
this.buildOptions.bundled = [ "^@spmhome\\/.+?-utils$" ];
}
}
}
/**
* @override
* @param {WpwBuild} build
*/
base(build)
{
const bo = this.buildOptions;
this.setExternalPresets();
build.logger.value(" externals presets", build.wpc.externalsPresets, 1);
build.wpc.externalsType = isWebpackExternalsType(bo.type) ? bo.type : this.getExternalsType();
build.logger.value(" default import type", build.wpc.externalsType, 2);
}
/**
* @override
* @param {WpwBuild} build
*/
baseDone(build)
{
const l = build.logger, bo = this.buildOptions,
ignRgxStr = bo.ignored && bo.ignored.length > 0 ? bo.ignored.join("|") : null,
notExt = this._arr_.uniq([ ...(bo.bundled || []), ...(bo.noImportBundled || []) ]),
bndRgxStr = this._arr_.uniq([ ...(bo.bundled || []), ...(bo.noImportBundled || []) ]).join("|") || null,
extRgxStr = this._arr_.uniq([ ...this.alwaysExternal, ...(bo.external || []), ...(bo.ignored || []) ]).join("|"),
allIncRgxStr = bo.all ? "^.+$" : (notExt.length > 0 ? `(?!(?:${notExt.join("|")}))` : "") + `(?:${extRgxStr})`;
this.externalRgx = new RegExp(extRgxStr);
build.externalsRgx = new RegExp(allIncRgxStr);
this.ignoredRgx = ignRgxStr ? new RegExp(ignRgxStr) : undefined;
this.bundledRgx = bndRgxStr ? new RegExp(bndRgxStr) : undefined;
l.values([
[ "all external", !!bo.all ], [ "ignored regex", ignRgxStr ], [ "bundled regex", bndRgxStr ],
[ "external regex", extRgxStr ], [ "all inclusive regex", this.build.externalsRgx ], [ "raw", !!bo.raw ]
], 1, "", false, "configured externals regex patterns");
this.getInstalledModules();
if (!this._types_.isObject(bo.raw, true))
{
build.logger.write("add externals inline runtime processor", 3);
build.wpc.externals = [ this.processModule.bind(this) ];
}
else
{
if(build.logger.level <= 2) {
build.logger.write("add externals configured 'raw' value", 1);
}
else {
build.logger.value("add externals configured 'raw' value", bo.raw, 3);
}
build.wpc.externals = this._arr_.asArray(bo.raw);
}
}
/**
* @override
*/
doxygen() {}
/**
* @override
*/
extjsdoc() {}
/**
* @private
* @param {string} [moduleName]
* @param {boolean} [isBuiltin]
* @param {string} [_ctxDepType] i.e. data.dependencyType
* @returns {WebpackExternalsType}
*/
getExternalsType(moduleName, isBuiltin, _ctxDepType)
{
const build = this.build;
let /** @type {WebpackExternalsType} */type;
if (isWebpackExternalsType(this.buildOptions.type))
{
type = this.buildOptions.type;
}
// else if (moduleName === "vscode")
// {
// type = "commonjs";
// }
else if (build.isWeb)
{
type = !build.isModule ? "commonjs" : (isBuiltin ? "import" : "module-import");
}
else {
type = !build.isModule ? "commonjs" : (isBuiltin ? "node-commonjs" : "import");
}
return type;
}
/**
* @private
* @throws {SpmhError}
*/
getInstalledModules()
{
const b = this.build, l = this.logger, p = b.pkgJson, path = this._path_,
store = this.store.get("node_modules", /** @type {Record<string, any>} */({})),
lastHash = store.hash, lastNodeModulesCount = store.nodeModulesCount || 0,
hash = this._crypto_.getMd5(this._json_.safeStringify(this._obj_.pickBy(p, (d) => /dependencies/.test(d))), "hex"),
nodeModules = [ ...Object.keys(p.dependencies || []), ...Object.keys(p.devDependencies || []),
...Object.keys(p.optionalDependencies || []), ...Object.keys(p.peerDependencies || []) ],
nodeModulesCount = nodeModules.length;
l.write(" examine project node_modules folder", 1);
l.value(" previous package.json level1 dependencies count", lastNodeModulesCount || "n/a", 1);
l.value(" current package.json level1 dependencies count", nodeModulesCount, 1);
if (store.nodeModules && lastNodeModulesCount > 0 && lastNodeModulesCount === nodeModulesCount && hash === lastHash)
{
this.installedModules = store.nodeModules;
l.write(` use cached node_modules list [total::${lastNodeModulesCount}]`, 1);
}
else
{ const _examinePackage = /* async */ (/** @type {string} */pkg) =>
{ if (this._path_.isAbsPath(pkg))
{ this.installedModules.push(
...readdirSync(pkg).map((mod) =>
{ if (WpwExternalsExport.regex.scopedPackageBaseDir.test(mod))
{ try {
return readdirSync(path.resolvePath(pkg, mod)).map((m) => `${mod}/${m}`);
} catch { return [ mod ]; }
}
else if (WpwExternalsExport.regex.scopedPackage.test(mod))
{ try {
return readdirSync(path.resolvePath(pkg, mod)).map((m) => path.joinPath(mod, m));
}
catch { return [ mod ]; }
}
return [ mod ];
}).reduce((prev, next) => this._arr_.pushUniq(prev, ...next), [])
);
} else { if (b.pkgJson.engines?.[pkg] || this._fs_.existsSync(pkg)) this.installedModules.push(pkg); }
};
for (const m of b.nodeModulesPaths.all) { _examinePackage(m); }
this.store.set({ node_modules: { nodeModulesCount, hash, nodeModules: this.installedModules }});
}
if (l.level === 3 || l.level === 4)
{
l.value(" package.json node_modules", this.installedModules.join(" | "),3);
if (l.level === 4) {
l.value(" installed node_modules", this.installedModules.join(" | "), 4);
}
}
else if (l.level === 5) {
l.value(" installed node_modules", this.installedModules, 5);
}
l.write(` processed ${this.installedModules.length} installed node_modules [direct::${nodeModulesCount}]`, 1);
}
/**
* @private
* @param {string} request
* @returns {string}
*/
getModuleName(request)
{
let mn = request.replace(WpwExternalsExport.regex.nodeModulesPathBaseAbsTrSep, "");
const aliasCfg = this.build.wpc.resolve.alias;
if (this._types_.isObject(aliasCfg) && mn.startsWith(":"))
{ const alias = Object.entries(aliasCfg).find(([ a ]) => mn.startsWith(a));
for (const path of alias[1].map(this._path_.normalizePath))
{ const mna = this._path_.relativePathEx(
this.build.getSrcPath(), this._path_.normalizePath(mn.replace(alias[0], path)), { psx: true, dot: true }
); if (mna) { mn = mna; break; }
} }
return mn;
}
/**
* @private
* @param {string} ctx
* @returns {string}
*/
getModuleNameFromCtx(ctx)
{
if (WpwExternalsExport.regex.nodeModulePath.test(ctx))
{
const name = ctx.replace(WpwExternalsExport.regex.nodeModulesPathBaseAbsTrSep, "")
.replace(WpwExternalsExport.regex.scopedPackage, "");
return !name.includes("/") ? name : name.split("/").slice(0, 2).join("/");
}
return "___invalid___";
}
/**
* @private
* @param {string} moduleName module / package name, scope inclusive
* @param {boolean} builtIn
* @param {string} dataReq
* @param {string} dataCtx
* @param {boolean} [vEntry]
* @returns {boolean}
*/
isExternalModule(moduleName, builtIn, dataReq, dataCtx, vEntry)
{
let isExt = false;
const isRelImport = this.isRelativeImport(dataReq),
isSassCssImport = this.isSassCssImport(dataReq),
bundleBuiltIn = this.buildOptions.bundleBuiltIn,
bundleRuntime = this.buildOptions.bundleRuntime,
issuerModuleName = this.getModuleNameFromCtx(dataCtx),
isInstalled = !vEntry && (this.installedModules.includes(moduleName) ||
(!isRelImport && this.installedModules.includes(issuerModuleName)));
this.logger.values([
[ "requested", dataReq ], [ "is relative", isRelImport ], [ "is built-in", builtIn ], [ "is v-entry", !!vEntry ],
[ "is installed", isInstalled ], [ "requested by", issuerModuleName ], [ "request context", dataCtx ]
], 4, "", false, `determine if imported module '${moduleName}' will be bundled or is externalzzzzzz`);
if (isSassCssImport || (builtIn && bundleBuiltIn !== true))
{
isExt = !isSassCssImport;
}
else if (isInstalled)
{
const cfgIgnored= !!this.ignoredRgx?.test(moduleName),
cfgBundled = !!this.bundledRgx?.test(moduleName),
cfgExternal = this.externalRgx.test(moduleName),
cfgBundleRt = bundleRuntime === false,
parentIsEx = isRelImport && !!this.rtExternals.find((r) => r === issuerModuleName) && !cfgBundleRt;
this.logger.values([
[ "ignore", cfgIgnored ], [ "bundle", cfgBundled ], [ "external", cfgExternal ],
[ "request issuer module is external", parentIsEx ]
], 5, " ", false, "rc configured externals regexes");
isExt = this.buildOptions.all || cfgIgnored || (!cfgBundled && (cfgExternal || parentIsEx));
if (isExt && !parentIsEx && !isRelImport && WpwExternalsExport.regex.nodeModulePath.test(dataCtx))
{
this.logger.write(" module identified as 'runtime external'", 4);
this.rtExternals.push(moduleName);
}
}
else if (!isRelImport)
{
isExt = !!vEntry || !!builtIn || !!(this.build.pkgJson.engines?.[moduleName]) || bundleRuntime === false ||
(!this.bundledRgx?.test(moduleName) && this.externalRgx.test(moduleName));
}
this.logger.write(`module '${moduleName}' is ${!isExt ? "not" : (builtIn ? "built-in |" : "")} external`, 4);
return isExt;
}
/**
* @param {string} moduleName
* @returns {boolean}
*/
isRelativeImport(moduleName) { return WpwExternalsExport.regex.relativeImportPath.test(moduleName); }
/**
* @param {string} moduleName
* @returns {boolean}
*/
isSassCssImport(moduleName)
{
return this.build.isWebApp && WpwExternalsExport.regex.relativeSassCssPath.test(moduleName);
}
/**
* @override
*/
jsdoc() {}
/**
* @override
* @param {WpwBuild} build
*/
lib(build)
{
const rgx = WpwExternalsExport.regex.spmhUtilsPackage;
this._arr_.pushUniq(this.alwaysExternal,
...Object.keys(this._obj_.asObject(build.pkgJson.dependencies)).filter((p) => rgx.test(p)).map((p) => `^${p}$`),
...Object.keys(this._obj_.asObject(build.pkgJson.peerDependencies)).map((p) => `^${p}$`),
...Object.keys(this._obj_.asObject(build.pkgJson.optionalDependencies)).map((p) => `^${p}$`)
);
}
/**
* @override
* @param {WpwBuild} build
*/
plugin(build) { this.app(build); }
// /**
// * @private
// */
// printResults()
// {
// const logger = this.xLogger,
// icon = withColor(logger.icons.star, logger.color);
// logger.write(`${icon}${icon} externals totals ${icon}${icon}`, 1);
// logger.value(" # of bundled node_modules", this._importStats.bundled, 1);
// logger.value(" # of external node_modules", this._importStats.external, 1);
// logger.value(" # of internal relative path imports", this._importStats.relative, 1);
// logger.value(" # of vendor relative path imports", this._importStats.relativeVendor, 1);
// }
/**
* @private
* @param {IWpwRuntimeExternal} nm
* @param {Readonly<WebpackExternalItemFunctionData>} data
*/
processFirstImport(nm, data)
{
const b = this.build,
logger = this.logger,
ctx = nm.ctx.replace(WpwExternalsExport.regex.nodeModulesPathBaseAbs, "node_modules")
.replace(new RegExp(`^${this._rgx_.escapeRegExp(b.getBasePath())}[/\\\\]`), "")
if (nm.external)
{
++this._importStats.external;
this.xLogger.write(
`[italic(external)] ${nm.builtin ? "built-in module" : "vendor package"} '${nm.name}' @ '${ctx}'`, 2
);
}
else if (this.isSassCssImport(nm.name))
{
++this._importStats.bundled;
if (!nm.ctx.includes("node_modules"))
{ this.xLogger.write(
`[italic(bundle)] stylesheet '${nm.name.substring(nm.name.lastIndexOf("/") + 1)}' @ '${ctx}'`, 2
);
}
}
else if (this.isRelativeImport(nm.name))
{
++this._importStats.bundled;
if (!nm.ctx.includes("node_modules")) {
this.xLogger.write(`[italic(bundle)] module '${nm.name}' @ '${ctx}'`, 2);
}
else {
++this._importStats.relativeVendor;
this.xLogger.write(`[italic(bundle)] vendor module '${nm.name}' @ '${ctx}'`, 4);
}
}
else
{ ++this._importStats.vendor;
++this._importStats.bundled;
if (nm.name.startsWith("@spmhome")) {
this.xLogger.write(`[italic(bundle)] spmhome package '${nm.name}' @ '${ctx}'`, 2);
}
else if (nm.name.startsWith("react")) {
this.xLogger.write(`[italic(bundle)] react package '${nm.name}' @ '${ctx}'`, 2);
}
else {
this.xLogger.write(`[italic(bundle)] vendor package '${nm.name}' @ '${ctx}'`, 2);
}
}
if (logger.level >= 2)
{
logger.values([
[ "request", nm.req ], [ "context", nm.ctx ], [ "dependency type", data.dependencyType ]
], 3, " ", 0, 0, 0, [ "externals" ]);
if (data.contextInfo && logger.level >= 4)
{
logger.values([
[ "issuer", data.contextInfo.issuer ], [ "issuer layer", data.contextInfo.issuerLayer ]
], 4, " ", 0, 0, 0, [ "externals" ]);
}
}
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => { this.store.set({ node_modules_imported: this.build.nodeModules }); }, 250);
}
// param {(e?: Error | null, result?: string, type?: WebpackExternalsType) => void} cb
/**
* @private
* @param {Readonly<WebpackExternalItemFunctionData>} data
* @param {any} cb
* @returns {any}
*/
processModule(data, cb)
{
if (data?.request && !this.build.hasGlobalError)
{ const b = this.build,
req = this._path_.fwdSlash(data.request),
ctx = this._path_.fwdSlash(data.context),
name = this.getModuleName(req),
builtin = b.wpc.resolve.fallback?.[name.replace(/\.\w$/, "")] ? false : isBuiltinNodeJsModule(name),
vEntry = !!name.match(new RegExp(`${name}\\.(?:${this.outputExt.substring(1)}|${b.source.ext})$`)),
external = this.isExternalModule(name, builtin, req, ctx, vEntry);
if (!vEntry && !b.nodeModules.find((e) => e.name === name))
{
const rt = external && (this.rtExternals.includes(name) ||
this.rtExternals.find((r) => ctx.includes(`/${r}/`) && !(b.nodeModules.find((e) => e.name === r)?.external)));
const rtExternal = { name, builtin, external, ctx, req, rt: !!rt };
this.processFirstImport(this._arr_.pushReturnOne(b.nodeModules, rtExternal), data);
}
if (external) {
return cb(null, `${this.getExternalsType(name, builtin, data.dependencyType)} ${name}`);
}
}
return cb();
}
/**
* @override
*/
resource() { this.buildOptions.all = true; }
/**
* @override
*/
schema() { this.buildOptions.all = true; }
/**
* @override
*/
script() { this.buildOptions.all = true; }
/**
* @private
*/
setExternalPresets()
{
const bo = this.buildOptions;
if (bo.presets?.length)
{
this.build.logger.write(` set externals presets '${bo.presets.join(" | ")}'`, 1);
this.build.wpc.externalsPresets = bo.presets.reduce((p, c) => this._obj_.apply(p, { [c]: true }), {});
}
else if (!this.build.isWeb)
{
this.build.wpc.externalsPresets = { node: true };
}
else {
this.build.wpc.externalsPresets = { web: true };
}
}
/**
* @override
*/
tests() { this.buildOptions.all = true; }
/**
* @override
*/
types() { this.buildOptions.all = true; }
/**
* @override
* @param {WpwBuild} build
*/
webapp(build) { this.app(build); }
}
module.exports = WpwExternalsExport.create;