• Home
  • About
  • Say Hi
  • Rollup main components

    Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application. Rollup can optimize ES modules for faster native loading in modern browsers, or output a legacy module format allowing ES module workflows today.

    Rollup

    Building module graph

    Rollup build starts by creating a module graph. Such a graph contains the entry files and their dependencies.

    It’ll be used to analyze the dependencies, find the exported expressions from the output bundle, run effective tree-shaking, and optimize the output bundle.

    The heads of the graph are the entry modules, the modules defined in the input option of the configuration. Each graph node is created by first resolving a module file name and fetching its source, parsing it, fetching its dependencies, and adding it to the graph.

    When resolving a module, ModuleLoader looks for a plugin that handles the hook 'resolveDynamicImport' or the hook 'resolveId'.

    When no plugin does, it looks for a file whose path is the given id. If not found, it attempts to add .mjs to the id. If also not found, it tries to add .js.

    Hooks augment the build process.

    A hook is a step in the build process where plugins extend the core logic. A plugin handles a hook by defining a function that takes a module id (its filename) and optionally a context object. It returns either the handling result or an undefined value, if it’s not responsible for the given module.

    Two resolution hooks exist because a dependency can be static or dynamic. A static dependency is an import statement:

    import { a } from './b'
    export * from './other' // import is implicit here
    export { name } from './other' // same here, import is implicit
    

    A dynamic dependency is an import expression:

    import('bar')
    

    The outcome of the resolution is an instance of ResolvedId.

    Such an object is the core of a graph node. It’s used to create a Module or a ExternalModule instance that’ll be inserted into the graph:

    interface ResolvedId {
      assertions: Record<string, string>;
      meta: CustomPluginOptions;
      moduleSideEffects: boolean | 'no-treeshake';
      syntheticNamedExports: boolean | string;
      external: boolean | 'absolute';
      id: string;
      resolvedBy: string;
    }
    

    A module is internal if a plugin handles it and marks it as internal, by setting external to false in the returned ResolveId instance, or if the module id references an existing file.

    A module, otherwise, is considered external. External modules are the ones that should be kept out of the output bundle.

    As external type says, external modules are not all the same. external is either true or 'absolute'.

    If the external filename is not absolute, the file name will be changed to a path relative to the current project. An absolute one always references an existing file on the file system with an absolute path. That one will be kept as it is in the final bundle.

    The created Module instance contains the module source code as a string. Setting the source is named “loading”.

    Given a module, Rollup checks whether a plugin handles it inside a 'load' hook. If no plugin does, the library reads the file with readFile:

    (await this.pluginDriver.hookFirst('load', [id])) ?? (await readFile(id, 'utf8'))
    

    The outcome of the loading step is a SourceDescription instance:

    interface SourceDescription extends Partial<PartialNull<ModuleOptions>> {
      ast?: AcornNode;
      code: string;
      map?: SourceMapInput;
    }
    

    The AST is either returned from the 'load' hook handler, or it’s locally created after the source is fetched, just before attaching it to the Module instance. You can read more about Acorn AST here.

    The dependencies of a module are collected during the creation of the AST. Static dependencies are stored inside Module.sourcesWithAssertions. Dynamic dependencies are stored inside Module.dynamicImports. Inside the constructor of an Acorn import or export node, the object adds itself to one of the two lists.

    Fetching the dependencies is then done in two steps: resolution and fetching. During the resolution step, every entry from the two lists is transformed into a ResolveId instance, then into a Module or an ExternalModule instance. During the fetching step, its content is attached. At the end, the dependencies are attached to the importing module.

    Identifying chunks

    You can learn about output generation and its hooks from the documentation.

    The build process identifies the output units and renders them inside a bundle entry. Such output units are named “chunks”.

    Each chunk contains a set of modules. Read the comment at the beginning of chunkAssignment.ts to learn about the identification algorithm.

    After getting the list of chunk names and their subject modules, Rollup creates Chunk instances for output units. It builds a chunk graph. Then it analyzes the modules to find chunk exports, imports, and re-exports.

    To build the chunk graph, Rollup iterates over each chunk module, gets their dependencies and their transitive dependencies using a depth-first traversal of the module graph, and then adds the chunks of these dependencies as dependencies to the subject chunk.

    Chunk.dependencies in the end will contain instances of Chunk and ExternalChunk (for external modules).

    The main output of Rollup is a bundle object. This bundle contains a map whose entries are output units.

    Usually, they are the files we get inside the build output directory.

    The bundle is defined as:

    interface OutputBundle {
      [fileName: string]: OutputAsset | OutputChunk;
    }
    

    The difference between an “asset” and a “chunk” is that an asset is added to the output bundle while a “chunk” is a Javascript filename that’s added as an entry module. Adding the latter manually, from a hook handler, triggers the process of resolution, loading, dependencies fetching, and augmenting the module graph.

    Hook handlers call emitFile to add a chunk or an asset to the bundle.

    Rendering chunks

    The last step is rendering. Rollup converts each chunk into a string and puts the result inside the bundle.

    For each chunk, the library creates a magic-string instance. It goes over the modules one by one and adds their string output to this string. Then, it uses a format-aware finalizer to create one final big string.

    Rollup offers six finalizers:

    export default { amd, cjs, es, iife, system, umd };
    

    Each one is a function that takes a magic string (the outcome of rendering a chunk), a set of options that describe the chunk rendering context, and the output options object.

    It modifies the given magic string to make it compatible with the target format.

    es finalizer for example adds an “import” block at the beginning and an “export” block at the end.

    To build the export block, the module iterates over the export expressions passed inside the rendering options object (These are Acorn export nodes. They are collected from the chunk modules) and builds one export statement. Same for the import block, it iterates over the dependencies. For each dependency, it adds an import or an export ... from ... statement for the values imported and reexported from the modules.

    Writing the output

    The rollup.rollup function receives an input options object and returns a bundle object. On a bundle object, you can call bundle.generate multiple times with different output options objects to generate different bundles in-memory. If you directly want to write them to disk, use bundle.write instead.

    JavaScript API

    Rollup persists files with writeOutputFile, which uses mkdir and writeFile native functions.

    This code writes the generated bundle into the file system:

    await Promise.all(
      Object.values(generated).map(chunk =>
        graph.fileOperationQueue.run(() => writeOutputFile(chunk, outputOptions))
      )
    );