1.4.3 • Published 4 years ago

mini-extract-plugin v1.4.3

Weekly downloads
53
License
MIT
Repository
github
Last release
4 years ago

mini-extract-plugin

Latest Version Documentation contributions welcome License: MIT Package Size

Build Status Dependencies Known Vulnerabilities codecov Total alerts Language grade: JavaScript Maintainability


Generalized and hookable mini-css-extract-plugin. Extract any format, process it your way.

🏠 Homepage | 🗃 Repository | 📦 NPM | 📚 Documentation | 🐛 Issue Tracker

🪑 Table of Content

🧰 Features

  • Exposes 18 hooks that enable you to:

    • Override how the modules are parsed when passed to the loader
    • Override how the modules are merged into a common file
    • Override what content is returned instead of the content was extracted.
    • Define if and how the modules should should be split
    • Hook into the compiler / compilation.
    • Extend initialization
  • Based on mini-css-extract-plugin v0.9.0, this package recycles the same logic, but allows you override the format-specific logic and more.

  • See how the original mini-css-extract-plugin has been reimplemented using this package.

👶 Install

npm install mini-extract-plugin

🚀 Usage

Minimal setup

For a minimal setup, just import the plugin and give it a unique type name by which the plugin will be known. The instances of the created class can be used in Webpack.

// Default import is a class factory
import mep from 'mini-extract-plugin';
// or const mep = require('mini-extract-plugin').default

// Minimal config
const MyMiniExtractPlugin =  mep({
  type: 'my-custom-type'
});

// Create instance to be used in the config.
// Class constructor optionally accepts an options object
const myMEP = new MyMiniExtractPlugin({
  ...
});

// webpack config
const config = {
  ...
  module: {
    rules: [
      {
        // We want all encountered files named "*.some.json"
        // to be extracted into separate files.
        //
        // The files could be e.g. i18n files, or some config
        // files that are defined in multiple places but we want
        // a single file that combines all matched files.
        resourceQuery: /\.some\.json$/,
        use: [
          // Register the plugin as a loader
          // `asLoader` is an object so we have to use it in the
          // `use` array
          myMEP.asLoader,
        ],
        ...
      }
    ],
  },
  plugins: [
    // Register the plugin as a plugin.
    myMEP,
    ...
  ]
}

However, this example above is not terribly useful since we haven't specified any hooks. That means, for example, that found JSONs will be concatenated into as single file as strings (e.g. "{}" + "{}"), but that won't yield a valid JSON. So to make the new plugin class to work properly, we need to speficy more than just the type.

import mep from 'mini-extract-plugin';
// const mep = require('mini-extract-plugin').default

const MyMiniExtractPlugin = mep({
  type: 'my-custom-type',
  displayName: `My Super Awesome Extract Plugin`,
  hooks: [
    // Tap sync function `compilationHookFn` into `compilation`
    // hook.
    // Maybe we want to tap into the Webpack's Compilation
    // instance
    {
      name: 'compilation',
      type: 'tap',
      hooks: [compilationHookFn],
    },
    // Tap sync function `mergeHookFn` into `merge` hook.
    // Here we will override how the modules should be
    // concatenated (instead of string concat, we join JSONs
    // as objects)
    { name: 'merge', type: 'tap', hooks: [mergeHookFn] },
  ],
});

// Create instance to be used in the config.
const myMEP = new MyMiniExtractPlugin();
...

This is better! We have specified how the modules should be merged and so when we use this plugin in the Webpack now, the end result will be a proper JSON.

This way, you can override only the parts of the process that needs to be modified. You can also tap multiple functions into any single hook. And where available, you can also tap asynchronously. Some hooks are "waterfall", meaning that result from one function is passed to another. Other are regular hooks where the tapped functions don't interact. See tapable for details on how hooks work.

For details on what hooks are available, their signatures, whether they're sync or async, etc, see Hooks.

You may have noticed that we've also specified a displayName property. Additional options allow you to:

  • override what classes should be used for modules, dependencies, and other Webpack entities (useful if you want to pass custom data along with the modules).
  • override what options can be passed to the class constructor when creating a plugin instance (by default, mini-css-extract-plugin options are used)
  • override the name of the created class, and how it is displayed.

See full description of the class options here.

Examples

Subclassing

Class factory

Here's an example how MiniExtractPlugin can be subclassed taken from the mini-css-extract-plugin replimenetation

import miniExtractPluginFactory, { types } from 'mini-extract-plugin';
import Module from './module';
import ModuleFactory from './module-factory';
import Dependency from './dependency';
import DependencyTemplate from './dependency-template';
import hooks from './hooks';
import { type, typeReadable } from './config';

const MiniExtractPluginClass = miniExtractPluginFactory<{
  dependencyClass: types.DependencyClass<Dependency>;
  moduleClass: types.ModuleClass<Module>;
  moduleFactoryClass: typeof ModuleFactory;
}>({
  type,
  displayName: `My Mini ${typeReadable} Extract Plugin`,
  moduleFactoryClass: ModuleFactory,
  dependencyClass: Dependency,
  dependencyTemplateClass: DependencyTemplate,
  hooks: [
    { name: 'compilation', type: 'tap', hooks: [hooks.compilation!] },
    { name: 'merge', type: 'tap', hooks: [hooks.merge!] },
  ],
});

export default MiniExtractPluginClass;

The factory function is passed:

  • identifiers type and optional displayName.
  • custom subclasses dependencyClass, moduleClass, and dependencyTemplateClass.
  • compilation and merge hooks.

Factory function was also given an object as a type parameter. This object specifies which types should be used for classes and options, and enable type inferrence for subclasses with custom classes and options.

Full list of type options with their defaults:

const MyMiniExtractPlugin = miniExtractPluginFactory<{
  // These reflect the types of the classes that we pass to the
  // class factory as options.
  dependencyClass?: DependencyClass;
  dependencyTemplateClass?: DependencyTemplateClass;
  moduleClass?: ModuleClass;
  moduleFactoryClass?: ModuleFactoryClass;
  // Type of the options object passed to constructor on instantiations.
  constructorOptions?: { [key: string]: any };
} = {}
>(...)

Options classes

Classes that can be passed to the factory function can be found at the root of the export. Subclassing is as simple as:

import {
  Dependency,
  DependencyTemplate,
  Module,
  ModuleFactory,
} from 'mini-extract-plugin';

class DependencySubclass extends Dependency {}
class DependencyTemplateSubclass extends DependencyTemplate {}
class ModuleSubclass extends Module {}
class ModuleFactorySubclass extends ModuleFactory {}

If you use TypeScript and want to override the types these subclass use in methods / constructor, you can pass type arguments. This is useful e.g. if your subclass adds properties, and you want TypeScript to recognize those properties.

All type parameters that can be passed to classes + their defaults:

import {
  subclassDependency,
  subclassDependencyTemplate,
  subclassModule,
  subclassModuleFactory,
} from 'mini-extract-plugin';

class DependencySubclass extends Dependency<{
  // Options object passed to the Dependency constructor
  dependencyOptions: DependencyOptions;
}> {}

// subclassDependencyTemplate has no type parameters
class DependencyTemplateSubclass extends DependencyTemplate {}

class ModuleSubclass extends Module<{
  // Dependency class whose instance is passed to the Module
  // constructor
  dependency: Dependency;
}> {}

class ModuleFactorySubclass extends ModuleFactory<{
  // Dependency class that is passed to ModuleFactory.create
  dependency: Dependency;
  // Module class whose instance is created in ModuleFactory.create
  // from Dependency
  module: Module;
}> {}

Subclassing helpers

If you need to subclass any of the above but don't need to override the behaviour, you can use helper subclassing functions.

Each of them accepts options

import {
  subclassDependency,
  subclassDependencyTemplate,
  subclassModule,
  subclassModuleFactory,
} from 'mini-extract-plugin';

// `type` is the same `type` argument passed to class factory
const DependencySubclass = subclassDependency({ type: 'custom-plugin' });
const DependencyTemplateSubclass = subclassDependencyTemplate({
  type: 'custom-plugin',
});
const ModuleSubclass = subclassModule({ type: 'custom-plugin' });
const ModuleFactorySubclass = subclassModuleFactory({
  type: 'custom-plugin',
  moduleClass: MyCustomModule, // Optionally define module class
});

If you use TypeScript and want to override the classes these subclass use, you can pass type arguments. The type arguments passed to subclass helpers are same as to the classes.

Given the example from above, if you want to module type created by ModuleFactorySubclass to match MyCustomModule, you can do following:

const ModuleFactorySubclass = subclassModuleFactory<{
    module: Module;
  }({
  type: 'custom-plugin',
  moduleClass: MyCustomModule,
});

All type parameters that can be passed to helper functions + their defaults:

import {
  subclassDependency,
  subclassDependencyTemplate,
  subclassModule,
  subclassModuleFactory,
} from 'mini-extract-plugin';

const DependencySubclass = subclassDependency<{
  // Options object passed to the Dependency constructor
  dependencyOptions: DependencyOptions;
}>(...);

// subclassDependencyTemplate has no type parameters
const DependencyTemplateSubclass = subclassDependencyTemplate(...);

const ModuleSubclass = subclassModule<{
  // Dependency class whose instance is passed to the Module
  // constructor
  dependency: Dependency;
}>(...);

const ModuleFactorySubclass = subclassModuleFactory<{
  // Dependency class that is passed to ModuleFactory.create
  dependency: Dependency;
  // Module class whose instance is created in ModuleFactory.create
  // from Dependency
  module: Module;
}>(...);

See how classes are extended in the re-implementation of mini-css-extract-plugin.

Instance options

By default, instances of the subclassed MiniExtractPlugin accept an options object with the same options as mini-css-extract-plugin v.0.9.0.

:warning: Some options, while having same name and fulfilling same task, accept different values than those of mini-css-extract-plugin. These are:

moduleFilename

The function signature is changed to:

(RenderContext, TemplateOptions, Module[]) => string;

ArgumentDescription
RenderContextContext available on Compilation's renderManifest hook. See RenderContext for details.
TemplateOptionsObject with info on the chunk that is being rendered:{   chunk: Chunk,    hash: string,    contentHashType: string }
ModuleModule that are being rendered. Modules are of the class as specified in the plugin's class options.

Helpers

The package also exposes a util export, which contains utility modules used for working with the hooks or objects passed to hook functions:

  • util.module includes helper functions for working with modules.
  • util.subclass includes helper functions for subclassing the classes that can be passed to the class factory. Use these functions if you need a subclass but don't care about implementation.

Typing

This project is written in TypeScript and the typings are included under the types import.

  • types, the root, includes interfaces related to subclassing (MiniExtractPlugin, class factory options, etc.)
  • types.context includes interfaces for the contexts passed to tapped functions
  • types.hook includes types related to Hooks (Hook overrides, types of recognized hooks, etc.)
  • types.webpack is a shim for Webpack, it includes types that either are not exposed in Webpack v4, or which have modified interfaces in the extraction process, so the provided types reflect the actual interface.
  • types.util includes helper types which may or may not be useful when working with the hooks.

What you will most likely want is to have the hook functions types inferred. You can use the types.hook.Taps interface, which is an object of { [hookName]: hookFunction }. If you want to define only some hooks, use types.hook.PartialTaps.

Both types accept MiniExtractPlugin interface as a type parameter. Use it to have correct types for arguments and return functions. The passed interface affects the inferred types of context objects, classes passed to MiniExtractPlugin (module, dependency, moduleFactory, ...).

import { types } from 'mini-extract-plugin';

import { IMyMiniExtractPlugin } from './types'

const hooks: types.hook.PartialTaps<IMyMiniExtractPlugin> = {
  // Arguments of `dependency` are inferred thanks to
  // `PartialTaps`
  dependency: (context, {exports: exported}) => {
    const { childCompilation, classOptions } = context;
    ...
};

Debugging

This project uses debug. To show debug logs, activate debug for mini-extract-plugin.

CLI example:

DEBUG=mini-extract-plugin node path/to/my/mini-extract-plugin-project

🔮 Background

mini-css-extract-plugin is great because it allows you to have modularized definitions, which are then merged or split as necessary for you during the build.

This is important for bundle optimization, but also for maintainability, as you can keep the information where it makes sense, without making a tradeoff in logistics.

When working with Vue, I was hoping to manage other auxiliary file types (i18n and documentation, to be specific) in a similar manner - modularized definition but processed and emitted as separate files during build.

There's (understandably) not as much support for other file formats as there is for CSS. But since other formats could benefit from same action,generalizing the process was in order (and thus encouraging modularized approach for more formats).

🤖 API

TypeDoc documentation can be found here.

Options

interface ClassOptions {
  type: string;
  pluginName?: string;
  displayName?: string;
  className?: string;
  moduleType?: string;
  pluginOptionsSchema?: any;
  loaderOptionsSchema?: any;
  dependencyTemplateClass?: DependencyTemplateClass;
  dependencyClass?: DependencyClass;
  moduleFactoryClass?: ModuleFactoryClass;
  moduleClass?: ModuleClass;
  hooks?: Overrides;
}

The class factory accepts an object with following options:

  • type - (Required) Namespace used by this class to distinguish it, its instances, and webpack resources (e.g. modules and dependencies) from other MiniExtractPlugin classes.
    • E.g. MiniCssExtractPlugin uses css, MiniI18nExtractPlugin uses i18n
  • pluginName - Name of plugin used in identifiers. Prefer package-like (kebab-case) format.
    • Default: mini-${type}-extract-plugin.
  • displayName - String to be used when printing the class in logging messages or similar.
    • Default: Mini ${type} Extract Plugin where type is capitalized.
  • className - Name of the plugin class that is shown when calling either class.toString() or class.name.
    • Default: Mini${type}ExtractPlugin where type is capitalized.
  • moduleType - Identifier used to find modules processed by the class.
    • Default: ${type}/mini-extract.
  • pluginOptionsSchema - JSON schema used to validate options passed to constructor and used in loader methods.
  • loaderOptionsSchema - JSON schema used to validate options passed to constructor and used in plugin methods.
  • dependencyTemplateClass - Class that implements Webpack's DependencyTemplate interface.
    • The instance of dependencyTemplateClass must have a method apply.
    • Default: Default is empty implementation. (see source file).
  • dependencyClass - Class that implements Webpack's Dependency interface.
    • Dependencies are used among other to pass data to Modules. Data passed to Dependency constructor can be overriden in dependency hook.
    • If providing ypur own Dependency class, be sure to subclass the default implementation to ensure the plugin works as expected. See the re-implementation of MiniCssExtractPlugin for an example of how the Dependency class can be extended.
    • Default: Default implementation ensures the MiniExtractPlugin works correctly. It stores information on MiniExtractPlugin type, identifier, context, and content (see source file).
  • moduleFactoryClass - Class that implements Webpack's Module Factory interface.
    • The instance of moduleFactoryClass must have a method create that is called with data and callback and must call the callback with either an error as first argument or Module instance as second argument. The data contains an array-wrapped dependency of class dependencyClass.
    • Default: Default implementation passed the dependency to Module constructor (see source file). If moduleClass is specified, the created Module will be of class moduleClass. Otherwise Webpack's Module class is used.
  • moduleClass - Class that implements Webpack's Module interface.
    • This class used only with the default moduleFactoryClass implementation. If you specify moduleFactoryClass, this option is ignored.
    • Module constructor accepts a dependency of class dependencyClass.
    • If providing your own Module class, be sure to subclass the default implementation to ensure the plugin works as expected. See the re-implementation of MiniCssExtractPlugin for an example of how the Module class can be extended.
    • Default: Default implementation ensures the MiniExtractPlugin works correctly, and it updates identifiers and hashes (see source file).
  • hooks - Array of objects specifying which Tapable hooks should be tapped with what functions, and how the hook should behave (sync/async). See Hooks for the list of available hooks.

    • Each object is expected to implement the hook Override interface:

      PropertyDescriptionTypeDefault
      nameName of the hook to be tapped.string-
      typeHow should be the functions applied to the hook. These are the methods defined by tapableOptions: 'tap' | 'tapAsync' | 'tapPromise' | 'intercept' Note: tapAsync and tapPromise are available only for async hooks. See tapable documentation for details.string-
      hooksArray of functions that should be applied to the hook.Functions are expected to conform to the hook signature they are tapping to. For details on using sync- / promise- / callback-style functions, see tapable documentation.See how the hooks are defined in MiniCssExtractPlugin re-implementation.Function[][]

Hooks order

Hooks are called in following order:

Hooks

The available hooks in alphabetical order are (see source file):

afterMerge

  • Signature: (RenderContext, Source) => void

  • Hook: SyncHook

  • Hook called after Modules were merged into a single Source that will be emitted into a file.

    Use this hook if you want to modify the resulting Source without overriding the merging process itself, or if you want to trigger some behaviour after the merge step has finished.

  • Default: No behaviour.

    ArgumentDescription
    RenderContextContext available on Compilation's renderManifest hook. See RenderContext for details.
    SourceInstance of Webpack's Source (value returned by merge).

afterRenderChunk

afterRenderMain

beforeMerge

  • Signature: (RenderContext, Module[]) => Module[]

  • Hook: SyncWaterfallHook

  • Hook called when merging multiple Modules into a single Source that will be emitted into a file.

    Use this hook if you want to modify the list of modules without overriding the merging process itself.

  • Default: No modifications done.

ArgumentDescription
RenderContextContext available on Compilation's renderManifest hook. See RenderContext for details.
ModulesList of Options.moduleClass modules.
ReturnsDescription
ModulesProcessed list of Options.moduleClass modules.

beforeRenderChunk

  • Signature: (RenderContext, Module[]) => Module[] | Module[][]

  • Hook: SyncWaterfallHook

  • Hook called when Webpack is generating a chunk (non-entrypoint file) from the given set of modules.

    Use this hook if you want to modify the list of modules or if you want to split the list of modules into multiple chunks without overriding the rendering process itself.

    This hook is called only if the extracted Dependencies from dependency hook were split into chunks by Webpack.

  • Default: No modifications done.

    ArgumentDescription
    RenderContextContext available on Compilation's renderManifest hook. See RenderContext for details.
    ModulesList of Options.moduleClass modules.
    ReturnsDescription
    ModulesGroupsList or list of lists of Options.moduleClass modules. If a list of lists is returned, these are interpreted as module groups. Each module group will emit separate chunk file.

beforeRenderMain

childCompilation

childCompiler

  • Signature: (PitchCompilerContext) => void

  • Hook: AsyncParallelHook

  • Hook called after a child Compiler was set up in loader's pitch method. Child Compiler is used to evaluate the modules passed to the loader, and the resulting content will be extracted.

    Use this hook if you need to modify the child Compiler before other hooks are tapped, or if you want to access child Compiler's hooks yourself.

  • Default: No behaviour.

    ArgumentDescription
    PitchCompilerContextContext available in loader's pitch function on creation of child Compiler instance. See PitchCompilerContext for details.

compiler

  • Signature: (CompilerContext) => void

  • Hook: SyncHook

  • Hook called at the beginning of plugin's apply method. Use if you need to access Compiler hooks or if you need to set things up at the beginning of the process.

  • Default: No behaviour.

    ArgumentDescription
    CompilerContextContext available in Plugin's apply method. See CompilerContext for details.

compilation

dependency

ArgumentDescription
PitchCompilationContextContext available in loader's pitch function on child Compiler's thisCompilation hook. See PitchCompilationContext for details.
LoaderModuleContextData generated by evaluating the source code of the Module that triggered the MiniExtractPlugin's loader. See LoaderModuleContext.
ReturnsDescription
DependencyOptionsList of options objects to be passed to the dependencyClass to create Webpack's Dependencies.

extracted

  • Signature: (PitchCompilationContext, string) => string

  • Hook: SyncWaterfallHook

  • Modify the source string of the module from which content has been extracted before it is passed to Webpack.

    Use this hook if you need to modify the string so it can conforms to a loader / parser that it will be passed to next.

  • Default: Inserts comment // extracted by plugin-name plus HMR compat.

ArgumentDescription
PitchCompilationContextContext available in loader's pitch function on child Compiler's thisCompilation hook. See PitchCompilationContext for details.
remainingSourceString that will be returned to Webpack as the source of the module from which data has been extracted.
ReturnsDescription
stringString that will be returned to Webpack as the source of the module from which data has been extracted.

initialize

  • Signature: (MiniExtractPlugin, object) => void

  • Hook: SyncHook

  • Modify the MiniExtractPlugin instance during initialization (called from constructor). This hook is called after other initialization logic is done.

    Use this hook if you need to extend the class with custom methods / properties, or to set default options values.

  • Default: No behaviour.

ArgumentDescription
instanceMiniExtractPlugin instance that is being constructed.
instanceOptionsOptions object with which the instance is being constructed.

merge

  • Signature: (RenderContext, Module[]) => Source

  • Hook: SyncWaterfallHook

  • Hook called when merging multiple Modules into a single Source that will be emitted into a file.

    Use this hook if you want to override how Modules are merged.

  • Default: Join contents with newline (\n).

ArgumentDescription
RenderContextContext available on Compilation's renderManifest hook. See RenderContext for details.
ModulesList of Options.moduleClass modules (values processed by beforeMerge).
ReturnsDescription
SourceInstance of Webpack's Source with content from the modules.

pitch

  • Signature: (PitchContext) => void

  • Hook: AsyncParallelHook

  • Hook called at the beginning of loader's pitch method. Use if you need to set up the loader environment.

  • Default: No behaviour.

ArgumentDescription
PitchContextContext available in loader's pitch function. See PitchContext for details.

renderChunk

renderMain

source

Contexts

Hooks can be tapped to modify the extraction process at different stages. T