ARCHITECTURE.md
rollup npm package contains both Rollup's Node.js JavaScript interface and the command-line-interface (CLI).@rollup/browser. It exposes the same JavaScript interface as the Node.js build but does not include the CLI and also requires writing a plugin to encapsulate file reading. Instead of native code dependencies, this build has a bundled WASM artifact included that can be loaded in the browser. This is what the Rollup REPL uses.package.json file but are added dynamically during publishing as optionalDependencies. The README.md and package.json files for those dependencies can be found in the npm folder. The corresponding binaries are built and published from GitHub Actions whenever a new release version tag is pushed. The actual loading of the native code is handled by native.js which is copied into the output folder during build. So to add a new platform-architecture combination, you need to
npm foldernative.js filepackage.json file as napi-rs depends on this@rollup/wasm-node package that is identical to the rollup package in that it contains the Node.js JavaScript interface and the CLI but does not contain any native code. Instead, it includes a WebAssembly artifact that runs in Node.js. This package and the corresponding artifact only built on GitHub Actions and published via the publish-wasm-node-package.js script.rollup.config.ts orchestrates building the ESM and CommonJS versions of the JavaScript interface, the CLI, which is an extension of and shares code with the CommonJS build, and the browser build.
npm run build:js builds all JavaScript artifacts. However, as it includes the browser build, it requires the browser Web Assembly artifact to be built first, otherwise the build will fail.replace-browser-modules.ts replaces some modules with modified browser versions that reside in the browser folder.resolveId and load plugin hooks. If there is no plugin implementing those hooks for a file, the browser build will fail.node-entry.ts while the browser build uses browser-entry.ts. Those are mostly identical except that the browser build does not expose the watch mode interface.cli folder with the entry point cli.ts.import { loadConfigFile } from "rollup/loadConfigFile".run/index.ts.The rollup package relies on optional dependencies to provide platform-specific native code. These are not listed in the committed package.json file but are added dynamically during publishing. This logic is handled by napi-rs via npm run prepublish:napi from scripts/prepublish.js.
As native modules do not work for the browser build, we use wasmpack to build a WebAssembly artefact. This is triggered from npm run build via npm run build:wasm. There is also a separate Node wasm build that is again triggered from GitHub actions via npm run build:wasm:node.
From JavaScript, native code is imported by importing native.js. This module, or its WebAssembly version native.wasm.js, is copied as native.js into the output folder during build (via publish-wasm-node-package.js for WebAssembly). Imports of this module are declared external.
The Rust entrypoints are bindings_napi/src/lib.rs for the native modules and bindings_wasm/src/lib.rs for WebAssembly.
Building an output has two phases
rollup(inputOptions) function exported by the JavaScript interfacegenerate and write methods.generate(outputOptions) or .write(outputOptions)
.write will write the files to disk while .generate just returns the output in-memory. Hence .write requires the file or dir options to be set while .generate does not.preserveModules to keep the file structure or inlineDynamicImports to inline dynamic imports, albeit at the cost of some semantic changes.flowchart LR
classDef default fill: transparent, color: black;
classDef graphobject fill: #ffb3b3, stroke: black;
classDef command fill: #ffd2b3, stroke: black, text-align: left;
build("rollup\n<code>input: main.js</code>"):::command
--> bundle(bundle):::command
--> generate1(".generate\n<code>file: main.mjs,\nformat: 'es'</code>"):::command
bundle
-->generate2(".generate\n<code>file: main.cjs,\nformat: 'cjs'</code>"):::command
On the highest level, all of this is orchestrated by the rollupInternal function in rollup.ts. This function
normalizeInputOptionsGraph instanceGraph.build().generate and .write methodsnormalizeOutputOptionsBundle instance that manages one outputBundle.generate()To understand this phase from a plugin perspective, have a look at Build Hooks, which also contains a graph to show in which order these hooks are executed. In detail, Graph.build performs the following steps
ModuleLoader class. This class
input option and additional entry points emitted from plugins via this.emitFile().Module instance.Module instance via Module.setSource().ModuleLoader, a process that repeats until the graph is complete.execIndex to each module in executionOrder.ts..include on the Module instance..include on the top-level AST node, that is then propagated to the child nodes. At several stages, inclusion will only happen if a statement or expression has "side effects", which is determined by calling its .hasEffects method. Usually, it means mutating a variable that is already included or performing an action where we cannot determine whether there are side effects like calling a global function..included flags on the AST nodes that are then picked up by the rendering logic in the "generate" phase.To understand this phase from a plugin perspective, have a look at Output Generation Hooks, which again contains a graph to show in which order these hooks are executed. In detail Bundle.generate performs the following steps
chunkAssignment.tsrenderChunks helper
Chunk.render()
import.meta references should be resolved and store this on the corresponding AST nodes.deconflictChunk.ts.render methods of their AST nodes. This is also where tree-shaken nodes are removed from the output.renderChunk plugin hookRollup parses code within the native/WebAssembly code. As most of Rollup is still TypeScript-based, this then needs to be transformed to a JavaScript representation. To do that efficiently, a binary buffer is constructed in Rust that can be passed without copying to TypeScript where it is further transformed.
converter.rs. Here we also make sure that the buffer follows the format of the ESTree specification.generate-ast-converters.js via npm run build:ast-converters. The definitions for the auto-generated converters can be found in ast-types.js, which is also the first file that needs to be extended to support additional AST nodes.There are two ways Rollup parses code into an abstract syntax tree
this.parse. This is a synchronous operation that returns a JSON-AST of the provided code.
bufferToAst.ts.setSource method of the Module class.
bufferParsers.ts.In general, when extending the AST parsing capabilities, the following places need to be touched:
ast-types.js.converter.rs.ast/nodes.