【Rolldown源码】三、开始入手Rolldown

1,278 阅读4分钟

写给自己看的Rolldown代码解析,有问题可以讨论。

unbuild: A unified javascript build system

@napi-rs/cli:Cli tools for napi-rs(A framework for building compiled Node.js add-ons in Rust via Node-API)

citty: Elegant CLI Builder

zod: TypeScript-first schema validation with static type inference

先从入口开始,根据 About Rolldown

Rolldown's scope is also larger than Rollup's and more similar to esbuild. It comes with built-in CommonJS support, `node_modules` resolution, and will also support TypeScript / JSX transforms and minification in the future.
We started with the intention of a JS to Rust port, but soon realized that in order to achieve the best possible performance, we have to prioritize writing code in a way that aligns with how Rust works. The internal architecture of Rolldown is closer to that of esbuild rather than Rollup, and our chunk splitting logic may end up being different from that of Rollup's.

可以大概猜到,Rolldown 会有薄薄的一层js代码,用来处理输入,主要的逻辑应该都会收拢在rust侧。

  1. 避免Rust 和 JavaScript 之间传递数据的成本过大,导致速度优化不明显。
  2. 实现整个代码解析、转换、代码生成管道统一在rust中进行,减少不同的工具反复对源码解析、转换和序列化的行为。

Pasted image 20240520175409.png

看看代码也证明了上述的猜想 rolldown/packages/rolldown 提供了命令行能力和包的调用能力 rolldown/crates/rolldown_binding 提供node与rust的胶水代码 rolldown/crates/.. 实现了编译、转换、生成等功能

rolldown/packages/rolldown

packages/rolldown中package.json中列出了入口文件和cli的执行文件

"bin": {
	"rolldown": "./bin/cli.js"
},
"main": "./dist/index.cjs",

从cli入口往下走会看到最终调到rolldown的方法中

// packages/rolldown/src/cli/commands/bundle.ts
  
// ...
const build = await rolldown(options)
const bundleOutput = await build.write(options?.output)
// ...

整个cli没有什么特殊的逻辑,就是打印出了整体的耗时,记录最大文件等。

回到 rolldown方法

export const rolldown = async (input: InputOptions): Promise<RolldownBuild> => {
	return new RolldownBuild(input)
}

调用rolldown方法返回的是一个包含inputOptions的实例。

class RolldownBuild {
	#inputOptions: InputOptions;
	#bundler?: Bundler;
	#stopWorkers?: () => Promise<void>;
	#getBundler: (outputOptions: OutputOptions) => Promise<Bundler>;
	generate: (outputOptions: OutputOptions = {}) => Promise<RolldownOutput>;
	write: (outputOptions: OutputOptions = {}) => Promise<RolldownOutput>;
	destroy: () => Promise<void>;
}

只有调用实例的generatewrite方法才会调用二进制包生成一个Bundler,并缓存到this.#bundler上

async generate(outputOptions: OutputOptions = {}): Promise<RolldownOutput> {
	const bundler = await this.#getBundler(outputOptions)
	const output = await bundler.generate()
	return transformToRollupOutput(output)
}

async write(outputOptions: OutputOptions = {}): Promise<RolldownOutput> {
	const bundler = await this.#getBundler(outputOptions)
	const output = await bundler.write()
	return transformToRollupOutput(output)
}

插一嘴,这里虽然调用了两个不同方法,但是到核心方法只是有没有写这个动作的区别 具体代码可以看rolldown/crates/rolldown/src/bundler.rs中结构体Bundler的write方法和generate方法


impl Bundler {
	pub async fn write(&mut self) -> Result<BundleOutput> {
		// ...
		let mut output = self.bundle_up(true).await?; 
		// ...assets的写动作
	}
	
	pub async fn generate(&mut self) -> Result<BundleOutput> {
		self.bundle_up(false).await
	}
}
async fn bundle_up(&mut self, is_write: bool) -> Result<BundleOutput> {
	// ...
	self.plugin_driver.generate_bundle(&mut output.assets, is_write).await?;
}

会发现generate和write方法的区别只有是否写到output的目标文件,这个后面放到具体代码解析的时候在看。

Pasted image 20240521114706.png

再重点说说plugins。

在rolldown(node)中会根据plugin中是否有_parallel字段来判断当前插件是否为parallelPlugin,并根据当前机器可以并行的数量,创建同等数量的worker,每个worker都将所有parallelPlugin初始化掉

Pasted image 20240522110434.png

所有的plugin最终给到PluginDriver的结构体中,具体的结构类似下面这个样子,这个到rolldown-binding的代码中会给具体的过程(参数plugins其实是存储了Plugin特征的一个动态数组)。

PluginDriver {
	plugins: [
		JsPlugin,
		ParallelJsPlugin<parallelPlugin1 * n>,
		JsPlugin,
		JsPlugin,
		JsPlugin,
		ParallelJsPlugin<parallelPlugin2 * n>,
	],
}

整理一下options,rolldown(node)接收到参数类型定义

interface IRenderedChunk {
	isEntry: boolean
	isDynamicEntry: boolean
	facadeModuleId?: string
	moduleIds: Array<string>
	exports: Array<string>
	fileName: string
	modules: Record<string, BindingRenderedModule>
	imports: Array<string>
	dynamicImports: Array<string>
}

interface InputOptions {
    input?: ((string | string[]) | {
        [x: string]: string;
    }) | undefined;
    plugins?: any[] | undefined;
    external?: (((string | any) | (string | any)[]) | (((args_0: string, args_1: string | undefined, args_2: boolean, ...args_3: unknown[]) => (((void | undefined) | null) | undefined) | boolean) | undefined)) | undefined;
    resolve?: {
        alias?: {
            [x: string]: string;
        } | undefined;
        aliasFields?: string[][] | undefined;
        conditionNames?: string[] | undefined;
        exportsFields?: string[][] | undefined;
        extensions?: string[] | undefined;
        mainFields?: string[] | undefined;
        mainFiles?: string[] | undefined;
        modules?: string[] | undefined;
        symlinks?: boolean | undefined;
        tsconfigFilename?: string | undefined;
    } | undefined;
    cwd?: string | undefined;
    platform?: (("node" | "browser") | "neutral") | undefined;
    shimMissingExports?: boolean | undefined;
    logLevel?: ((("info" | "debug") | "warn") | "silent") | undefined;
}



interface OutputOptions {
	dir?: string;
	format?: 'es' | 'cjs' | 'esm' | 'module' | 'commonjs';
	exports?: 'default' | 'named' | 'none' | 'auto';
	sourcemap?: boolean | 'inline' | 'hidden';
	sourcemapIgnoreList?: boolean | (relativeSourcePath: string, sourcemapPath: string) => boolean;
	sourcemapPathTransform?: (relativeSourcePath: string, sourcemapPath: string) => string;
	banner?: string | (chunk: IRenderedChunk) => string | Promise<string>;
	footer?: string | (chunk: IRenderedChunk) => string | Promise<string>;
	entryFileNames?: string;
	chunkFileNames?: string;
}

// output只有在读取配置文件的时候才传入,调用rolldown生成时先只传InputOptions,然后调用write或generate的时候再传output
interface RolldownOptions extends InputOptions {
	output?: OutputOptions
}

rolldown-binding(rust)接收到参数类型定义

// 看代码应该是这样的
type BindingPluginOrParallelJsPluginPlaceholder = BindingPluginOptions | void;

export interface BindingInputOptions {
	external?: undefined | ((source: string, importer: string | undefined, isResolved: boolean) => boolean)
	input: Array<{
		name?: string
		import: string
	}>
	plugins: Array<BindingPluginOrParallelJsPluginPlaceholder>
	resolve?: {
		alias?: Array<{
			find: string
			replacements: Array<string>
		}>
		aliasFields?: Array<Array<string>>
		conditionNames?: Array<string>
		exportsFields?: Array<Array<string>>
		extensions?: Array<string>
		mainFields?: Array<string>
		mainFiles?: Array<string>
		modules?: Array<string>
		symlinks?: boolean
		tsconfigFilename?: string
	}
	shimMissingExports?: boolean
	platform?: 'node' | 'browser' | 'neutral'
	logLevel?: 'silent' | 'error' | 'warn' | 'info'
	cwd: string
}