CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-webpack-assets-manifest

Webpack plugin that generates JSON manifest files mapping original filenames to their hashed versions with extensive customization options

Pending
Overview
Eval results
Files

hooks.mddocs/

Hook System

This document covers the Tapable-based hook system for customizing manifest generation at various stages of the webpack compilation process.

Hook Interface

interface WebpackAssetsManifest {
  hooks: {
    apply: SyncHook<[manifest: WebpackAssetsManifest]>;
    customize: SyncWaterfallHook<[
      entry: KeyValuePair | false | undefined | void,
      original: KeyValuePair,
      manifest: WebpackAssetsManifest,
      asset: Asset | undefined
    ]>;
    transform: SyncWaterfallHook<[asset: AssetsStorage, manifest: WebpackAssetsManifest]>;
    done: AsyncSeriesHook<[manifest: WebpackAssetsManifest, stats: Stats]>;
    options: SyncWaterfallHook<[options: Options]>;
    afterOptions: SyncHook<[options: Options, manifest: WebpackAssetsManifest]>;
  };
}

Hook Types and Usage

apply Hook

Runs after the plugin setup is complete, before compilation starts:

apply: SyncHook<[manifest: WebpackAssetsManifest]>;
manifest.hooks.apply.tap("MyPlugin", (manifest) => {
  console.log("Plugin setup complete");
  manifest.set("build-time", Date.now().toString());
});

// Or via constructor options
const manifest = new WebpackAssetsManifest({
  apply(manifest) {
    manifest.set("version", "1.0.0");
  },
});

customize Hook

Customizes individual asset entries as they are added to the manifest:

customize: SyncWaterfallHook<[
  entry: KeyValuePair | false | undefined | void,
  original: KeyValuePair,
  manifest: WebpackAssetsManifest,
  asset: Asset | undefined
]>;
manifest.hooks.customize.tap("MyCustomizer", (entry, original, manifest, asset) => {
  // Skip certain files
  if (original.key.includes(".map")) {
    return false; // Skip this entry
  }
  
  // Modify entry
  if (entry && entry.key.startsWith("img/")) {
    return {
      key: entry.key.replace("img/", "images/"),
      value: entry.value,
    };
  }
  
  // Add integrity information
  if (entry && manifest.options.integrity) {
    const integrity = asset?.info[manifest.options.integrityPropertyName];
    if (integrity && typeof entry.value === "string") {
      return {
        key: entry.key,
        value: {
          src: entry.value,
          integrity,
        },
      };
    }
  }
  
  return entry;
});

transform Hook

Transforms the entire manifest before serialization:

transform: SyncWaterfallHook<[asset: AssetsStorage, manifest: WebpackAssetsManifest]>;
manifest.hooks.transform.tap("MyTransformer", (assets, manifest) => {
  // Add metadata
  const transformed = {
    ...assets,
    _meta: {
      generatedAt: new Date().toISOString(),
      version: manifest.options.extra.version || "unknown",
      nodeEnv: process.env.NODE_ENV,
    },
  };
  
  return transformed;
});

done Hook

Runs after compilation is complete and the manifest has been written:

done: AsyncSeriesHook<[manifest: WebpackAssetsManifest, stats: Stats]>;
manifest.hooks.done.tapPromise("MyDoneHandler", async (manifest, stats) => {
  console.log(`Manifest written to ${manifest.getOutputPath()}`);
  
  // Upload to CDN
  await uploadToCDN(manifest.toString());
  
  // Generate additional files
  await generateServiceWorkerManifest(manifest.assets);
});

// Synchronous version
manifest.hooks.done.tap("MySyncHandler", (manifest, stats) => {
  console.log(`Assets: ${Object.keys(manifest.assets).length}`);
});

options Hook

Modifies plugin options before they are processed:

options: SyncWaterfallHook<[options: Options]>;
manifest.hooks.options.tap("OptionsModifier", (options) => {
  // Environment-specific options
  if (process.env.NODE_ENV === "development") {
    options.writeToDisk = true;
    options.space = 2;
  } else {
    options.space = 0; // Minify in production
  }
  
  return options;
});

afterOptions Hook

Runs after options have been processed and validated:

afterOptions: SyncHook<[options: Options, manifest: WebpackAssetsManifest]>;
manifest.hooks.afterOptions.tap("PostOptionsSetup", (options, manifest) => {
  console.log(`Plugin configured with output: ${options.output}`);
  
  // Set up additional hooks based on final options
  if (options.integrity) {
    console.log("Subresource integrity enabled");
  }
});

Hook Combination Examples

Advanced Customization

const manifest = new WebpackAssetsManifest({
  output: "assets-manifest.json",
  integrity: true,
});

// Setup hook - initialize shared data
manifest.hooks.apply.tap("Setup", (manifest) => {
  manifest.options.extra.startTime = Date.now();
});

// Customize individual entries
manifest.hooks.customize.tap("AssetCustomizer", (entry, original, manifest, asset) => {
  if (!entry) return entry;
  
  // Group assets by type
  const ext = manifest.getExtension(original.key);
  const group = ext === ".js" ? "scripts" : ext === ".css" ? "styles" : "assets";
  
  return {
    key: `${group}/${entry.key}`,
    value: entry.value,
  };
});

// Transform entire manifest
manifest.hooks.transform.tap("ManifestTransformer", (assets, manifest) => {
  const buildTime = Date.now() - (manifest.options.extra.startTime as number);
  
  return {
    ...assets,
    _build: {
      time: buildTime,
      timestamp: new Date().toISOString(),
      env: process.env.NODE_ENV,
    },
  };
});

// Post-processing
manifest.hooks.done.tapPromise("PostProcessor", async (manifest, stats) => {
  // Write additional manifests
  const serviceWorkerManifest = Object.entries(manifest.assets)
    .filter(([key]) => !key.startsWith("_"))
    .map(([key, value]) => ({
      url: typeof value === "string" ? value : value.src,
      revision: stats.hash,
    }));
  
  await manifest.writeTo("./dist/sw-manifest.json");
});

Plugin Integration

class MyWebpackPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap("MyPlugin", (compilation) => {
      // Find WebpackAssetsManifest instances
      const manifests = compiler.options.plugins?.filter(
        plugin => plugin instanceof WebpackAssetsManifest
      ) as WebpackAssetsManifest[];
      
      manifests.forEach(manifest => {
        // Hook into the manifest
        manifest.hooks.customize.tap("MyPlugin", (entry, original, manifest, asset) => {
          // Custom logic for this plugin
          if (entry && asset?.info.myPluginMetadata) {
            return {
              ...entry,
              value: {
                ...entry.value,
                metadata: asset.info.myPluginMetadata,
              },
            };
          }
          return entry;
        });
      });
    });
  }
}

Hook Utilities Access

The manifest provides utility functions through the utils property:

interface WebpackAssetsManifest {
  utils: {
    isKeyValuePair: typeof isKeyValuePair;
    isObject: typeof isObject;
    getSRIHash: typeof getSRIHash;
  };
}
manifest.hooks.customize.tap("UtilityExample", (entry, original, manifest, asset) => {
  // Use utility functions
  if (manifest.utils.isKeyValuePair(entry) && manifest.utils.isObject(entry.value)) {
    // Generate custom SRI hash
    const content = asset?.source.source();
    if (content) {
      entry.value.customHash = manifest.utils.getSRIHash("md5", content);
    }
  }
  
  return entry;
});

Hook Execution Order

  1. options - Modify plugin options
  2. afterOptions - Post-process validated options
  3. apply - Plugin setup complete
  4. customize - Per-asset customization (during compilation)
  5. transform - Transform entire manifest (before serialization)
  6. done - Post-compilation cleanup and additional processing

Understanding this order is crucial for proper hook coordination and data flow.

Install with Tessl CLI

npx tessl i tessl/npm-webpack-assets-manifest

docs

asset-management.md

hooks.md

index.md

plugin-configuration.md

utilities.md

tile.json