html-bundler-webpack-plugin
Version:
Generates complete single-page or multi-page website from source assets. Build-in support for Markdown, Eta, EJS, Handlebars, Nunjucks, Pug. Alternative to html-webpack-plugin.
301 lines (252 loc) • 9.19 kB
JavaScript
const path = require('path');
const Collection = require('./Collection');
const { resolveException } = require('./Messages/Exception');
const Snapshot = require('./Snapshot');
class Resolver {
fs = null;
/**
* @type {string} The issuer filename of required the file.
*/
issuerFile = '';
/**
* @type {FileInfo} The issuer of required the file.
*/
issuer = {};
/**
* @type {AssetEntryOptions} The current entry point where are resolved all dependencies.
*/
entryPoint = {};
/**
* @type {string} The context directory of required the file.
*/
context = '';
/**
* The cache of resolved source files. Defined at one time.
*
* @type {Map<string, Map<string, string>>}
*/
sourceFiles = new Map();
/**
* The data of assets to resolving output assets.
* For each new chunk must be cleaned.
* Note: the same module can have many issuers and can be saved under different asset filenames.
*
* @type {Map<string, {assets:Map, originalFilename?:string, resolve?:(issuer:FileInfo) => string}>}
*/
data = new Map();
pluginOption = null;
collection = null;
assetEntry = null;
assetInline = null;
constructor({ pluginOption, assetEntry, assetInline, collection }) {
this.pluginOption = pluginOption;
this.collection = collection;
this.assetEntry = assetEntry;
this.assetInline = assetInline;
}
/**
* @param {FileSystem} fs
*/
init({ fs }) {
this.fs = fs;
// webpack root context path
this.rootContext = this.pluginOption.context;
// bind this context to the method for using in VM context
this.require = this.require.bind(this);
}
/**
* Set the current issuer and the entry point.
*
* @param {Object} entryPoint The current entry point.
* @param {FileInfo} issuer The issuer.
*/
setContext(entryPoint, issuer) {
const [file] = issuer.resource.split('?', 1);
this.context = path.dirname(file);
this.entryPoint = entryPoint;
this.issuer = issuer;
this.issuerFile = file;
}
/**
* Add the context and resolved path of the resource to resolve it in require() at render time.
*
* @param {{resource: string, filename: string, resolve?: (FileInfo)=> string}} assetInfo The asset info.
*/
addAsset(assetInfo) {
let sourceFile = path.resolve(assetInfo.resource);
let resourceData = this.data.get(sourceFile);
if (!resourceData) {
resourceData = {
assets: new Map(),
sourceFile,
};
this.data.set(sourceFile, resourceData);
}
if (assetInfo.resolve) {
resourceData.resolve = assetInfo.resolve;
} else {
resourceData.originalFilename = assetInfo.filename;
}
}
/**
* Resolve the full path of asset source file.
*
* @param {string} rawRequest The raw request of resource.
* @param {string} issuer The issuer of resource.
* @return {string|null} The resolved full path of resource.
*/
resolveResource(rawRequest, issuer) {
let resource = this.sourceFiles.get(issuer)?.get(rawRequest);
if (resource) return resource;
// normalize request, e.g., the relative `path/to/../to/file` path to absolute `path/to/file`
resource = path.resolve(this.context, rawRequest);
const [file] = resource.split('?', 1);
if (rawRequest.startsWith(this.context) || this.fs.existsSync(file)) {
this.addSourceFile(resource, rawRequest, issuer);
return resource;
}
return null;
}
/**
* Add resolved source file to data.
*
* @param {string} sourceFile The resolved full path of resource.
* @param {string} rawRequest The rawRequest of resource.
* @param {string} issuer The issuer of resource.
*/
addSourceFile(sourceFile, rawRequest, issuer) {
let item = this.sourceFiles.get(issuer);
if (item == null) {
this.sourceFiles.set(issuer, new Map([[rawRequest, sourceFile]]));
} else {
item.set(rawRequest, sourceFile);
}
}
/**
* Get the key of asset file to save it as resolved under its issuer.
*
* Note: the key can be an output filename when the issuer is a current entry point
* otherwise is a source file of an issuer, e.g., a style.
*
* @param {string} issuer
* @param {AssetEntryOptions} entryPoint
*/
getAssetKey(issuer, entryPoint) {
return issuer === entryPoint?.resource ? entryPoint.filename : issuer;
}
/**
* Resolve output filename, given the auto public path.
* Resolves styles, images, fonts and others except scripts.
*
* @param {string} resource The resource file, including a query.
* @return {string|null}
*/
resolveAsset(resource) {
const resourceData = this.data.get(resource);
if (!resourceData) return null;
const { entryPoint, issuer } = this;
const assetKey = this.getAssetKey(issuer.resource, entryPoint);
const assetFile = resourceData.assets.get(assetKey);
const isIssuerInlinedStyle = this.collection.isInlineStyle(issuer.resource);
if (assetFile && !isIssuerInlinedStyle) return assetFile;
const { originalFilename, resolve } = resourceData;
const realIssuer = isIssuerInlinedStyle
? {
resource: entryPoint.resource,
filename: entryPoint.filename,
}
: issuer;
let outputFilename;
if (originalFilename != null) {
outputFilename = this.pluginOption.getAssetOutputFile(originalFilename, realIssuer.filename);
} else if (resolve != null) {
// resolve asset filename processed via external loader, e.g. `responsive-loader`
outputFilename = resolve(realIssuer);
}
if (outputFilename != null) {
resourceData.assets.set(assetKey, outputFilename);
if (this.collection.hasStyle(resource)) {
// set the output filename for already created (in renderManifest) data
this.collection.setResourceFilename(this.entryPoint, { resource, filename: outputFilename });
} else {
this.collection.setData(this.entryPoint, issuer, {
type: Collection.type.resource,
inline: false,
resource,
assetFile: outputFilename,
});
}
}
return outputFilename;
}
/**
* Require the resource request in the compiled template or CSS.
*
* @param {string} rawRequest The raw request of source resource.
* @returns {string} The output asset filename generated by filename template.
* @throws {Error}
*/
require(rawRequest) {
const { issuer, issuerFile } = this;
// @import CSS rule is not supported
if (rawRequest.includes('??ruleSet')) {
resolveException(rawRequest, issuer.resource, this.rootContext, this.pluginOption);
}
// bypass the asset contained data-URL
if (this.assetInline.isDataUrl(rawRequest)) return rawRequest;
// bypass the source script file to replace it after the process
if (this.collection.hasScript(rawRequest)) return rawRequest;
// bypass the inline CSS
if (this.collection.isInlineStyle(rawRequest)) return rawRequest;
const resource = this.resolveResource(rawRequest, issuerFile);
// resolve resource
if (resource != null) {
// bypass the asset/inline as inline SVG
if (this.assetInline.isInlineSvg(resource, issuerFile)) {
this.collection.setData(this.entryPoint, issuer, {
type: Collection.type.inlineSvg,
inline: true,
resource,
});
return resource;
}
const assetFile = this.resolveAsset(resource);
if (assetFile != null) return assetFile;
// try to resolve inline data url
const dataUrl = this.assetInline.getDataUrl(resource, issuerFile);
if (dataUrl != null) {
this.collection.setData(this.entryPoint, issuer, { type: Collection.type.resource, inline: true, resource });
return dataUrl;
}
}
// if is used the filename like `./main.js`, then the resource is an absolute file
// if is used the filename like `../js/main.js`, then the resource is null and the rawRequest is an absolute file
const file = resource || rawRequest;
if (this.pluginOption.js.test.test(file) && this.assetEntry.isEntryResource(issuer.resource)) {
// occur after rename/delete of a js file when the entry module was already rebuilt
Snapshot.addMissingFile(issuer.resource, file);
resolveException(file, issuer.resource, this.rootContext, this.pluginOption);
}
// require a native JavaScript, JSON or css-loader API file
if (/\.js[a-z0-9]*$/i.test(resource)) return require(resource);
resolveException(rawRequest, issuer.resource, this.rootContext, this.pluginOption);
}
/**
* Clear caches.
* Called only once when the plugin is applied.
*/
clear() {
this.data.clear();
this.sourceFiles.clear();
}
/**
* Reset settings.
* Called before each new compilation after changes, in the serve/watch mode.
*/
reset() {
// reset outdated assets after rebuild via webpack dev server
// note: new filenames are generated on the fly in the this.resolveAsset() method
this.data.forEach((item) => item.assets.clear());
}
}
module.exports = Resolver;