// @ts-check
/**
* @file src/plugins/sourcemaps.js
* @version 0.0.1
* @copyright @spmhome @_2025
* @author Scott Meesseman @spmeesseman
*//** */
const WpwPlugin = require("../base");
const { apply } = require("@spmhome/type-utils");
/**
* @augments WpwPlugin
*/
class WpwSourceMapsPlugin extends WpwPlugin
{
/**
* @param {WpwPluginOptions} options
*/
constructor(options)
{
super(apply({ buildOptionsNone: true }, options));
this.buildOptions = /** @type {WpwBuildOptionsExportConfig<"devtool">} */(this.buildOptions);
}
/**
* @override
* @param {WpwBuild} b
*/
static create = (b) => WpwSourceMapsPlugin.wrap.call(this, b, !!b.options.hash?.enabled, !!b.options.devtool?.syncHash);
/**
* @override
* @returns {WpwPluginTapOptions<any, any, boolean> | undefined}
*/
onApply()
{
if (this.buildOptions.syncHash !== false || this.build.options.hash?.enabled !== true)
{
return {
renameSourceMaps: {
hook: "compilation",
stage: "DEV_TOOLING",
hookCompilation: "processAssets",
callback: this.renameSourceMaps.bind(this)
},
attachSourceMapsToCopiedModules: {
hook: "compilation",
stage: "DEV_TOOLING",
hookCompilation: "processAssets",
callback: this.attachSourceMaps.bind(this)
}
};
}
}
/**
* @private
* @param {WebpackCompilationAssets} assets
*/
attachSourceMaps = (assets) =>
{
const l = this.hookstart();
Object.entries(assets).filter(([ file ]) => this.isOutputAsset(file)).forEach(([ file ]) =>
{
const asset = this.compilation.getAsset(file);
if (asset && asset.info.copied && !asset.info.related?.sourceMap)
{
const chunkName = this.fileNameStrip(file, true),
srcAssetFile = `${chunkName}.${this.global.hash ? this.global.hash.next[chunkName] : ""}.js`,
srcAsset = this.compilation.getAsset(srcAssetFile);
l.writeMsgTag(file, "chunk: " + chunkName, 1);
l.value(" source asset filename", srcAssetFile, 3);
l.value(" source asset found", !!srcAsset, 3);
l.value(" source asset has sourcemap", !!srcAsset?.info.related?.sourceMap, 3);
if (srcAsset && srcAsset.info.related?.sourceMap)
{
l.write("attaching sourcemap", 3);
l.value(" copied asset filename", file, 3);
l.value(" source asset filename", srcAssetFile, 3);
l.value(" chunk name", chunkName, 4);
l.value(" source asset found", !!srcAsset, 4);
l.value(" source asset has sourcemap", !!srcAsset?.info.related?.sourceMap, 4);
const newInfo = apply({ ...asset.info }, { related: { sourceMap: srcAsset.info.related.sourceMap }});
this.compilation.updateAsset(file, srcAsset.source, newInfo);
}
}
});
this.hookdone();
};
/**
* @private
* @param {WebpackCompilationAssets} assets
*/
renameSourceMaps = (assets) =>
{
const l = this.hookstart(),
compilation = this.compilation;
l.write("replace filename hashes with entry module hash", 1);
Object.entries(assets).filter(([ file ]) => file.endsWith(".map")).forEach(([ file ]) =>
{
const asset = compilation.getAsset(file);
if (asset)
{
const hashKey = this.fileNameStrip(file, true),
entryHash = this.build.global.runtimeVars.next[hashKey],
newFile = this.fileNameStrip(file).replace(/\.([mc]?jsx?)\.map/, (_, g) => `.${entryHash}.${g}.map`);
l.write(` found sourcemap asset italic(${asset.name})`, 1);
l.value(" current filename", file, 2);
l.value(" new filename", newFile, 2);
l.value(" asset info", JSON.stringify(asset.info), 3);
compilation.renameAsset(file, newFile);
const srcAsset = compilation.getAsset(newFile.replace(".map", ""));
if (srcAsset && srcAsset.info.related && srcAsset.info.related.sourceMap)
{
const sources = this.build.wp.sources;
const { source, map } = srcAsset.source.sourceAndMap();
const newInfo = apply(
{ ...srcAsset.info },
{ related: { ...srcAsset.info.related, sourceMap: newFile }}
);
let newSource = source;
l.write(" update source entry asset with new sourcemap filename", 2);
l.value(" source entry asset info", JSON.stringify(srcAsset.info), 3);
newSource = source.toString().replace(file, newFile);
compilation.updateAsset(
srcAsset.name,
new sources.SourceMapSource(newSource, srcAsset.name, map),
newInfo
);
}
}
});
this.hookdone();
};
/**
* @override
* @param {WebpackCompiler} compiler
* @param {boolean} [applyFirst]
* @returns {WebpackPluginInstance | undefined}
*/
getVendorPlugin = (compiler, applyFirst) =>
{
if (applyFirst && this.buildOptions.mode === "plugin")
{
const ext = this.outputAssetRegex,
{ SourceMapDevToolPlugin } = this.build.wp || compiler.webpack,
hash = this.build.options.hash?.enabled === true,
sChk = this._arr_.asArray(this.build.options.optimization?.customCacheGroup)
.reduce((p, c) => `${p}|${c}`, ""),
cChk = this.build.options.output?.name ? "|" + this.build.options.output.name : "";
return new SourceMapDevToolPlugin(
{
test: /\.[mc]jsx?($|\?)/i,
filename: hash ? `[name].[contenthash]${ext}.map` : `[name]${ext}.map`,
exclude: new RegExp(
`(?:node_modules|(?:vendor|spmh[acl]|react|runtime${sChk}${cChk}|workbox|tests)(?:\\.[a-f0-9]{12,32})?${ext})`
),
//
// The bundled node_modules will produce reference tags within the main entry point
// files in the form:
//
// external commonjs "vscode"
// external-node commonjs "crypto"
// ...etc...
//
// This breaks the istanbul reporting library when the tests have completed and the
// coverage report is being built (via nyc.report()). Replace the quote and space
// characters in this external reference name with filename friendly characters.
//
moduleFilenameTemplate: (/** @type {any} */info) =>
{
if ((/["| ]/).test(info.absoluteResourcePath)) {
return info.absoluteResourcePath.replace(/"/g, "").replace(/[ |]/g, "_");
}
return `${info.absoluteResourcePath}`;
},
fallbackModuleFilenameTemplate: hash ? "[absolute-resource-path]?[hash]" : "[absolute-resource-path]"
});
}
};
}
module.exports = WpwSourceMapsPlugin.create;