Webpack 4 Plugin: Add module and get result from loader

前提是你 提交于 2021-01-29 10:57:00

问题


I am making a Webpack 4 plugin for fun and to try to understand its internals. The idea is simple:

  1. Parse an HTML template file into a tree;
  2. Get the asset paths from <img src="..."> and <link href="...">;
  3. Add the assets to dependencies to load them through the file-loader;
  4. Get the path emitted from file-loader(which might include a hash)and fix the nodes in the tree;
  5. 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!