plugins_web_html.js

/**
 * @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;