Hello, my name is Xiaoyu Xiaoyu and I’m dedicated to sharing interesting, practical technical articles.

Content is divided into translation and original, if you have questions, please feel free to comment or private message, hope to progress with everyone.

Everyone’s support is the motivation for my creation.

plan

The Rollup series is intended to be released chapter by chapter, making the content shorter, more focused and easier to understand

This is the final article in the Rollup series, and here are links to all of them.

  • rollup.rollup
  • rollup.generate + rollup.write
  • rollup.watch
  • tree shaking
  • Plugins <==== Current article

TL; DR

Rollup’s plug-ins are similar to other large frameworks in that they provide a standard interface, define common configurations through conventions, and inject properties and methods related to the current build results for developers to add, delete, modify, and check. For stable and sustainable growth has provided a strong and powerful foundation!

While webPack doesn’t want to distinguish between loaders and plugins, rollup’s plugin can act as both a loader and a traditional plugin. The hook functions provided by Rollup are the core hook functions, such as Load and Transform to parse the chunk, resolveFileUrl to parse the loaded module legally, options to dynamically update the configuration, and so on

Pay attention to the point

All the comments are here and can be read by yourself

!!!!!!!!! Prompt => labeled TODO for specific implementation details, will be analyzed as the case.

!!!!!!!!! Note that => each subheading is an internal implementation of the parent heading (function)

!!!!!!!!! Note that the id of the module (file) in rollup is the address of the file, so something like resolveID is the address of the file. We can return the id of the file that we want to return (i.e., the address, relative path, and determination path) for rollup to load

Rollup is the core, it does the most basic things, such as providing a default module (file) loading mechanism, such as packaging into different styles of content, our plugin provides load file path, parse file content (handle TS, sass, etc.) and so on, it is a plug and pull design. Similar to WebPack, pluggable design is a very flexible and long-term iterative design, which is at the heart of a medium to large framework. Many hands make light work

Main generic modules and their meanings

  1. Graph: globally unique Graph that contains entries and dependencies, operations, caching, etc. Is the heart of rollup
  2. PathTracker: Refers to (calls) a tracker
  3. PluginDriver: a plug-in driver that invokes the plug-in and provides context for the plug-in environment
  4. FileEmitter: Resource operator
  5. GlobalScope: the GlobalScope, and the local scope
  6. ModuleLoader: indicates the ModuleLoader
  7. NodeBase: The construction base class of the AST syntax (ArrayExpression, AwaitExpression, etc.)

Analysis of plug-in mechanism

The plugin for Rollup is actually a normal function that returns an object that contains some basic properties (such as name) and various phases of the hook function, like this:

function plugin(options = {}) {
  return {
    name: 'rollup-plugin',
    transform() {
      return {
        code: 'code'.map: { mappings: ' '}}; }}; }Copy the code

Here’s what the official advice is to keep.

When we write a rollup plugin, we focus on the hook function. There are three types of hook function calls:

  1. Const chunks = rollup.rollup Build Hooks during execution
  2. Chunks. Generator (write) Output Generation Hooks during execution
  3. Listen for file changes and re-execute the watchChange hook function during the execution of the built rollup.watch

In addition to the categories, Rollup also provides several ways to execute hook functions, each of which is classified as synchronous or asynchronous for internal use:

  1. Async: An asynchronous hook for handling promises. There are also synchronous versions
  2. First: If multiple plugins implement the same hook function, it is executed in sequence, from beginning to end, but if one of them returns a value that is neither null nor undefined, the subsequent plugins are terminated.
  3. Sequential: If multiple plug-ins implement the same hook function, it executes sequentially, from beginning to end in the order in which the plug-ins are used, or asynchronously, it waits for processing to finish before executing the next plug-in.
  4. Parallel: same as above, but if one plug-in is asynchronous, subsequent plug-ins do not wait, but execute in parallel.

Text expression is relatively pale, let’s see a few implementations:

  • Hook function: hookFirst Usage scenario: resolveId, resolveAssetUrl, etc
function hookFirst<H extends keyof PluginHooks.R = ReturnType<PluginHooks[H] > > (hookName: H, args: Args<PluginHooks[H]>, replaceContext? : ReplaceContext |null, skip? :number | null
) :EnsurePromise<R> {
    // Initialize the promise
    let promise: Promise<any> = Promise.resolve();
    // This.plugins are initialized when the Graph is initialized
    for (let i = 0; i < this.plugins.length; i++) {
        if (skip === i) continue;
        // Overwrite the previous promise. In other words, execute the hook function sequentially
        promise = promise.then((result: any) = > {
            // If null or undefined is returned, stop running and return the result
            if(result ! =null) return result;
            // Execute the hook function
            return this.runHook(hookName, args as any[], i, false, replaceContext);
        });
    }
    // The result of the last promise
    return promise;
}
Copy the code
  • Hook function: hookFirstSync Usage scenario: resolveFileUrl, resolveImportMeta etc
// The synchronized version of hookFirst
function hookFirstSync<H extends keyof PluginHooks.R = ReturnType<PluginHooks[H] > > (hookName: H, args: Args
       
        , replaceContext? : ReplaceContext
       [h]>) :R {
    for (let i = 0; i < this.plugins.length; i++) {
        // Synchronous version of runHook
        const result = this.runHookSync(hookName, args, i, replaceContext);
        // If null or undefined is returned, stop running and return the result
        if(result ! =null) return result as any;
    }
    // Otherwise null is returned
    return null as any;
}
Copy the code
  • Hook function: hookSeq Usage scenario: onWrite, generateBundle, etc
// The difference between hookFirst and hookFirst is that there is no interruption
async function hookSeq<H extends keyof PluginHooks> (hookName: H, args: Args
       
        , replaceContext? : ReplaceContext
       [h]>) :Promise<void> {
    let promise: Promise<void> = Promise.resolve();
    for (let i = 0; i < this.plugins.length; i++)
        promise = promise.then((a)= >
            this.runHook<void>(hookName, args as any[], i, false, replaceContext)
        );
    return promise;
}
Copy the code
  • Use scenarios: buildStart, buildEnd, renderStart, etc
// Synchronize, using promise.all
function hookParallel<H extends keyof PluginHooks> (hookName: H, args: Args
       
        , replaceContext? : ReplaceContext
       [h]>) :Promise<void> {
    // Create the promise.all container
    const promises: Promise<void= > [] [];// Go through each plugin
    for (let i = 0; i < this.plugins.length; i++) {
        // Execute hook to return promise
        const hookPromise = this.runHook<void>(hookName, args as any[], i, false, replaceContext);
        // Don't push if there isn't
        if(! hookPromise)continue;
        promises.push(hookPromise);
    }
    / / return promise
    return Promise.all(promises).then((a)= > {});
}
Copy the code
  • Hook function: hookReduceArg0 Usage scenarios: outputOptions and renderChunk
// Perform the reduce operation on the first item of the ARG
function hookReduceArg0<H extends keyof PluginHooks.V.R = ReturnType<PluginHooks[H] > > (
    hookName: H,
    [arg0, ...args]: any[].// Take the first argument of the passed array and place the rest in an arrayreduce: Reduce<V, R>, replaceContext? : ReplaceContext// Replace the context in which the plugin is currently invoked
) {
    let promise = Promise.resolve(arg0); // Source.code is returned by default
    for (let i = 0; i < this.plugins.length; i++) {
        // The first promise will only receive the arg0 passed above
        // Each subsequent promise accepts the value of source.code processed by the previous plugin
        promise = promise.then(arg0= > {
            const hookPromise = this.runHook(hookName, [arg0, ...args], i, false, replaceContext);
            // Return arg0 if no promise is returned
            if(! hookPromise)return arg0;
            // Result is the return value of plug-in execution
            return hookPromise.then((result: any) = >
                reduce.call(this.pluginContexts[i], arg0, result, this.plugins[i])
            );
        });
    }
    return promise;
}
Copy the code

By looking at the above methods of calling hook functions, we can see that there is an internal method to call the hook function: runHook(Sync), which executes the hook function provided in the plug-in.

The implementation is simple:

function runHook<T> (
    hookName: string,
    args: any[],
    pluginIndex: number,
    permitValues: boolean, hookContext? : ReplaceContext |null
) :Promise<T> {
    this.previousHooks.add(hookName);
    // Find the current plugin
    const plugin = this.plugins[pluginIndex];
    // Find the current executed hooks function defined in the plugin
    const hook = (plugin as any)[hookName];
    if(! hook)return undefined as any;

    PluginContexts is an array that holds the context of each plug-in
    let context = this.pluginContexts[pluginIndex];
    // Plugin context to distinguish between different hook functions
    if (hookContext) {
        context = hookContext(context, plugin);
    }
    return Promise.resolve()
        .then((a)= > {
            // permit values allows values to be returned instead of a functional hook
            if (typeofhook ! = ='function') {
                if (permitValues) return hook;
                return error({
                    code: 'INVALID_PLUGIN_HOOK',
                    message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.`
                });
            }
            // Return the result of plug-in execution by passing in the plug-in context and parameters
            return hook.apply(context, args);
        })
        .catch(err= > throwPluginError(err, plugin.name, { hook: hookName }));
}
Copy the code

Of course, not everyone will be using plugins to start with, so Rollup itself provides a few necessary hook functions to concat with custom plugins while Graph is instantiated:

import { getRollupDefaultPlugin } from './defaultPlugin';

this.plugins = userPlugins.concat(
    // A plugin that uses the built-in default plugin or Graph's plugin driver. Either way, there must be a built-in default plugin
    // basePluginDriver is a plug-in initialized by the previous PluginDriver
    // preserveSymlinks: preserveSymlinks
    basePluginDriver ? basePluginDriver.plugins : [getRollupDefaultPlugin(preserveSymlinks)]
);
Copy the code

What are the required hook functions that rollup provides:

export function getRollupDefaultPlugin(preserveSymlinks: boolean) :Plugin {
	return {
        / / the plugin name
		name: 'Rollup Core'.// Default module (file) loading mechanism, internal mainly use path.resolve
		resolveId: createResolveId(preserveSymlinks) as ResolveIdHook,
        / / this. PluginDriver. HookFirst (' load '[id]) for asynchronous calls, readFile internal packing in promise the fs. ReadFile, and returns the promise
		load(id) {
			return readFile(id);
		},
        // To handle urls or files added via emitFile
		resolveFileUrl({ relativePath, format }) {
			// Different formats return different addresses for parsing files
			return relativeUrlMechanisms[format](relativePath);
		},
        / / handle the import. Meta. Url, refer to the address: https://nodejs.org/api/esm.html#esm_import_meta)
		resolveImportMeta(prop, { chunkId, format }) {
			// Change the behavior of retrieving import.meta information
			const mechanism = importMetaMechanisms[format] && importMetaMechanisms[format](prop, chunkId);
			if (mechanism) {
				returnmechanism; }}}; }Copy the code

At a glance, these are the most basic hook functions that handle path resolution.

Furthermore, rollup injects a context into the hook function to facilitate adding, deleting, and querying chunks and other build information.

It is also clearly written in the document, such as:

  • Using this.parse, the AST is parsed by calling the Acron instance inside rollup
  • Using this.emitfile to increase the output file, see this example.

Let’s take a quick look at the transform operation. When we transform the AST, we call the transform hook:


graph.pluginDriver
    .hookReduceArg0<any.string> ('transform',
        [curSource, id], // source. Code and module ID
        transformReducer,
    	// The fourth argument is a function that declares the required method in some hook context
        (pluginContext, plugin) => {
            // This pile is used by the plugin, called through this.xxx
            curPlugin = plugin;
            if (curPlugin.cacheKey) customTransformCache = true;
            else trackedPluginCache = getTrackedPluginCache(pluginContext.cache);
            return {
                ...pluginContext,
                cache: trackedPluginCache ? trackedPluginCache.cache : pluginContext.cache,
                warn(warning: RollupWarning | string, pos? :number | { column: number; line: number{})if (typeof warning === 'string') warning = { message: warning } as RollupWarning;
                    if (pos) augmentCodeLocation(warning, pos, curSource, id);
                    warning.id = id;
                    warning.hook = 'transform';
                    pluginContext.warn(warning);
                },
                error(err: RollupError | string, pos? :number | { column: number; line: number }): never {
                    if (typeof err === 'string') err = { message: err };
                    if (pos) augmentCodeLocation(err, pos, curSource, id);
                    err.id = id;
                    err.hook = 'transform';
                    return pluginContext.error(err);
                },
                emitAsset(name: string, source? :string | Buffer) {
                    const emittedFile = { type: 'asset' as const, name, source }; emittedFiles.push({ ... emittedFile });return graph.pluginDriver.emitFile(emittedFile);
                },
                emitChunk(id, options) {
                    const emittedFile = { type: 'chunk' as const, id, name: options && options.name }; emittedFiles.push({ ... emittedFile });return graph.pluginDriver.emitFile(emittedFile);
                },
                emitFile(emittedFile: EmittedFile) {
                    emittedFiles.push(emittedFile);
                    return graph.pluginDriver.emitFile(emittedFile);
                },
                addWatchFile(id: string) {
                    transformDependencies.push(id);
                    pluginContext.addWatchFile(id);
                },
                setAssetSource(assetReferenceId, source) {
                    pluginContext.setAssetSource(assetReferenceId, source);
                    if(! customTransformCache && ! setAssetSourceErr) {try {
                            return this.error({
                                code: 'INVALID_SETASSETSOURCE',
                                message: `setAssetSource cannot be called in transform for caching reasons. Use emitFile with a source, or call setAssetSource in another hook.`
                            });
                        } catch (err) {
                            setAssetSourceErr = err;
                        }
                    }
                },
                getCombinedSourcemap() {
                    const combinedMap = collapseSourcemap(
                        graph,
                        id,
                        originalCode,
                        originalSourcemap,
                        sourcemapChain
                    );
                    if(! combinedMap) {const magicString = new MagicString(originalCode);
                        return magicString.generateMap({ includeContent: true, hires: true, source: id });
                    }
                    if(originalSourcemap ! == combinedMap) { originalSourcemap = combinedMap; sourcemapChain.length =0;
                    }
                    return newSourceMap({ ... combinedMap, file:null as any, sourcesContent: combinedMap.sourcesContent! }); }}; })Copy the code

One judgment in runHook is the use of context:

function runHook<T> (
		hookName: string,
		args: any[],
		pluginIndex: number,
		permitValues: boolean, hookContext? : ReplaceContext |null
) {
    // ...
    const plugin = this.plugins[pluginIndex];
    // Get the default context
    let context = this.pluginContexts[pluginIndex];
    // If it is, replace it
    if (hookContext) {
        context = hookContext(context, plugin);
    }
    // ...
}
Copy the code

The timing of the rollup call to the hook function provided by the plug-in is beyond the point where the distribution of the code is clear.

There is also rollup in order to facilitate our change plug-in, but also provides a tool set, can be very convenient for module operation and judgment, interested in their own view.

Caching of plug-ins

The plug-in also provides caching capability, which is implemented very cleverly:

export function createPluginCache(cache: SerializablePluginCache) :PluginCache {
	// Cache with closures
	return {
		has(id: string) {
			const item = cache[id];
			if(! item)return false;
			item[0] = 0; // If it is, then reset the number of times the access expires, guessing that the user intends to use it
			return true;
		},
		get(id: string) {
			const item = cache[id];
			if(! item)return undefined;
			item[0] = 0; // If so, reset the access expiration times
			return item[1];
		},
		set(id: string, value: any) {
            // The storage unit is an array. The first item is used to mark the number of accesses
			cache[id] = [0, value];
		},
		delete(id: string) {
			return deletecache[id]; }}; }Copy the code

Then after creating the cache, it will be added to the plug-in context:

import createPluginCache from 'createPluginCache';

const cacheInstance = createPluginCache(pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null)));

const context = {
	// ...
    cache: cacheInstance,
    // ...
}
Copy the code

Then we can use cache in the plug-in environment to improve packaging efficiency:

function testPlugin() {
  return {
    name: "test-plugin",
    buildStart() {
      if (!this.cache.has("prev")) {
        this.cache.set("prev"."Result of last plug-in execution");
      } else {
        // The second time the rollup is executed
        console.log(this.cache.get("prev")); }}}; }let cache;
async function build() {
  const chunks = await rollup.rollup({
    input: "src/main.js".plugins: [testPlugin()],
    // Need to pass the last packing result
    cache,
  });
  cache = chunks.cache;
}

build().then((a)= > {
  build();
});
Copy the code

Note that the options hook function does not inject context, and is not called the same way as other hooks:

function applyOptionHook(inputOptions: InputOptions, plugin: Plugin) {
	if (plugin.options){
        // Specify this and the processed input configuration, without passing the context
    	return plugin.options.call({ meta: { rollupVersion } }, inputOptions) || inputOptions;
    }

	return inputOptions;
}
Copy the code

conclusion

This is the end of the Rollup series. It’s been an unforgettable experience to start reading with a muddled face, and then to read with ten muddled faces depending on the collection and various tools

Learn the operation of the big guys and take its essence, go to its dross like playing strange upgrade, your product, your fine product. Ha ha

During this period is also misleading some things, see a lot, you will find that the routine is the same, to explore their core framework, and then the function of the patch, constantly update iteration, maybe we can also become the author of open source masterpiece.

To describe rollup in a few words:

Read and merge configuration -> Create dependency graph -> Read entry module content -> Borrow open source ESTREE specification parser for source analysis, obtain dependency, recurse this operation -> Generate module, mount module corresponding file information -> Analyze AST, Build each node instance -> generate chunks -> call each node rewrite render -> use magic- String concatenation and wrap operation -> write

To simplify:

String -> AST -> string

If the series can help you in any way, please also move your finger, encourage ~

Have a bye ~