Generating Module Dependency Graph
The first step in the build phase is to generate the module dependency graph.
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.
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.
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.
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.
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.
class ModuleLoader {
async addEntryModules(
entryModules: UnresolvedModule[],
isInitialBuild: boolean
): Promise<void> {}
}