plugins_runtimevars.js

/**
 * @file plugin/runtimevars.js
 * @copyright @spmhome @_2025
 * @author Scott Meesseman @spmeesseman
 *//** */

const WpwCache = require("../services/cache");
const WpwPlugin = require("./base");
const { apply } = require("@spmhome/type-utils");


/**
 * @augments WpwPlugin
 */
class WpwRuntimeVarsPlugin extends WpwPlugin
{
    /**
     * @param {WpwPluginOptions} options Plugin options to be applied
     */
	constructor(options)
    {
        super(options);
        this.buildOptions = /** @type {WpwBuildOptionsConfig<"runtimevars">} */(this.buildOptions);
    }


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


    /**
     * @override
     * @returns {WpwPluginTapOptions<any, any, boolean>}
     */
    onApply()
    {
        return {
            replaceRuntimePlaceholderVariables: {
                hook: "compilation",
                stage: "ADDITIONS",
                statsProperty: this.optionsKey,
                callback: this.runtimeVars.bind(this)
            }
        };
    }


    /**
     * @private
     * @param {WebpackAssetInfo} info
     * @returns {WebpackAssetInfo}
     */
    info = (info) => apply({ ...(info || {}) }, { runtimeVars: true });


    /**
     * @private
     * @param {WebpackCompilationAssets} assets
     */
    runtimeVars(assets)
    {
        this.hookstart("replace runtime placeholder variables");
        try
        {   Object.entries(assets).filter(([ file ]) => this.isOutputAsset(file)).forEach(([ file ]) =>
            {
                this.build.logger.write(`   queue asset '${file}' for variable replacement`, 2);
                this.compilation.updateAsset(file, source => this.source(file, source), this.info.bind(this))
            }, this);
        }
        catch (e)
        {   this.addMessage({
                exception: e, code: this.MsgCode.ERROR_PLUGIN_HOOK_FAILED,
                message: "runtime placeholder variable replacement pllugin failed"
            })
        }
        this.hookdone("runtime placeholder variable replacement completed");
    };


    /**
     * Performs all source code modifications
     * @private
     * @param {string} file
     * @param {WebpackSource} sourceInfo
     * @returns {WebpackSource}
     */
    source(file, sourceInfo)
    {
        const sourceCode = this.sourceUpdateVars(file, sourceInfo.source().toString());
        return this.sourceObj(file, sourceCode, sourceInfo);
    }


    /**
     * @private
     * @param {string} file
     * @param {string | Buffer} content
     * @param {WebpackSource} sourceInfo
     * @returns {WebpackSource}
     */
    sourceObj(file, content, sourceInfo)
    {
        const { source, map } = sourceInfo.sourceAndMap();
        return map && this.build.options.devtool?.enabled ?
               new this.build.wp.sources.SourceMapSource(content, file, map, source) :
               new this.build.wp.sources.RawSource(content);
    }


    /**
     * Performs source code modifications and populates predefined build-time variable names
     * with their respective values, e.g. :
     *
     *     __WPW__.contentHash.[chunkName]
     *
     * Where 'chunkName' is one of `vendor`, `runtime`, `workbox` or the build.name/alias / sharedChunkName
     *
     * @private
     * @param {string} file
     * @param {string} sourceCode
     * @returns {string}
     * @throws {Error}
     */
    sourceUpdateVars(file, sourceCode)
    {
        let rCt = 0;
        const l = this.logger;

        l.value("   process source code for placeholder variables", file, 1);
        l.write("      process variable[1] 'contenthash'", 2);

        for (const asset of this.compilation.getAssets().filter((a) => this.isOutputAsset(a.name, true)))
        {
            let idx = -1, varCt = 0, varCt2 = 0;
            const chunk = this.fileNameStrip(asset.name, true);
            while ((idx = sourceCode.indexOf("__WPW__", idx + 1)) !== -1) {
                varCt++;
            }
            if (varCt > 0)
            {
                const hash = asset.info.contenthash;
                if (this._types_.isString(hash))
                {
                    const regex = new RegExp(`(?:.+?\\.)?__WPW__\\.contentHash(?:\\.|\\[ *")${chunk}(?:" *\\])?`, "gmi");
                    this.logger.write(`        process asset '${chunk}' with hash '${hash}'`, 2);
                    sourceCode = sourceCode.replace(regex, `"${hash}"`);
                    idx = -1;
                    while ((idx = sourceCode.indexOf("__WPW__", idx + 1)) !== -1) {
                        varCt2++;
                    }
                    if (varCt2 !== 0) {
                        this.logger.warn(`         unable to set ${varCt2} contenthash placeholders for '${chunk}', hash ${hash}`)
                        break;
                    }
                    rCt += varCt;
                    this.logger.write(`         found and replaced '${varCt}' placeholders`, 1);
                }
                else {
                    throw new Error(`unable to process ${varCt} placeholder variable(s), contenthash not a string`)
                }
            }
        }
        l.write(`      completed ${rCt} replacement for 'contenthash'`, 2);
        l.write("   completed placeholder variable replacement for " + file, 1);
        return sourceCode;
    }


}


module.exports = WpwRuntimeVarsPlugin.create;