/**
* @file plugins/web/html.js
* @copyright @spmhome @_2025
* @author Scott Meesseman @spmeesseman
* @description base class for spa and mpa webapp or webworker
*//** */
const { posix } = require("path");
const WpwPlugin = require("../base");
// const WpwCspWebPlugin = require("./csp");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const WpwWebAppInlineChunksPlugin = require("./inlinechunks");
const CspHtmlWebpackPlugin = require("csp-html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
/**
* @augments WpwPlugin
*/
class WpwWebPlugin extends WpwPlugin
{
/**
* @param {WpwPluginOptions} options Plugin options to be applied
*/
constructor(options)
{
super(options);
this.buildOptions = /** @type {WpwBuildOptionsPluginConfig<"web">} */(this.buildOptions);
}
/**
* @override
* @param {WpwBuild} build
* @returns {WpwWebPlugin | undefined}
*/
static create = WpwWebPlugin.wrap.bind(this);
/**
* @returns {CspHtmlWebpackPlugin}
*/
csp()
{
const plugin = new CspHtmlWebpackPlugin(
{
// "connect-src":
// !build.isProdMode
// ? [ "#{cspSource}", "'nonce-#{cspNonce}'", "https://www.sandbox.paypal.com", "https://www.paypal.com" ]
// : [ "#{cspSource}", "'nonce-#{cspNonce}'", "https://www.paypal.com" ],
"default-src": "'none'",
"font-src": [ "#{cspSource}" ],
// "frame-src":
// !build.isProdMode
// ? [ "#{cspSource}", "'nonce-#{cspNonce}'", "https://www.sandbox.paypal.com", "https://www.paypal.com" ]
// : [ "#{cspSource}", "'nonce-#{cspNonce}'", "https://www.paypal.com" ],
"img-src": [ "#{cspSource}", "https:", "data:" ],
"script-src":
!this.build.isProdMode ?
[ "#{cspSource}", "'nonce-#{cspNonce}'" ] :
[ "#{cspSource}", "'nonce-#{cspNonce}'", "'unsafe-eval'" ],
"style-src":
this.build.isProdMode ?
[ "#{cspSource}", "'nonce-#{cspNonce}'", "'unsafe-hashes'" ] :
[ "#{cspSource}", "'unsafe-hashes'", "'unsafe-inline'" ]
},
{
enabled: true,
hashingMethod: "sha256",
hashEnabled: {
"script-src": true,
"style-src": this.build.isProdMode
},
nonceEnabled: {
"script-src": true,
"style-src": this.build.isProdMode
}
});
plugin.createNonce = () => "#{cspNonce}"; // Override 'createNonce' & return a ph to be replaced @ runtime
return plugin;
}
/**
* @returns {MiniCssExtractPlugin | undefined}
*/
css()
{
if (!this.buildOptions.css) // if not built-in webpack css support (experimental as of 9/9/25)
{
return new MiniCssExtractPlugin(
{
filename: (pathData, _assetInfo) =>
{
let name = "[name]";
if (pathData.chunk?.name)
{
// data = /** @type {WebpackPathDataOutput} */(pathData);
const // hashEnabled = !!this.build.options.hash?.enabled,
hashedName = true; // hashEnabled && !TestsChunk.test(pathData.chunk?.name || "");
name = pathData.chunk.name.replace(
/[a-z]+([A-Z])/g,
(substr, token) => substr.replace(token, "-" + token.toLowerCase())
) + (hashedName ? ".[contenthash]" : "");
}
return `css/${name}.css`;
}
});
}
}
/**
* @override
* @param {WebpackCompiler} _compiler
* @param {boolean} [applyFirst]
* @returns {WebpackPluginInstance[] | undefined}
*/
getVendorPlugin(_compiler, applyFirst)
{
if (!applyFirst)
{ try {
return [ this.css(), ...this.webapps(), this.csp(), this.inlinechunks(), this.images() ];
}
catch (e)
{ this.addMessage({
exception: e,
message: "failed to configure web plugin",
code: this.MsgCode.ERROR_CONFIG_INVALID_PLUGINS
})
}
}
}
/**
* @returns {WpwWebAppInlineChunksPlugin}
*/
inlinechunks() { return new WpwWebAppInlineChunksPlugin(HtmlWebpackPlugin, [], this.logger); }
/**
* @returns {ImageMinimizerPlugin | undefined}
*/
images()
{
return !this.build.isProdMode ? undefined :
new ImageMinimizerPlugin({ deleteOriginalAssets: true, generator: [ this.imageminimizer() ] });
}
/**
* @returns { ImageMinimizerPlugin.Generator<any> }
*/
imageminimizer()
{
return this.buildOptions.optimizeImages ?
{
type: "asset",
implementation: ImageMinimizerPlugin.sharpGenerate,
options: { encodeOptions: { webp: { lossless: true }}}
} : {
type: "asset",
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [[
"imagemin-webp",
{ lossless: true, nearLossless: 0, quality: 100, method: this.build.isProdMode ? 4 : 0 }
]]
} };
}
/**
* @returns {HtmlWebpackPlugin[]}
*/
webapps()
{
const build = this.build,
apps = this._types_.isString(build.entry) ? [ build.entry ] : Object.keys(build.entry);
return apps.map((name) =>
{
const wwwName = name.replace(/[a-z]+([A-Z])/g, (m, g) => m.replace(g, "-" + g.toLowerCase()));
return new HtmlWebpackPlugin(
{
inject: true,
scriptLoading: "module",
chunks: [ name, wwwName ],
template: posix.join(name, `${wwwName}.html`),
inlineSource: build.isProdMode ? ".css$" : undefined,
filename: posix.join(this.compiler.outputPath || build.getDistPath(), "page", `${wwwName}.html`),
minify: !build.isProdMode ? false :
{
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: false,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyCSS: true
}
});
});
}
}
module.exports = WpwWebPlugin.create;