How to inspect files packaged by webpack before they are emitted
Tags: webpack, javascript, programming, tutorial
Published: 2022-06-30 20:30:34

I recently had a need to inspect files in my front-end project that are packaged by webpack before they were emitted.  In my pursuit to accomplish this an elegant and robust manner, I came across webpack hooks which are an exceedingly powerful way to tap into the inner workings of webpack.

## What is webpack?
webpack is a module bundler for JavaScript.  Front-end applications contain many types of assets such as JavaScript, (HOPEFULLY) Typescript, JSON, HTML, CSS, and images.  webpack (after you've configure it to process files in a certain manner) will generate static assets, representing your applications modules so that they can be interpreted by a browser.

## What are webpack hooks?
A "hook" in software development is a place in code that allows you to tap into a module to either provide different behavior or to react when something happens. webpack provides the following types of hooks:
- [Compiler hooks](https://webpack.js.org/api/compiler-hooks/)
- [Compilation hooks](https://webpack.js.org/api/compilation-hooks/)
- [ContextModuleFactory hooks](https://webpack.js.org/api/contextmodulefactory-hooks/)
- [JavascriptParser hooks](https://webpack.js.org/api/parser/)
- [NormalModuleFactory hooks](https://webpack.js.org/api/normalmodulefactory-hooks/)

## How about an example?
For an example scenario, let's pretend we want to make sure that, when we build our application, no file is outputted that contains the string `MY_SUPER_SECRET`.  Perhaps we want to do this to provide a last line of defense from a developer including a sensitive value in our code and we want to prevent webpack from even compiling it.  If that string is detected, we'd like webpack to:
- throw an error.
- not emit any files at any point during the build.

To do this, let's look at the [`shouldEmit`](https://webpack.js.org/api/compiler-hooks/#shouldemit) compiler hook. This hook is called _before_ assets are emitted and will allow us to error out and not emit assets if our validation fails.

To start, let's create a new plugin class and add it to the `plugins` block of our `webpack.config`:

```ts
// src/webpack.config.ts
import { AssetValidatorPlugin } from './plugins/asset-validator-plugin';

module.exports= {
    entry: {...},
    module:{...},
    plugins: [
        new AssetValidatorPlugin(),
        ...
    ]
};
```

Note that, while I've defined my plugin in a separate class/file, you could include it in your `webpack.config` inline.

Now, let's take a look at the plugin:

```ts
// src/plugins/asset-validator-plugin.ts
import * as webpack from 'webpack';

export class AssetValidatorPlugin {
  apply(compiler: webpack.Compiler) {
    compiler.hooks.shouldEmit.tap('AssetValidatorPlugin', (compilation: webpack.compilation.Compilation) => {
      this.validateAssets(compilation);
    });
  }

  public validateAssets(compilation: webpack.compilation.Compilation) {
    const assets = Object.entries(compilation.assets);
    const regex = new RegExp('MY_SUPER_SECRET', 'g');

    // Loop through each asset and check to see if it contains any sensitive strings
    for (let i = 0; i < assets.length; i++) {
      const [fileName] = assets[i];
      const asset = compilation.getAsset(fileName);
      const source = asset.source.source();
      const contents = convertSourceToString(source);
      const matches = contents.match(regex);

      if (matches) {
        throw new Error(
          "Our tool has identified the presence of the string 'MY_SUPER_SECRET' in your compiled code. Compilation has been aborted."
        );
      }
    }

    return true;
  }
}

// This function is only needed because asset.source.source() can be a string or ArrayBuffer
const convertSourceToString = (source: string | ArrayBuffer): string => {
  if (typeof source === 'string') {
    return source;
  } else {
    return new TextDecoder().decode(source);
  }
};
```

So, let's review what is included our plugin.  We've defined our plugin class and, within, are able to [tap](https://webpack.js.org/api/plugins/#tapable) into the `compiler.hooks.shouldEmit` hook.  In the hook callback, we simply call a `validateAssets()` function we've defined which loops through all assets that are part of the compilation and use regular expression matching to see if the string exists.  We throw an error if it does, short-circuiting the compilation and not emitting any files. If it doesn't contain our special string, we'll return `true`, compilation will continue, emitting the packaged files as expected.
 
If you have a need to pass any parameters to your plugin, that can easily be accomplished by defining a constructor in your plugin class like this: 

```ts
constructor(options: MyPluginOptions) {
    this.options = options;
}
```

## Conclusion
Hopefully you now have a better understanding of webpack hooks and how you can leverage them to provide additional behavior when your application's assets are packaged.