/**
* @file src/exports/rules.js
* @copyright @spmhome @_2025
* @author Scott Meesseman @spmeesseman
*
* @description @see {@link https://webpack.js.org/configuration/rules webpack.js.org/rules}
*
*//** */
const WpwWebpackExport = require("./base");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
/**
* @augments WpwWebpackExport
* @implements {WpwWebpackExport}
*/
class WpwRulesExport extends WpwWebpackExport
{
/**
* @param {WpwExportOptions} options
*/
constructor(options)
{
super(options);
this.buildOptions = /** @type {WpwBuildOptionsExportConfig<"rules">} */(this.buildOptions); // reset for typings
}
get rules() { return this.build.wpc.module.rules; }
/**
* @override
*/
static create = WpwRulesExport.wrap.bind(this);
/**
* @override
* @param {WpwBuild} build
*/
app(build)
{
const b = this.build,
loader = this.getSourceLoader(),
srcRgx = build.getSrcPathRegExp(),
jsTsRgx = build.isJsTs ? /\.[cm]?[jt]sx?$/ : (build.isTs || build.isTsJs ? /\.[cm]?tsx?$/ : /\.[cm]?jsx?$/),
incNodeModules = this._arr_.asArray(build.wpc.resolve.modules)
.map((m) => this._path_.isAbsPath(m) ? m : this._path_.resolvePath(build.getContextPath(), m));
build.logger.write(`add '${build.type}' build rules`, 1);
build.logger.value(" configured loader", loader.loader, 2);
if (build.resources) {
this.appResources(build);
}
if (build.debug) {
this.appDebug(build);
}
if (loader.loader === "ts-loader" && build.options.types?.enabled)
{
const outFilename = build.options.types.bundle?.enabled ?
(b.options.output?.name || build.name).replace(/\.d\.ts$/, "") : undefined;
loader.options.compilerOptions = this._obj_.apply(
b.tsc.getCompilerOptions(),
{ declaration: true,
declarationMap: false,
emitDeclarationsOnly: false,
outFile: outFilename ? this._path_.joinPath(
build.virtualEntry.dirDistRelToProj, `${outFilename}.d.ts`
) : undefined,
declarationDir: !outFilename ? build.virtualEntry.dirBuild : undefined
}
);
}
this.rules.push(
{
use: loader, include: incNodeModules, test: /\.[\w-]{1,6}$/,
exclude: [ srcRgx,...this.getExcludes(false, true) ]
}, {
use: loader, test: jsTsRgx, include: srcRgx, exclude: this.getExcludes()
});
}
/**
* @private
* @param {WpwBuild} build
*/
appDebug(build)
{
const exclude = this.getExcludes(), srcPathRgx = build.getSrcPathRegExp();
build.logger.write(`add '${build.type}' build rules`, 1);
build.logger.value(" configured loader", "string-replace-loader", 2);
this.rules.push(
{
issuerLayer: "release",
exclude, include: srcPathRgx,
test: new RegExp(`${build.source.dotext}$`),
// include(resourcePath, issuer) {
// console.log(` context: ${build.wpc.context} (from ${issuer})`);
// console.log(` resourcePath: ${resourcePath} (from ${issuer})`);
// console.log(` included: ${path.relative(build.wpc.context || ".", resourcePath)} (from ${issuer})`);
// return true; // include all
// },
loader: "string-replace-loader", options: this.stripLoggingOptions()
},
{
exclude, include: srcPathRgx,
test: new RegExp(`wrapper${build.source.dotext}$`),
issuerLayer: "release", loader: "string-replace-loader",
options: {
search: /^log\.(?:write2?|error|warn|info|values?|method[A-Z][a-z]+)\]/g,
replace: "() => {}]"
}
});
}
/**
* @private
* @param {WpwBuild} build
*/
appResources(build)
{
const res = build.resources,
exclude = this.getExcludes(),
sourceAssets = this._arr_.asArray(res.source),
inlineAssets = this._arr_.asArray(res.inline),
resourceAssets = this._arr_.asArray(res.resource);
build.logger.write(`add '${build.type}' build rules`, 1);
build.logger.value(" configured loader", "asset", 2);
build.logger.value(" # of 'inline' assets", inlineAssets.length, 2);
build.logger.value(" # of 'resource' assets", resourceAssets.length, 2);
build.logger.value(" # of 'source' assets", sourceAssets.length, 2);
if (sourceAssets.length > 0)
{
this.rules.push({
exclude, type: "asset/source", test: /^.+\.(?:[a-zA-Z0-9]{1,6})$/,
include: new RegExp(this._rgx_.escapeRegExp(sourceAssets.join("|")))
});
}
if (resourceAssets.length > 0)
{
this.rules.push({
exclude, type: "asset/resource", test: /^.+\.(?:[a-zA-Z0-9]{1,6})$/,
include: new RegExp(this._rgx_.escapeRegExp(resourceAssets.join("|")))
});
}
if (inlineAssets.length > 0)
{
this.rules.push({
exclude, type: "asset/inline", test: /^.+\.(?:[a-zA-Z0-9]{1,6})$/,
include: new RegExp(this._rgx_.escapeRegExp(inlineAssets.join("|")))
});
}
}
/**
* @override
*/
doxygen() { this.task(); }
/**
* @override
*/
extjsdoc() { this.task(); }
/**
* @since 1.11.0
* @param {*} loader
* @param {boolean} [allowTest]
* @param {boolean} [allowNodeModules]
* @param {boolean} [allowTypes]
* @param {boolean} [allowDts]
* @returns {RegExp[]}
*/
getExcludes(loader, allowTest, allowNodeModules, allowTypes, allowDts)
{
const ex = [ /\.vscode[\\/]/ ];
if (allowNodeModules !== true) {
ex.push(/node_modules/);
}
if (allowTest !== true) {
ex.push(/tests?[\\/]/, /\.test\.[jt]s$/, /\.spec\.[jt]s$/);
}
if (allowTypes === false) {
ex.push(/types[\\/]/);
}
if (allowDts === false) {
ex.push(/\.d\.ts$/);
}
if (this.build.isTranspiled && loader === "babel")
{ try
{ this._arr_.asArray(this.build.tsc.tsconfig.exclude)
.filter((exg) => !exg.includes("node_modules"))
.map((exg) => exg.replace("**/", "[\\w-\\\\/\\.]+\\/").replace("/**", "\\/.+?\\*\\.\\*")
.replace(/\/(\*|[^*])(\.\*)$/, (_, g, g2) => `.+${g && !g2 ? g : ""}`)
.replace(/[\\/]/g, this._path_.sep))
.filter((exg) => !(/^(?:[^/]\..+|\.\.?\*?|)$/).test(exg))
.forEach((exg) => ex.push(new RegExp(exg)));
} catch {}
}
return ex;
}
/**
* @private
* @param {string} [loaderNm]
* @returns {WebpackRuleSetUseItem}
*/
getSourceLoader(loaderNm)
{
const b = this.build,
lNm = loaderNm || b.loader || b.options.rules?.loader ||
(b.isTranspiled ? (b.isTs || b.isTsJs ? "ts" : "babel") : "babel");
return this.sourceLoaders[lNm](b.isTs || b.isTsJs);
}
/**
* The `jsdoc` module is a {@link WpwBaseTaskPlugin} type module, using a virtual file so that
* Webpack runs through it's normal process as if it going to bundle ts/js code. This scenario
* is used when the build really isn't a Webpack bundling, but more of something that usually
* a task runner would handle, e.g this `jsdoc` build or the `types` build. They are implemented
* here in an effort to condense all tasks to Webpack only, where something like Gulp, Grunt, Ant,
* etc is not needed, especially for several smaller projects.
*
* @override
* @throws {SpmhError}
*/
jsdoc() { this.task(); }
/**
* @override
* @param {WpwBuild} build
*/
lib(build) { this.app(build); }
/**
* @override
* @param {WpwBuild} build
*/
plugin(build) { this.app(build); }
/**
* @override
*/
resource()
{
this.task();
// this.rules.push(this.taskRule(this.build.options.resource, "wpw-static-loader");
}
/**
* @override
*/
schema() { this.task(); }
/**
* @override
*/
script() { this.task(); }
sourceLoaders =
{
/**
* @param {boolean} [ts]
* @returns {WebpackRuleSetUseItem}
*/
babel: (ts) =>
{
const b = this.build, bo = this.buildOptions, presets = [],
engineNodeVersion =this._node_.nodejsEnginesVersion(b.pkgJson),
nodejsVersion = engineNodeVersion || b.tsc.versionNodeRuntime,
babel = this._obj_.asObject(bo.babel), compilerOptions = b.tsc.compilerOptions,
esmModulesLib = compilerOptions.target || this._arr_.asArray(compilerOptions.lib)[0],
cfg = { loader: "babel-loader", options: { presets, cwd: this._path_.resolvePath(b.nodeModulesPathWpw, "..") }};
b.logger.values([
[ "use default presets", !!babel.defaultPresets ], [ "node lts version", b.tsc.versionNodeLtsMajor ],
[ "node package.json.engine version", nodejsVersion ], [ "target node version", nodejsVersion ],
[ "is wpw global exec", this.global.isGlobalExec, 2 ], [ "is wpw dev exec", this.global.isDevExec, 2 ],
[ "is wpw dist exec", this.global.isDevDistExec, 2 ]
], 1, "", false, "configure babel-loader");
if (!b.isWeb && babel.defaultPresets !== true) {
presets.push([ "@babel/preset-env", { targets: { node: nodejsVersion, esmodules: b.isModule }}]);
} else { presets.push([ "@babel/preset-env" ]); }
if (b.isModule) {
cfg.options.presets.push([ "@babel/preset-modules", { targets: { node: nodejsVersion, modules: esmModulesLib }}]);
}
if (ts || b.isTs || b.isTsJs || b.isJsTs) {
cfg.options.presets.push([ "@babel/preset-typescript" ]);
}
if (this._types_.isArray(babel.plugins)) {
cfg.options.plugins = babel.plugins.map((p) => this._arr_.asArray(p));
}
if (this._types_.isArray(babel.presets)) {
cfg.options.presets.push(...babel.presets.map((p) => this._arr_.asArray(p)));
}
return cfg;
},
/**
* @param {boolean} [ts]
* @returns {WebpackRuleSetUseItem}
*/
esbuild: (ts) =>
{
const b = this.build,
ecmaVersion = this._arr_.asArray(b.target).find((t) => t.startsWith("es")) ||
b.tsconfig.compilerOptions.target ||
this._arr_.asArray(b.tsconfig.compilerOptions.lib)[0];
const cfg =
{ loader: "esbuild-loader",
options:
{ color: false,
loader: ts ? "ts" : "js",
context: b.getContextPath(),
tsconfig: b.source.info.path,
raw: { ...b.tsconfig.compilerOptions },
target: this._arr_.uniq([
b.tsc.versionNodeLtsMajor, ecmaVersion,
...this._arr_.asArray(b.tsconfig.compilerOptions.lib)
])
}
}; return cfg;
},
/**
* @param {boolean} [_ts]
* @returns {WebpackRuleSetUseItem}
*/
task: (_ts) =>
{
const b = this.build;
const cfg =
{ loader: "wpw-task-loader",
options: {
build: b,
input: b.getSrcPath(),
output: b.getDistPath(),
// context: this.build.getContextPath(),
vFile: `${b.virtualEntry.filePathAbs}${b.source.dotext}`
}
}; return cfg;
},
/**
* @param {boolean} [_ts]
* @returns {WebpackRuleSetUseItem}
*/
ts: (_ts) =>
{
const b = this.build;
const cfg =
{ loader: "ts-loader",
options:
{ transpileOnly: true,
experimentalWatchApi: false,
context: b.getContextPath(),
configFile: b.tsc.configFileRtPath,
logInfoToStdOut: b.logger.level >= 1,
compilerOptions: b.tsc.compilerOptions, // b.tsc.getTranspilationCompilerOptions(),
logLevel: b.log.level >= 2 ? "info" : (b.log.level === 1 ? "warn" : "error")
}
}; return cfg;
}
};
/**
* @private
* @returns {Record<string, any>}
*/
stripLoggingOptions = () => ({
multiple: [
{ // eslint-disable-next-line stylistic/max-len
search: /=>\s*(?:this\.wrapper|this|wrapper|w)\._?log\.(?:write2?|info|values?|method[A-Z][a-z]+)\s*\([^]*?\)\s*\}\);/g,
replace: (/** @type {string} */r) => {
return "=> {}\r\n" + r.substring(r.slice(0, r.length - 3).lastIndexOf(")") + 1);
}
},
{
search: /=>\s*(?:this\.wrapper|this|wrapper|w)\._?log\.(?:write2|info|values?|method[A-Z][a-z]+)\s*\([^]*?\),/g,
replace: "=> {},"
},
{
search: /=>\s*(?:this\.wrapper|this|wrapper|w)\._?log\.(?:write2?|info|values?|method[A-Z][a-z]+)\s*\([^]*?\) *;/g,
replace: "=> {};"
},
{
// eslint-disable-next-line stylistic/max-len
search: /(?:this\.wrapper|this|wrapper|w)\._?log\.(?:write2?|info|values?|method[A-Z][a-z]+)\s*\([^]*?\)\s*;\s*?(?:\r\n|$)/g,
replace: "\r\n"
},
{
search: /this\.wrapper\.log\.(?:write2?|info|values?|method[A-Z][a-z]+),/g,
replace: "this.wrapper.emptyFn,"
},
{
search: /wrapper\.log\.(?:write2?|info|values?|method[A-Z][a-z]+),/g,
replace: "wrapper.emptyFn,"
},
{
search: /w\.log\.(?:write2?|info|values?|method[A-Z][a-z]+),/g,
replace: "w.emptyFn,"
},
{
search: /this\.wrapper\.log\.(?:write2?|info|values?|method[A-Z][a-z]+)\]/g,
replace: "this.wrapper.emptyFn]"
},
{
search: /wrapper\._?log\.(?:write2?|info|values?|method[A-Z][a-z]+)\]/g,
replace: "wrapper.emptyFn]"
},
{
search: /w\.log\.(?:write2?|info|values?|method[A-Z][a-z]+)\]/g,
replace: "w.emptyFn]"
}]
});
/**
* @private
*/
task()
{
const b = this.build,
loader = this.getSourceLoader("task"),
vFile = `${b.virtualEntry.file}${b.source.dotext}`,
vFilePath = `${b.virtualEntry.filePathAbs}${b.source.dotext}`;
b.logger.value("add compilation task-plugin trigger rules", 1);
b.logger.value(" configured loader", loader.loader, 2);
b.logger.value(" v-build file name", vFile, 2);
b.logger.value(" v-build file path", vFilePath, 3);
this.rules.push(
{
use: loader, include: vFilePath, exclude: this.getExcludes(loader),
test: new RegExp(`[\\\\/]${b.virtualEntry.file}${b.source.dotext}$`)
});
}
/**
* @override
* @param {WpwBuild} build
*/
tests(build)
{
const loader = this.getSourceLoader();
build.logger.write(`add '${build.type}' build rules`, 1);
build.logger.value(" configured loader", loader.loader, 2);
this.rules.push(
{ use: loader, test: new RegExp(`${build.source.dotext}x?$`),
include: build.getSrcPathRegExp(), exclude: this.getExcludes(loader, true)
});
}
/**
* The `types` module is a {@link WpwBaseTaskPlugin} type module, using a virtual file so that
* Webpack runs through it's normal process as if it going to bundle ts/js code. This scenario
* is used when the build really isn't a Webpack bundling, but more of something that usually
* a task runner would handle, e.g this `types` build or the `jsdoc` build. They are implemented
* here in an effort to condense all tasks to Webpack only, where something like Gulp, Grunt, Ant,
* etc is not needed, especially for several smaller projects.
*
* @override
* @param {WpwBuild} build
*/
types(build)
{
const config = build.options.types,
loader = this.getSourceLoader();
build.logger.write(`add '${build.type}' build rules`, 1);
build.logger.value(" configured loader", loader.loader, 2);
if (config && (config.mode === "loader" || config.mode === "plugin")) {
this.task();
}
this.rules.push(
{ use: loader,
test: /\.tsx?$/,
exclude: this.getExcludes(loader),
include: this._arr_.uniq([
build.getSrcPath(), build.getSrcPath({ build: "app" })
], process.platform === "win32")
});
}
/**
* @override
* @param {WpwBuild} build
*/
webapp(build)
{
const wpCss = build.options.web.css,
loader = this.getSourceLoader(),
exclude = this.getExcludes(false, true);
build.logger.write(`add '${build.type}' build rules`, 1);
this.rules.push(
{
exclude,
use: loader,
test: /\.[cm]?js/,
resolve: { fullySpecified: false }
},
{
exclude,
test: wpCss ? /\.scss$/ : /\.s?css$/,
use: wpCss ? [
{ // webpack_config -> experiments -> css: ENABLED
loader: "sass-loader",
options: { api: "modern", sourceMap: build.isDevMode }
}]
: // webpack_config -> experiments -> css: DISABLED
[{
loader: build.isProdMode ? MiniCssExtractPlugin.loader : "style-loader"
},{
loader: "css-loader",
options: { sourceMap: build.isDevMode, url: false }
},{
loader: "sass-loader",
options: { api: "modern", sourceMap: build.isDevMode }
}]
});
if (build.isTs || build.isJsTs || build.isTsJs || build.tsc.compilerOptions.jsx)
{
const ctxPath = build.getContextPath(),
include = ctxPath === build.getBasePath() ? build.getSrcPathRegExp() :
[ new RegExp(this._rgx_.escapeRegExp(ctxPath)), build.getSrcPathRegExp() ];
this.rules.unshift({
include, exclude,
test: /\.tsx?$/,
use: this.sourceLoaders.ts(true).loader
});
}
};
};
module.exports = WpwRulesExport.create;