@atlassian/webresource-webpack-plugin
Version:
Auto-generates web-resource definitions from your webpacked code, for usage in an Atlassian product or plugin.
675 lines (669 loc) • 35.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
// we need this module to be present so that webpack and rspack can resolve it when we rewrite requests to it
require("./shims/empty-wrm-module.js");
const assert_1 = __importDefault(require("assert"));
const crypto_1 = require("crypto");
const fs_1 = require("fs");
const flatMap_1 = __importDefault(require("lodash/flatMap"));
const isObject_1 = __importDefault(require("lodash/isObject"));
const once_1 = __importDefault(require("lodash/once"));
const unionBy_1 = __importDefault(require("lodash/unionBy"));
const uniq_1 = __importDefault(require("lodash/uniq"));
const path_1 = __importDefault(require("path"));
const pretty_data_1 = require("pretty-data");
const url_join_1 = __importDefault(require("url-join"));
const uuid_1 = require("uuid");
const AppResourcesFactory_1 = __importDefault(require("./AppResourcesFactory"));
const base_dependencies_1 = require("./deps/base-dependencies");
const provided_dependencies_1 = require("./deps/provided-dependencies");
const bundler_bridge_1 = require("./helpers/bundler-bridge");
const file_system_1 = require("./helpers/file-system");
const options_parser_1 = require("./helpers/options-parser");
const peer_dependency_checker_1 = require("./helpers/peer-dependency-checker");
const web_resource_generator_1 = require("./helpers/web-resource-generator");
const logger_1 = require("./logger");
const runtime_load_shim_1 = __importDefault(require("./shims/runtime-load-shim"));
const WebpackHelpers_1 = require("./WebpackHelpers");
const WebpackRuntimeHelpers_1 = require("./WebpackRuntimeHelpers");
const WrmManifestPlugin_1 = __importDefault(require("./WrmManifestPlugin"));
const PLUGIN_KEY = 'WRM Plugin';
const defaultResourceParams = new Map().set('svg', [
{
name: 'content-type',
value: 'image/svg+xml',
},
]);
const defaultTransformations = new Map().set('js', ['jsI18n']).set('soy', ['soyTransformer', 'jsI18n']);
const DEFAULT_DEV_ASSETS_HASH = 'DEV_PSEUDO_HASH';
const ASSOCIATIONS_PATH_IN_BUNDLE = './META-INF/fe-manifest-associations';
class WrmPlugin {
static extendTransformations(values) {
return [defaultTransformations, (0, options_parser_1.toMap)(values)].reduce((acc, map) => {
for (const [key, val] of map.entries()) {
const oldVals = acc.get(key);
const newVals = [].concat(oldVals).concat(val).filter(Boolean);
acc.set(key, newVals);
}
return acc;
}, new Map());
}
/**
* A Webpack plugin that takes the compilation tree and creates <web-resource> XML definitions that mirror the
* dependency graph.
*
* This plugin will:
*
* - generate <web-resource> definitions for each entrypoint, along with additional <web-resource> definitions for
* and appropriate dependencies on all chunks generated during compilation.
* - Add <dependency> declarations to each generated <web-resource> as appropriate, both for internal and external
* dependencies in the graph.
* - Add appropriate metadata to the <web-resource> definition, such as appropriate <context>s,
* enabled/disabled state, and more.
* @param {Options} options
*/
constructor(options) {
this.dependencyModuleMap = new Map();
this.resourceModuleMap = new Map();
this.dependencyIssuerMap = new Map();
this.resourceIssuerMap = new Map();
this.resourceAssetsMap = new Map();
this.storeMapEntryInValueSet = (map, key, values) => {
if (!map.has(key)) {
map.set(key, new Set());
}
const iterable = Array.isArray(values) || values instanceof Set ? values : [values];
for (const value of iterable) {
map.get(key).add(value);
}
};
(0, peer_dependency_checker_1.assertNpmPeerDependencies)();
(0, assert_1.default)(options.pluginKey, `Option [String] "pluginKey" not specified. You must specify a valid fully qualified plugin key. e.g.: com.atlassian.jira.plugins.my-jira-plugin`);
(0, assert_1.default)(options.xmlDescriptors, `Option [String] "xmlDescriptors" not specified. You must specify the path to the directory where this plugin stores the descriptors about this plugin, used by the WRM to load your frontend code. This should point somewhere in the "target/classes" directory.`);
(0, assert_1.default)(path_1.default.isAbsolute(options.xmlDescriptors), `Option [String] "xmlDescriptors" must be absolute!`);
// pull out our options
this.options = Object.assign({
addAsyncNameAsContext: false,
addEntrypointNameAsContext: false,
noWRM: false,
useDocumentWriteInWatchMode: false,
verbose: false,
watch: false,
watchPrepare: false,
conditionMap: new Map(),
contextMap: new Map(),
dataProvidersMap: new Map(),
deprecatedEntrypoints: new Map(),
providedDependencies: new Map(),
webresourceKeyMap: new Map(),
resourceParamMap: defaultResourceParams,
transformationMap: defaultTransformations,
devAssetsHash: DEFAULT_DEV_ASSETS_HASH,
locationPrefix: '',
}, options);
(0, logger_1.setVerbose)(this.options.verbose);
this.options.locationPrefix = (0, options_parser_1.extractPathPrefixForXml)(this.options.locationPrefix);
// convert various maybe-objects to maps
this.options.conditionMap = (0, options_parser_1.toMap)(this.options.conditionMap);
this.options.contextMap = (0, options_parser_1.toMap)(this.options.contextMap);
this.options.webresourceKeyMap = (0, options_parser_1.toMap)(this.options.webresourceKeyMap);
// make sure various maps contain only unique items
this.options.resourceParamMap = this.ensureResourceParamsAreUnique((0, options_parser_1.toMap)(this.options.resourceParamMap));
this.options.transformationMap = this.ensureTransformationsAreUnique((0, options_parser_1.toMap)(this.options.transformationMap));
this.options.providedDependencies = this.ensureProvidedDependenciesAreUnique((0, options_parser_1.toMap)(this.options.providedDependencies));
this.options.dataProvidersMap = this.ensureDataProvidersMapIsValid((0, options_parser_1.toMap)(this.options.dataProvidersMap));
this.options.deprecatedEntrypoints = (0, options_parser_1.toMap)(this.options.deprecatedEntrypoints);
this.getAssetsUUID = (0, once_1.default)(this.getAssetsUUID.bind(this));
}
/**
* Generate an asset uuid per build - this is used to ensure we have a new "cache" for our assets per build.
* As JIRA-Server does not "rebuild" too often, this can be considered reasonable.
*/
getAssetsUUID(isProduction) {
return isProduction ? (0, uuid_1.v4)() : this.options.devAssetsHash;
}
ensureTransformationsAreUnique(transformations) {
transformations.forEach((val, key, map) => {
const values = [].concat(val).filter(Boolean);
map.set(key, (0, uniq_1.default)(values));
});
return transformations;
}
ensureResourceParamsAreUnique(params) {
params.forEach((val, key, map) => {
const values = [].concat(val).filter(Boolean);
map.set(key, (0, unionBy_1.default)(values.reverse(), 'name').reverse());
});
return params;
}
ensureProvidedDependenciesAreUnique(providedDependencies) {
const result = new Map(provided_dependencies_1.builtInProvidedDependencies);
for (const [name, providedDependency] of providedDependencies) {
if (result.has(name)) {
continue;
}
result.set(name, providedDependency);
}
(0, logger_1.log)('Using provided dependencies', Array.from(result));
return result;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
ensureDataProvidersMapIsValid(dataProvidersMap) {
const map = new Map();
const requiredKeys = ['key', 'class'];
for (const [entryPoint, dataProviders] of dataProvidersMap) {
if (!Array.isArray(dataProviders)) {
(0, logger_1.error)(`The value of data providers for "${entryPoint}" entry point should be an array of data providers.`, {
entryPoint,
dataProviders,
});
continue;
}
const validDataProviders = [];
for (const dataProvider of dataProviders) {
const keys = (0, isObject_1.default)(dataProvider) ? Object.keys(dataProvider) : [];
const isValidShape = requiredKeys.every((key) => keys.includes(key));
if (!isValidShape) {
(0, logger_1.error)(`The data provider shape for "${entryPoint}" entry point doesn't include required keys: ${requiredKeys.concat(', ')}.`, { entryPoint, dataProvider });
continue;
}
const { key, class: providerClass } = dataProvider;
if (!key || !providerClass) {
(0, logger_1.error)(`The data provider shape for "${entryPoint}" entry point contains missing or empty values.`, {
entryPoint,
key,
class: providerClass,
});
continue;
}
validDataProviders.push({
key,
class: providerClass,
});
}
if (validDataProviders.length) {
map.set(entryPoint, validDataProviders);
}
}
return map;
}
checkConfig(compiler) {
const ensureJsonPFunction = () => {
const outputOptions = compiler.options.output;
// @ts-expect-error TODO: Check if this is still valid with Webpack 5 as types suggest otherwise
const { jsonpFunction } = outputOptions;
if (!jsonpFunction || jsonpFunction === 'webpackJsonp') {
const generatedJsonpFunction = `atlassianWebpackJsonp${(0, crypto_1.createHash)('md5')
.update(this.options.pluginKey, 'utf8')
.digest('hex')}`;
(0, logger_1.warn)(`
*********************************************************************************
The output.jsonpFunction is not specified. This needs to be done to prevent clashes.
An automated jsonpFunction name for this plugin was created:
"${generatedJsonpFunction}"
*********************************************************************************
`);
// @ts-expect-error TODO: Check if this is still valid with Webpack 5 as types suggest otherwise
outputOptions.jsonpFunction = generatedJsonpFunction;
}
};
/**
* In Webpack 5 / Rspack, the runtime uses output.chunkLoadingGlobal as the global for
* loading chunks. When multiple apps using this plugin load on the same page, each must
* have a distinct chunkLoadingGlobal so chunk/module IDs don't clash.
*/
const ensureChunkLoadingGlobal = () => {
const outputOptions = compiler.options.output;
const current = outputOptions.chunkLoadingGlobal;
// If context is set to directory without package.json chunkLoadingGlobal is set to `webpackChunk` by default
// if context is set to directory with package.json chunkLoadingGlobal is set to `webpackChunk_normalized_package_name` by default
// for consistency of having `webpackChunkWrm` prefix we're going to override either way
if (!current || current.startsWith('webpackChunk')) {
const generated = this.options.chunkLoadingGlobal ??
`webpackChunkWrm${(0, crypto_1.createHash)('md5').update(this.options.pluginKey, 'utf8').digest('hex')}`;
(0, logger_1.warn)(`
*********************************************************************************
output.chunkLoadingGlobal is not set. This is required when multiple apps load on one page.
An automated chunkLoadingGlobal for this plugin was set (derived from pluginKey):
"${generated}"
This gives each app its own chunk loading global and avoids runtime conflicts.
*********************************************************************************
`);
outputOptions.chunkLoadingGlobal = generated;
}
};
const ensureDevToolsAreDisabledForPrepare = () => {
const devToolOption = compiler.options.devtool;
if (this.options.watchPrepare && devToolOption) {
const message = `Having "devtool" option set to anything but "false" during the "watch-prepare" is invalid. It was set to: "${devToolOption}".`;
(0, logger_1.error)(message);
compiler.options.devtool = false;
// report issue to webpack
compiler.hooks.thisCompilation.tap('WarnNoDevToolInWatchPrepare', (compilation) => {
compilation.warnings.push((0, bundler_bridge_1.createError)(compiler, message));
});
}
};
const ensureOutputPath = () => {
const outputPath = compiler.options.output.path;
if (!outputPath) {
throw new Error(`No "output.path" specified in your webpack configuration. This is required! Stopping.`);
}
};
const ensurePublicPath = () => {
if (this.shouldOverwritePublicPath()) {
if (compiler.options.output.publicPath) {
(0, logger_1.warn)('Public path is overridden in runtime to handle product context and WRM static resource resolution');
}
compiler.options.output.publicPath = '';
}
};
const ensurePackageName = () => {
if (!this.options.packageName) {
let packageName;
// Find package closest to context
let directory = compiler.context;
while (directory !== path_1.default.parse(directory).root) {
const packageJsonPath = path_1.default.join(directory, 'package.json');
if ((0, fs_1.existsSync)(packageJsonPath)) {
const packageJson = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, 'utf8'));
packageName = packageJson.name;
break;
}
directory = path_1.default.dirname(directory);
}
if (!packageName) {
throw new Error(`WrmPlugin expects Webpack context to be NPM package. Current context ${compiler.context} is not an NPM package.`);
}
this.options.packageName = packageName;
}
};
const ensureMiniCssExtractPluginHasDisabledRuntime = () => {
const plugins = compiler.options.plugins;
const miniCssExtractPlugin = plugins.find(
// we don't want to lock this check to a specific MiniCssExtractPlugin version
// so we can't use instanceof
(plugin) => plugin && ['MiniCssExtractPlugin', 'CssExtractRspackPlugin'].includes(plugin.constructor?.name ?? ''));
if (miniCssExtractPlugin && 'options' in miniCssExtractPlugin && miniCssExtractPlugin.options.runtime !== false) {
(0, logger_1.warn)(`
*********************************************************************************
MiniCssExtractPlugin runtime option needs to be disabled so that it doesn't
interfere with Web Resource Manager runtime.
*********************************************************************************
`);
// report issue to webpack
compiler.hooks.compilation.tap('WarnMiniCssExtractPluginRuntimeEnabled', (compilation) => {
const warningMessage = `MiniCssExtractPlugin runtime option needs to be disabled so that it doesn't interfere with Web Resource Manager runtime.`;
compilation.warnings.push((0, bundler_bridge_1.createError)(compiler, warningMessage));
});
}
};
const ensureProvidedDependenciesInExternals = () => {
const providedDependenciesAsExternals = {};
for (const [key, value] of this.options.providedDependencies.entries()) {
providedDependenciesAsExternals[key] = value.import;
}
const externals = compiler.options.externals;
if (!externals) {
compiler.options.externals = providedDependenciesAsExternals;
return;
}
if (Array.isArray(externals)) {
compiler.options.externals = [...externals, providedDependenciesAsExternals];
return;
}
compiler.options.externals = [externals, providedDependenciesAsExternals];
};
const ensureAmdIsEnabled = () => {
const amdOption = compiler.options.amd;
if (!amdOption) {
(0, logger_1.warn)(`
*********************************************************************************
The "amd" option is disabled in your configuration. This will cause issues with
AMD modules that are common in the Atlassian ecosystem.
The option has been set to "{}" automatically.
*********************************************************************************
`);
compiler.options.amd = {};
}
};
compiler.hooks.afterEnvironment.tap('Check Config', () => {
ensureJsonPFunction();
ensureChunkLoadingGlobal();
ensureDevToolsAreDisabledForPrepare();
ensureOutputPath();
ensurePublicPath();
ensurePackageName();
ensureMiniCssExtractPluginHasDisabledRuntime();
ensureProvidedDependenciesInExternals();
ensureAmdIsEnabled();
});
}
overwritePublicPath(compiler) {
const isProductionMode = (0, WebpackHelpers_1.isRunningInProductionMode)(compiler);
const uuid = this.getAssetsUUID(isProductionMode);
const assetWebresource = `${this.options.pluginKey}:assets-${uuid}`;
const RuntimeGlobals = (0, bundler_bridge_1.getRuntimeGlobals)(compiler);
const override = `
${RuntimeGlobals.publicPath} = "";
if (typeof AJS !== "undefined") {
${RuntimeGlobals.publicPath} = AJS.contextPath() + "/s/${uuid}/_/download/resources/${assetWebresource}/";
}
`;
compiler.hooks.compilation.tap(PLUGIN_KEY, (compilation) => {
compilation.hooks.runtimeModule.tap(PLUGIN_KEY, (module) => {
if (['public_path', 'publicPath'].includes(module.name)) {
(0, base_dependencies_1.addBaseDependency)('com.atlassian.plugins.atlassian-plugins-webresource-plugin:context-path');
if ((0, bundler_bridge_1.isRspackCompiler)(compiler)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
module.source.source = Buffer.from(override, 'utf8');
}
else {
module.generate = () => override;
}
}
});
});
}
storeModuleDependency(moduleKey, fileDependencies, dependencyRequest) {
// get globally available libraries through wrm
if (this.options.providedDependencies.has(dependencyRequest)) {
(0, logger_1.log)('plugging hole into request to %s, will be provided as a dependency through WRM', dependencyRequest);
const providedDependency = this.options.providedDependencies.get(dependencyRequest).dependency;
this.storeMapEntryInValueSet(this.dependencyModuleMap, moduleKey, providedDependency);
}
[...fileDependencies.keys()].forEach((fileDependency) => {
if (this.dependencyIssuerMap.has(fileDependency)) {
const dependencyRequests = this.dependencyIssuerMap.get(fileDependency);
this.storeMapEntryInValueSet(this.dependencyModuleMap, moduleKey, dependencyRequests);
}
if (this.resourceIssuerMap.has(fileDependency)) {
const resourceRequests = this.resourceIssuerMap.get(fileDependency);
this.storeMapEntryInValueSet(this.resourceModuleMap, moduleKey, resourceRequests);
}
});
}
hookUpModuleDependencies(compiler) {
compiler.hooks.compilation.tap(PLUGIN_KEY, (compilation) => {
compilation.hooks.finishModules.tap(PLUGIN_KEY, (modules) => {
for (const module of modules) {
const moduleKey = module.identifier();
const moduleResource = module.resource;
const moduleFileDeps = module.buildInfo?.fileDependencies ||
new Set(moduleResource ? [moduleResource] : []);
for (const dep of module.dependencies) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dependencyRequest = dep.request;
if (dependencyRequest) {
this.storeModuleDependency(moduleKey, moduleFileDeps, dependencyRequest);
}
}
}
});
});
}
// eslint-disable-next-line sonarjs/cognitive-complexity
hookUpWrInlineLoaders(compiler) {
const isRspack = (0, bundler_bridge_1.isRspackCompiler)(compiler);
const WR_DEPENDENCY = 'wr-dependency!';
const WR_RESOURCE = 'wr-resource!';
const processModuleDependencies = (resolveData) => {
// handle custom imports for resources and dependencies
const issuerKey = resolveData.contextInfo.issuer;
const { request, context } = resolveData;
if (request.startsWith(WR_DEPENDENCY)) {
(0, logger_1.log)('adding %s as a web-resource dependency through WRM', request);
const pluginKey = this.options.pluginKey;
const dependency = request.slice(WR_DEPENDENCY.length);
const dependencyKey = dependency.includes(':') ? dependency : `${pluginKey}:${dependency}`;
this.storeMapEntryInValueSet(this.dependencyIssuerMap, issuerKey, dependencyKey);
resolveData.request = path_1.default.resolve(__dirname, './shims/empty-wrm-module.js');
}
if (request.startsWith(WR_RESOURCE)) {
(0, logger_1.log)('adding %s as a resource through WRM', request);
const requestContext = context;
const rootContext = compiler.options.context;
const resource = request.slice(WR_RESOURCE.length);
const parts = resource.split('!');
const name = parts[0];
let location = parts[1] || name;
if (location.startsWith('./') || location.startsWith('../')) {
const fullResourcePath = path_1.default.join(requestContext, location);
location = path_1.default.relative(rootContext, fullResourcePath);
}
this.storeMapEntryInValueSet(this.resourceIssuerMap, issuerKey, {
name,
location: location || name,
});
resolveData.request = path_1.default.resolve(__dirname, './shims/empty-wrm-module.js');
}
return undefined;
};
if (isRspack) {
compiler.hooks.compilation.tap(PLUGIN_KEY, (_compilation, { normalModuleFactory }) => {
normalModuleFactory.hooks.beforeResolve.tap(PLUGIN_KEY, processModuleDependencies);
});
}
else {
compiler.hooks.normalModuleFactory.tap(PLUGIN_KEY, (normalModuleFactory) => {
normalModuleFactory.hooks.beforeResolve.tap(PLUGIN_KEY, processModuleDependencies);
});
}
}
/**
* We need to handle CSS url(...) assets so that we can include them as resources in the web-resource,
* and so they can be correctly relativized by a transformer from WRM.
* In a very specific setup (see `test/test-cases/asset-loading-via-css`), we can't rely on chunk.auxiliaryFiles
* and need to track these assets manually.
*/
hookUpCssUrlAssets(compiler) {
compiler.hooks.compilation.tap(PLUGIN_KEY, (compilation) => {
compilation.hooks.processAssets.tap({
name: PLUGIN_KEY,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
stage: compilation.constructor.PROCESS_ASSETS_STAGE_OPTIMIZE,
}, (assets) => {
for (const assetName of Object.keys(assets)) {
const assetDependencies = new Set();
if (assetName.endsWith('.css')) {
const source = assets[assetName].source().toString();
const urlRegex = /url\(([^)]+)\)/g;
let match;
while ((match = urlRegex.exec(source))) {
const assetReference = match[1]
.replace(/['"]/g, '')
.replace(/^\//, '') // paths can be absolute
.replace(/#.*/g, '') // remove svg mask id
.trim();
assetDependencies.add(assetReference);
}
}
this.resourceAssetsMap.set(assetName, assetDependencies);
}
});
});
}
/**
* Ensure the WRM.require function is available at runtime and is used to load any code-split chunks.
*/
enableAsyncLoadingWithWRM(compiler) {
compiler.hooks.compilation.tap('enable async loading with wrm - compilation', (compilation) => {
const hook = (0, bundler_bridge_1.isRspackCompiler)(compiler)
? compiler.rspack.RuntimePlugin.getCompilationHooks(compilation).createScript
: // eslint-disable-next-line @typescript-eslint/no-var-requires,n/no-missing-require
require('webpack/lib/runtime/LoadScriptRuntimeModule').getCompilationHooks(compilation).createScript;
// FIXME any type
hook.tap('enable async loading with wrm - jsonp-script', (_source, chunk) => {
// TODO: understand how to set this data on chunk "properly" so that
// our normalModuleFactory hook will pick it up and generate this dep for us.
// @ts-expect-error We know we shouldn't set a random attribute...
chunk.needsWrmRequire = true;
// Add the WRM async loader in to webpack's loader function.
return (0, runtime_load_shim_1.default)(this.options.pluginKey, this.options.watch, compiler.options.output.publicPath);
});
});
}
shouldOverwritePublicPath() {
return !this.options.watch && !this.options.noWRM;
}
shouldEnableAsyncLoadingWithWRM() {
return !this.options.noWRM;
}
clearInternalMaps() {
this.dependencyModuleMap.clear();
this.resourceModuleMap.clear();
this.dependencyIssuerMap.clear();
this.resourceIssuerMap.clear();
this.resourceAssetsMap.clear();
}
// eslint-disable-next-line sonarjs/cognitive-complexity
apply(compiler) {
// ensure settings make sense
this.checkConfig(compiler);
// ensure global records are cleared
this.clearInternalMaps();
// hook up external dependencies
this.hookUpModuleDependencies(compiler);
// allow `wr-dependency/wr-resource` inline loaders.
this.hookUpWrInlineLoaders(compiler);
// rewrite css url loader to public path
this.hookUpCssUrlAssets(compiler);
if (this.shouldOverwritePublicPath()) {
this.overwritePublicPath(compiler);
}
if (this.shouldEnableAsyncLoadingWithWRM()) {
this.enableAsyncLoadingWithWRM(compiler);
}
const outputPath = compiler.options.output.path;
const isProductionMode = (0, WebpackHelpers_1.isRunningInProductionMode)(compiler);
const assetsUUID = this.getAssetsUUID(isProductionMode);
// Generate a 1:1 mapping from original filenames to compiled filenames
compiler.hooks.compilation.tap('wrm plugin setup phase', (compilation) => {
const moduleLoader = (0, bundler_bridge_1.isRspackCompiler)(compiler)
? compiler.rspack.NormalModule.getCompilationHooks(compilation).loader
: // eslint-disable-next-line @typescript-eslint/no-var-requires
require('webpack/lib/NormalModule').getCompilationHooks(compilation).loader;
moduleLoader.tap('wrm plugin - normal module', (loaderContext, module) => {
const { emitFile } = loaderContext;
loaderContext.emitFile = (name, content, sourceMap) => {
return emitFile.call(module, name, content, sourceMap);
};
});
});
const appResourcesFactory = new AppResourcesFactory_1.default({
assetsUUID,
options: this.options,
dependencyModuleMap: this.dependencyModuleMap,
resourceModuleMap: this.resourceModuleMap,
resourceAssetsMap: this.resourceAssetsMap,
});
/**
* Given a completed compilation, determine where each file generated
* by webpack should be referenced in a web-resource.
*
* @param compilation the finalised compilation for the build
* @returns a list of {@see ChunkResourceDescriptor} objects which describe a web-resource bundle
*/
const getWebResourceDescriptors = (compilation) => {
const appResourceGenerator = appResourcesFactory.build(compiler, compilation);
return appResourceGenerator.getResourceDescriptors();
};
/**
* Given a list of web-resource descriptors, write their definitions
* to XML, ready for the WRM to pick them up at product runtime.
*
* @param descriptors the list of web-resources to write definitions for
*/
const generateWebResourceXmlReport = (descriptors) => {
const webResources = descriptors.map((descriptor) => (0, web_resource_generator_1.renderWebResource)(descriptor, descriptors, this.options));
const xmlDescriptorsFilepath = this.options.xmlDescriptors;
const xmlDescriptors = pretty_data_1.pd.xml(`<bundles>${webResources.join('')}</bundles>`);
(0, file_system_1.writeFileSync)(xmlDescriptorsFilepath, xmlDescriptors);
};
/**
* Given a list of web-resource descriptors, identify JS assets and list them in
* association report.
*/
const generateAssociationReport = (buildOutputPath, descriptors) => {
const files = [...new Set(descriptors.map((descriptor) => descriptor.resources).flat()).values()];
const output = {
packageName: this.options.packageName,
outputDirectoryFiles: files,
};
const jsonOutput = JSON.stringify(output, null, 2);
const fullOutputPath = path_1.default.join(buildOutputPath, ASSOCIATIONS_PATH_IN_BUNDLE, `${this.options.pluginKey}-webpack.intermediary.json`);
(0, file_system_1.writeFileSync)(fullOutputPath, jsonOutput);
};
/**
* Given a list of web-resource descriptors, for each that
* references javascript files, overwrite them with new files that
* will request content from the webpack-dev-server after the WRM
* loads them at product runtime.
*
* @param descriptors the list of web-resources to scan and find
* javascript files within
*/
const generateHotModuleRedirectFiles = (descriptors) => {
const redirectDescriptors = (0, flatMap_1.default)(descriptors, (c) => c.resources)
.filter((res) => path_1.default.extname(res) === '.js')
.map((r) => ({ fileName: r, writePath: path_1.default.join(outputPath, r) }));
const overwriteFiles = () => {
const generateAssetCall = (fileName) => {
// TODO: should we just pretend `publicPath` is a string here?
const pathName = (0, url_join_1.default)(compiler.options.output.publicPath, fileName);
const appendScript = `
var script = document.createElement('script');
script.src = '${pathName}';
script.async = false;
script.crossOrigin = 'anonymous';
document.head.appendChild(script);`.trim();
if (this.options.useDocumentWriteInWatchMode) {
return `
!function(){
if (document.readyState === "loading" && 'initiallyRendered' in document.currentScript.dataset) {
document.write('<script src="${pathName}"></script>')
} else {
${appendScript}
}
}();
`;
}
return `!function() { ${appendScript} }();`;
};
for (const { fileName, writePath } of redirectDescriptors) {
(0, file_system_1.writeFileSync)(writePath, generateAssetCall(fileName));
}
};
compiler.hooks.afterDone.tap('wrm plugin - add watch mode modules', overwriteFiles);
};
/**
* After the compilation is complete, we analyse the result to produce our descriptors,
* which we then use to generate various metadata files in the build output.
*/
(0, WebpackRuntimeHelpers_1.hookIntoCompileDoneToGenerateReports)('wrm plugin - generate descriptors', compiler, (compilation, cb) => {
const descriptors = getWebResourceDescriptors(compilation);
// write the xml for web-resource module descriptors
generateWebResourceXmlReport(descriptors);
// generate association report in case packageName is provided
if (this.options.packageName) {
generateAssociationReport(compiler.outputPath, descriptors);
}
// write javascript files to enable hot-reloading at dev time
if (this.options.watch && this.options.watchPrepare) {
generateHotModuleRedirectFiles(descriptors);
}
cb();
});
// Enable manifest output if provided
if (this.options.wrmManifestPath) {
const filename = path_1.default.isAbsolute(this.options.wrmManifestPath)
? this.options.wrmManifestPath
: path_1.default.resolve(path_1.default.join(outputPath, this.options.wrmManifestPath));
new WrmManifestPlugin_1.default(appResourcesFactory, filename, this.options.pluginKey).apply(compiler);
}
}
}
module.exports = WrmPlugin;
//# sourceMappingURL=WrmPlugin.js.map