const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
class BootloaderPlugin {
constructor(htmlWebpackPlugin, options) {
this.htmlWebpackPlugin = htmlWebpackPlugin;
this.options = {
name: 'bootloader',
...options,
};
}
isBootloaderScriptTag(tag, bootloaderFiles) {
if (tag.tagName !== 'script' || !(tag.attributes && tag.attributes.src)) {
return true;
}
return bootloaderFiles.has(tag.attributes.src);
}
isBootloaderStyleTag(tag, bootloaderFiles) {
if (tag.tagName !== 'link' || !(tag.attributes && tag.attributes.href)) {
return true;
}
return bootloaderFiles.has(tag.attributes.href);
}
inlineScriptTag(publicPath, assets, tag) {
if (tag.tagName !== 'script' || !(tag.attributes && tag.attributes.src)) {
return tag;
}
const scriptName = publicPath
? tag.attributes.src.replace(publicPath, '')
: tag.attributes.src;
const asset = assets[scriptName];
if (asset == null) {
return tag;
}
return { tagName: 'script', innerHTML: asset.source(), closeTag: true };
}
inlineStyleTag(publicPath, assets, tag) {
if (tag.tagName !== 'link' || !(tag.attributes && tag.attributes.href)) {
return tag;
}
const scriptName = publicPath
? tag.attributes.href.replace(publicPath, '')
: tag.attributes.href;
const asset = assets[scriptName];
if (asset == null) {
return tag;
}
return { tagName: 'style', innerHTML: asset.source(), closeTag: true };
}
processHtmlAsset(publicPath, src, assets, excludeFiles, result) {
const scriptName = publicPath ? src.replace(publicPath, '') : src;
if (excludeFiles.has(scriptName)) {
return;
}
const asset = assets[scriptName];
if (!asset) {
return;
}
result.push({
file: src,
size: asset.size(),
});
}
apply(compiler) {
const isProductionLikeMode =
compiler.options.mode === 'production' || !compiler.options.mode;
if (!isProductionLikeMode) {
return;
}
let publicPath = compiler.options.output.publicPath || '';
if (publicPath && !publicPath.endsWith('/')) {
publicPath += '/';
}
const htmlAssets = {
js: [],
css: [],
};
compiler.hooks.entryOption.tap('BootloaderPlugin', (context) => {
compiler.hooks.make.tapAsync(
'BootloaderPlugin',
(compilation, callback) => {
const entry = SingleEntryPlugin.createDependency(
this.options.script,
this.options.name
);
compilation.addEntry(context, entry, this.options.name, callback);
}
);
});
compiler.hooks.thisCompilation.tap('BootloaderPlugin', (compilation) => {
compilation.hooks.afterOptimizeChunks.tap('BootloaderPlugin', () => {
const entrypoint = compilation.entrypoints.get(this.options.name);
if (entrypoint) {
const newChunk = compilation.addChunk(this.options.name);
for (const chunk of Array.from(entrypoint.chunks)) {
if (chunk === newChunk) {
continue;
}
for (const module of chunk.getModules()) {
chunk.moveModule(module, newChunk);
}
entrypoint.removeChunk(chunk);
const index = compilation.chunks.indexOf(chunk);
if (index > -1) {
compilation.chunks.splice(index, 1);
}
compilation.namedChunks.delete(chunk.name);
}
entrypoint.pushChunk(newChunk);
entrypoint.setRuntimeChunk(newChunk);
}
});
const hooks = this.htmlWebpackPlugin.getHooks(compilation);
hooks.beforeAssetTagGeneration.tap('BootloaderPlugin', ({ assets }) => {
const entrypoint = compilation.entrypoints.get(this.options.name);
if (entrypoint) {
const bootloaderFiles = new Set(entrypoint.getFiles());
assets.js.forEach((src) =>
this.processHtmlAsset(
assets.publicPath,
src,
compilation.assets,
bootloaderFiles,
htmlAssets.js
)
);
assets.css.forEach((src) =>
this.processHtmlAsset(
assets.publicPath,
src,
compilation.assets,
bootloaderFiles,
htmlAssets.css
)
);
}
});
hooks.alterAssetTags.tap('BootloaderPlugin', ({ assetTags }) => {
const entrypoint = compilation.entrypoints.get(this.options.name);
if (entrypoint) {
const bootloaderFiles = new Set(
entrypoint.getFiles().map((filename) => publicPath + filename)
);
assetTags.scripts = assetTags.scripts
.filter((tag) => this.isBootloaderScriptTag(tag, bootloaderFiles))
.map((tag) =>
this.inlineScriptTag(publicPath, compilation.assets, tag)
);
assetTags.styles = assetTags.styles
.filter((tag) => this.isBootloaderStyleTag(tag, bootloaderFiles))
.map((tag) =>
this.inlineStyleTag(publicPath, compilation.assets, tag)
);
const assetSource = `!function(){var bootloader={};bootloader.assets=${JSON.stringify(
htmlAssets
)};window.$bootloader=bootloader;}();`;
assetTags.scripts.unshift({
tagName: 'script',
innerHTML: assetSource,
closeTag: true,
});
entrypoint.getFiles().forEach((filename) => {
delete compilation.assets[filename];
});
}
});
});
}
}
module.exports = BootloaderPlugin;