问题
I am making a Webpack 4 plugin for fun and to try to understand its internals. The idea is simple:
- Parse an HTML template file into a tree;
- Get the asset paths from
<img src="...">
and<link href="...">
; - Add the assets to dependencies to load them through the
file-loader
; - Get the path emitted from
file-loader
(which might include a hash)and fix the nodes in the tree; - Emit the final HTML string into a file.
So far, I am stuck at step 4. Parsing the template and extracting the asset paths was easy thanks to parse5, to load the assets, I used the PrefetchPlugin but now I don't know how to get the result from file-loader
.
I need to load the result because it generates a hash and might change the location of the asset:
{
exclude: /\.(css|jsx?|mjs)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]?[sha512:hash:base64:8]`',
},
}],
}
Not only that, but I want to use the url-loader
later which might generate the asset encoded. I am trying to get the result from the loader at tapAfterCompile
.
The current source code for the plugin is as follows:
import debug from 'debug'
import prettyFormat from 'pretty-format'
import validateOptions from 'schema-utils'
import {dirname, resolve} from 'path'
import {html as beautifyHtml} from 'js-beautify'
import {minify as minifyHtml} from 'html-minifier'
import {parse, serialize} from 'parse5'
import {PrefetchPlugin} from 'webpack'
import {readFileSync} from 'fs'
let log = debug('bb:config:webpack:plugin:html')
const PLUGIN_NAME = 'HTML Plugin'
/**
* This schema is used to validate the plugin’s options, right now, all it does
* is requiring the template property.
*/
const OPTIONS_SCHEMA = {
additionalProperties: false,
type: 'object',
properties: {
minify: {
type: 'boolean',
},
template: {
type: 'string',
},
},
required: ['template'],
}
/**
* Extract an attribute’s value from the node; Returns undefined if the
* attribute is not found.
*/
function getAttributeValue(node, attributeName) {
for (let attribute of node.attrs) {
if (attribute.name === attributeName)
return attribute.value
}
return undefined
}
/**
* Update a node’s attribute value.
*/
function setAttributeValue(node, attributeName, value) {
for (let attribute of node.attrs) {
if (attribute.name === attributeName)
attribute.value = value
}
}
/**
* Recursively walks the parsed tree. It should work in 99.9% of the cases but
* it needs to be replaced with a non recursive version.
*/
function * walk(node) {
yield node
if (!node.childNodes)
return
for (let child of node.childNodes)
yield * walk(child)
}
/**
* Actual Webpack plugin that generates an HTML from a template, add the script
* bundles and and loads any local assets referenced in the code.
*/
export default class SpaHtml {
/**
* Options passed to the plugin.
*/
options = null
/**
* Parsed tree of the template.
*/
tree = null
constructor(options) {
this.options = options
validateOptions(OPTIONS_SCHEMA, this.options, PLUGIN_NAME)
}
/**
* Webpack will call this method to allow the plugin to hook to the
* compiler’s events.
*/
apply(compiler) {
let {hooks} = compiler
hooks.afterCompile.tapAsync(PLUGIN_NAME, this.tapAfterCompile.bind(this))
hooks.beforeRun.tapAsync(PLUGIN_NAME, this.tapBeforeRun.bind(this))
}
/**
* Return the extracted the asset paths from the tree.
*/
* extractAssetPaths() {
log('Extracting asset paths...')
const URL = /^(https?:)?\/\//
const TEMPLATE_DIR = dirname(this.options.template)
for (let node of walk(this.tree)) {
let {tagName} = node
if (!tagName)
continue
let assetPath
switch (tagName) {
case 'link':
assetPath = getAttributeValue(node, 'href')
break
case 'img':
assetPath = getAttributeValue(node, 'src')
break
}
// Ignore empty paths and URLs.
if (!assetPath || URL.test(assetPath))
continue
const RESULT = {
context: TEMPLATE_DIR,
path: assetPath,
}
log(`Asset found: ${prettyFormat(RESULT)}`)
yield RESULT
}
log('Done extracting assets.')
}
/**
* Returns the current tree as a beautified or minified HTML string.
*/
getHtmlString() {
let serialized = serialize(this.tree)
// We pass the serialized HTML through the minifier to remove any
// unnecessary whitespace that could affect the beautifier. When we are
// actually trying to minify, comments will be removed too. Options can be
// found in:
//
// https://github.com/kangax/html-minifier
//
const MINIFIER_OPTIONS = {
caseSensitive: false,
collapseBooleanAttributes: true,
collapseInlineTagWhitespace: true,
collapseWhitespace: true,
conservativeCollapse: false,
decodeEntities: true,
html5: true,
includeAutoGeneratedTags: false,
keepClosingSlash: false,
preserveLineBreaks: false,
preventAttributesEscaping: true,
processConditionalComments: false,
quoteCharacter: '"',
removeAttributeQuotes: true,
removeEmptyAttributes: true,
removeEmptyElements: false,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
sortAttributes: true,
sortClassName: true,
useShortDoctype: true,
}
let {minify} = this.options
if (minify) {
// Minify.
serialized = minifyHtml(serialized, {
minifyCSS: true,
minifyJS: true,
removeComments: true,
...MINIFIER_OPTIONS,
})
} else {
// Beautify.
serialized = minifyHtml(serialized, MINIFIER_OPTIONS)
serialized = beautifyHtml(serialized, {
indent_char: ' ',
indent_inner_html: true,
indent_size: 2,
sep: '\n',
unformatted: ['code', 'pre'],
})
}
return serialized
}
/**
* Load the template and parse it using Parse5.
*/
parseTemplate() {
log('Loading template...')
const SOURCE = readFileSync(this.options.template, 'utf8')
log('Parsing template...')
this.tree = parse(SOURCE)
log('Done loading and parsing template.')
}
async tapAfterCompile(compilation, done) {
console.log()
console.log()
for (let asset of compilation.modules) {
if (asset.rawRequest == 'assets/logo.svg')
console.log(asset)
}
console.log()
console.log()
// Add the template to the dependencies to trigger a rebuild on change in
// watch mode.
compilation.fileDependencies.add(this.options.template)
// Emit the final HTML.
const FINAL_HTML = this.getHtmlString()
compilation.assets['index.html'] = {
source: () => FINAL_HTML,
size: () => FINAL_HTML.length,
}
done()
}
async tapBeforeRun(compiler, done) {
this.parseTemplate()
// Add assets to the compilation.
for (let {context, path} of this.extractAssetPaths()) {
new PrefetchPlugin(context, path)
.apply(compiler)
}
done()
}
}
回答1:
Found the answer, after I loaded the dependencies, I can access the generated module's source:
// Index the modules generated in the child compiler by raw request.
let byRawRequest = new Map
for (let asset of compilation.modules)
byRawRequest.set(asset.rawRequest, asset)
// Replace the template requests with the result from modules generated in
// the child compiler.
for (let {node, request} of this._getAssetRequests()) {
if (!byRawRequest.has(request))
continue
const ASSET = byRawRequest.get(request)
const SOURCE = ASSET.originalSource().source()
const NEW_REQUEST = execAssetModule(SOURCE)
setResourceRequest(node, NEW_REQUEST)
log(`Changed: ${prettyFormat({from: request, to: NEW_REQUEST})}`)
}
And execute the module's source with a VM:
function execAssetModule(code, path) {
let script = new Script(code)
let exports = {}
let sandbox = {
__webpack_public_path__: '',
module: {exports},
exports,
}
script.runInNewContext(sandbox)
return sandbox.module.exports
}
来源:https://stackoverflow.com/questions/52320135/webpack-4-plugin-add-module-and-get-result-from-loader