Skip to content

Generating Module Dependency Graph

The first step in the build phase is to generate the module dependency graph.

ts
class Graph {
  async build(): Promise<void> {
    timeStart('generate module graph', 2);
    await this.generateModuleGraph();
    timeEnd('generate module graph', 2);

    timeStart('sort and bind modules', 2);
    this.phase = BuildPhase.ANALYSE;
    this.sortModules();
    timeEnd('sort and bind modules', 2);

    timeStart('mark included statements', 2);
    this.includeStatements();
    timeEnd('mark included statements', 2);

    this.phase = BuildPhase.GENERATE;
  }
}

Rollup determines the entry modules through the input option configured by the user (i.e., options.input), instantiates the Module class, obtains the module information corresponding to the path, and then recursively finds all dependent modules to finally generate a module dependency graph.

ts
function normalizeEntryModules(
  entryModules: readonly string[] | Record<string, string>
  ): UnresolvedModule[] {
  if (Array.isArray(entryModules)) {
    return entryModules.map(id => ({
      fileName: null,
      id,
      implicitlyLoadedAfter: [],
      importer: undefined,
      name: null
    }));
  }
  return Object.entries(entryModules).map(([name, id]) => ({
    fileName: null,
    id,
    implicitlyLoadedAfter: [],
    importer: undefined,
    name
  }));
}
class Graph {
  private async generateModuleGraph(): Promise<void> {
    ({ entryModules: this.entryModules, implicitEntryModules: this.implicitEntryModules } =
      await this.moduleLoader.addEntryModules(normalizeEntryModules(this.options.input), true));
    if (this.entryModules.length === 0) {
      throw new Error('You must supply options.input to rollup');
    }
    for (const module of this.modulesById.values()) {
      module.cacheInfoGetters();
      if (module instanceof Module) {
        this.modules.push(module);
      } else {
        this.externalModules.push(module);
      }
    }
  }
}

After all entry modules and their dependent modules are instantiated, the Graph instance collects all module instances and stores them in the modules array. At the same time, it calls the cacheInfoGetters method of the Module class to cache property access of the Module instance.

ts
function cacheObjectGetters<T, K extends PropertyKey = keyof T>(
  object: T,
  getterProperties: K[]
) {
  for (const property of getterProperties) {
    const propertyGetter = Object.getOwnPropertyDescriptor(
      object,
      property
    )!.get!;
    Object.defineProperty(object, property, {
      get() {
        const value = propertyGetter.call(object);
        // This replaces the getter with a fixed value for subsequent calls
        Object.defineProperty(object, property, { value });
        return value;
      }
    });
  }
}
class Module {
  cacheInfoGetters(): void {
    cacheObjectGetters(this.info, [
      'dynamicallyImportedIdResolutions',
      'dynamicallyImportedIds',
      'dynamicImporters',
      'exportedBindings',
      'exports',
      'hasDefaultExport',
      'implicitlyLoadedAfterOneOf',
      'implicitlyLoadedBefore',
      'importedIdResolutions',
      'importedIds',
      'importers'
    ]);
  }
}

The creation and parsing of Module instances is implemented through the addEntryModules method of the moduleLoader instance. The moduleLoader instance is created when the Graph class is instantiated.

ts
class Graph {
  readonly moduleLoader: ModuleLoader;
  readonly modulesById = new Map<string, Module | ExternalModule>();
  readonly pluginDriver: PluginDriver;

  constructor(
    private readonly options: NormalizedInputOptions,
    watcher: RollupWatcher | null
  ) {
    this.pluginDriver = new PluginDriver(
      this,
      options,
      options.plugins,
      this.pluginCache
    );
    this.moduleLoader = new ModuleLoader(
      this,
      this.modulesById,
      this.options,
      this.pluginDriver
    );
  }
}

The Graph instance is globally unique. Rollup creates a Graph instance in the pre-build phase and passes the inputOptions instance and watcher instance to the Graph instance.

ts
async function rollupInternal(
  rawInputOptions: RollupOptions,
  watcher: RollupWatcher | null
): Promise<RollupBuild> {
  const graph = new Graph(inputOptions, watcher);
  await catchUnfinishedHookActions(graph.pluginDriver, async () => {
    try {
      timeStart('initialize', 2);
      await graph.pluginDriver.hookParallel('buildStart', [inputOptions]);
      timeEnd('initialize', 2);
      await graph.build();
    } catch (error_: any) {
      const watchFiles = Object.keys(graph.watchFiles);
      if (watchFiles.length > 0) {
        error_.watchFiles = watchFiles;
      }
      await graph.pluginDriver.hookParallel('buildEnd', [error_]);
      await graph.pluginDriver.hookParallel('closeBundle', []);
      throw error_;
    }
    await graph.pluginDriver.hookParallel('buildEnd', []);
  });
}

The ModuleLoader class is one of the core classes of Rollup, responsible for module loading and parsing. When the Graph class is instantiated, it creates a unique ModuleLoader instance and passes its own instance, modulesById instance, options instance, and pluginDriver instance to the moduleLoader property. At the same time, the plugin system is one of the core features of Rollup, and Vite's plugin system also draws inspiration from Rollup's implementation. We can see that the Graph class initializes the plugin system through the pluginDriver class in the initialization phase.

The addEntryModules method of the moduleLoader instance generates entry modules based on the options.input configuration and recursively finds all dependent modules to finally generate a module dependency graph.

ts
class ModuleLoader {
  async addEntryModules(
    entryModules: UnresolvedModule[],
    isInitialBuild: boolean
  ): Promise<void> {}
}

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (dbcbf17)