【Rolldown源码】七、rolldown核心包hooks之resolveDynamicImport和resolveId

804 阅读6分钟

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

上一章解释了buildStart做了什么,代码执行到resolve_id方法,具体代码如下。

let resolver = &self.resolver;

let plugin_driver = &self.plugin_driver;

let resolved_ids = join_all(self.input_options.input.iter().map(|input_item| async move {

	struct Args<'a> {
		specifier: &'a str,
	}
	
	let args = Args { specifier: &input_item.import };
	
	let resolved = resolve_id(
		resolver,
		plugin_driver,
		args.specifier,
		None,
		HookResolveIdExtraOptions { is_entry: true, kind: ImportKind::Import },
	)
	.await; 
	
	resolved.map(|info| (args, info.map(|info| (input_item.name.clone(), info))))

}))
.await;

let mut ret = Vec::with_capacity(self.input_options.input.len());

for resolve_id in resolved_ids {
	let (args, resolve_id) = resolve_id?;
	
	match resolve_id {
		Ok(item) => {
			ret.push(item);
		}
		Err(e) => match e {
			ResolveError::NotFound(..) => {
				self.errors.push(BuildError::unresolved_entry(args.specifier, None));
			}
			ResolveError::PackagePathNotExported(..) => {
			self.errors.push(BuildError::unresolved_entry(args.specifier, Some(e)));
			}
			_ => {
			return Err(e.into());
			}
		},
	}
}
Ok(ret)

这里遍历入参数input,每个入口参数都传入 resolve_id 方法

let import_kind = options.kind;
if matches!(import_kind, ImportKind::DynamicImport) {
	if let Some(r) = plugin_driver
	  .resolve_dynamic_import(&HookResolveDynamicImportArgs {
		importer: importer.map(std::convert::AsRef::as_ref),
		source: request,
	  })
	  .await?
	{
	  return Ok(Ok(ResolvedRequestInfo {
		module_type: ModuleType::from_path(&r.id),
		path: r.id.into(),
		is_external: matches!(r.external, Some(true)),
	  }));
	}
}
// Run plugin resolve_id first, if it is None use internal resolver as fallback
if let Some(r) = plugin_driver
.resolve_id(&HookResolveIdArgs {
  importer: importer.map(std::convert::AsRef::as_ref),
  source: request,
  options,
})
.await?
{
	return Ok(Ok(ResolvedRequestInfo {
	  module_type: ModuleType::from_path(&r.id),
	  path: r.id.into(),
	  is_external: matches!(r.external, Some(true)),
	}));
}

// Auto external http url or data url
if is_http_url(request) || is_data_url(request) {
	return Ok(Ok(ResolvedRequestInfo {
	  path: request.to_string().into(),
	  module_type: ModuleType::Unknown,
	  is_external: true,
	}));
}

// Rollup external node packages by default.
// Rolldown will follow esbuild behavior to resolve it by default.
// See https://github.com/rolldown/rolldown/issues/282
let resolved = resolver.resolve(importer.map(Path::new), request, import_kind)?;
Ok(resolved.map(|resolved| ResolvedRequestInfo {
	path: resolved.path,
	module_type: resolved.module_type,
	is_external: false,
}))

这里一共分成了四种情况

  1. 如果是动态引入的就调用 resolveDynamicImport 钩子处理
  2. 否则就尝试调用 resolveId 钩子处理
  3. 如果引入的是 线上资源或形如data: 则判断为外部资源直接返回
  4. 上面都没有命中就走rolldown的默认处理流程

这边给大家看看测试用例中resolveDynamicImport和resolveId钩子是怎么写的

const entry = path.join(__dirname, './main.js')
config: {
	input: entry,
	plugins: [{
		name: 'test-plugin',
		resolveDynamicImport: function (id, importer) {
			resolveDynamicImport()			
			if (id === 'foo') {
				expect(importer).toStrictEqual(entry)
				return {
					id: path.join(__dirname, './foo.js'),
				}
			}
		},
	}],
},
  • resolveId的plugin代码
// main.js
import './foo'
require('external')

// foo.js
// blank

// config
const entry = path.join(__dirname, './main.js')
config: {
	input: entry,
	plugins: [{
		name: 'test-plugin',
		resolveId: function (id, importer, options) {
			resolveIdFn()
			if (id === 'external') {
				expect(importer).toStrictEqual(entry)
				expect(options).toMatchObject({
					isEntry: false,
					kind: 'require-call',
				})
			
				return {
					id,
					external: true,
				}
			}
			
			if (id === './foo') {
				expect(importer).toStrictEqual(entry)
				expect(options).toMatchObject({
					isEntry: false,
					kind: 'import-statement',
				})
			
				return {
					id: path.join(__dirname, './foo.js'),
					external: false,	
				}
			}
			
			if (id === entry) {
				expect(importer).toBeUndefined()
				expect(options).toMatchObject({
					isEntry: true,
					kind: 'import-statement',
				})
			}
		},
	}],
}

`` 具体的逻辑就不说了,反正就是根据自己的逻辑去处理imports。

我们主要看看rolldown默认是怎么处理的imports的。

// 入口代码
let resolved = resolver.resolve(importer.map(Path::new), request, import_kind)?;

pub enum ImportKind {
	Import,
	DynamicImport,
	Require,
}

这边传入了三个参数,一个路径,一个import对象,一个import类型

import类型也分三种,普通import、动态import、request,说明这边磨平了多种引入的语法差异。

接下来看看resolve方法中做了啥

pub fn resolve(
	&self,
	importer: Option<&Path>,
	specifier: &str,
	import_kind: ImportKind,
) -> anyhow::Result<Result<ResolveReturn, ResolveError>> {
	// 先判断使用什么方式引入模块
	let selected_resolver = match import_kind {
		ImportKind::Import | ImportKind::DynamicImport => &self.import_resolver,
		ImportKind::Require => &self.require_resolver,
	};
	
	let resolution = if let Some(importer) = importer {
		let context = importer.parent().expect("Should have a parent dir");
		selected_resolver.resolve(context, specifier)
	} else {
		// importer如果为None,一般是input的原因
		// 因为兼容rollup所以一般默认的路径是 <cwd>/main.{js,mjs} 文件
		let joined_specifier = self.cwd.join(specifier).normalize();

		// 这里判断是否为相对或绝对路径,如果不是一般需要特殊处理
		let is_path_like = specifier.starts_with('.') || specifier.starts_with('/');

		// 底层调用 oxc_resolve 方法,处理引入的字符串,将其解析为引入文件的绝对路径
		// https://juejin.cn/column/7376821583048818740 以后oxc信息放这里
		let resolution = selected_resolver.resolve(&self.cwd, joined_specifier.to_str().unwrap());
		
		if resolution.is_ok() {
			resolution
		} else if !is_path_like {
			// 特殊处理非相对或绝对路径
			selected_resolver.resolve(&self.cwd, specifier)
		} else {
			resolution
		}
	};
	
	match resolution {
		Ok(info) => {
			// 判断文件类型,具体逻辑可以看下面 ModuleType 的枚举注释
			let module_type = calc_module_type(&info);
			// 成功并返回信息
			Ok(Ok(build_resolve_ret(
				info.full_path().to_str().expect("Should be valid utf8").to_string(),
				false,
				module_type,
			)))
		}
		// 错误处理
		Err(err) => match err {
			ResolveError::Ignored(p) => Ok(Ok(build_resolve_ret(
				p.to_str().expect("Should be valid utf8").to_string(),
				true,
				ModuleType::Unknown,
			))),
			_ => Ok(Err(err)),
			
		},
	}
}

pub enum ModuleType {
	Unknown,
	// ".cjs"
	CJS,
	// "type: commonjs" in package.json
	CjsPackageJson,
	// ".mjs"
	EsmMjs,
	// "type: module" in package.json
	EsmPackageJson,
}

这里简单提一下 oxc_resolve ,这个包是webpack 中 enhanced-resolve 的rust实现。 主要就是将 require / import 语句中引入的字符串,解析为引入文件的绝对路径。

整体处理完的成功返回的信息如下,拿到了引入文件的绝对路径和引入文件的类型

pub struct ResolveReturn {
	pub path: ResolvedPath,
	pub module_type: ModuleType,
}

回到主流程中,其实我们还在 resolve_user_defined_entries ,看名字就知道我们还在处理input参数的流程中。

展示一下resolve_user_defined_entries后续的代码

let mut ret = Vec::with_capacity(self.input_options.input.len());

for resolve_id in resolved_ids {

	let (args, resolve_id) = resolve_id?;

	match resolve_id {
		Ok(item) => {
			// 将resolve后的数据塞入ret中
			ret.push(item);
		}
		Err(e) => match e {
			ResolveError::NotFound(..) => {
				self.errors.push(BuildError::unresolved_entry(args.specifier, None));
			}
			ResolveError::PackagePathNotExported(..) => {
				self.errors.push(BuildError::unresolved_entry(args.specifier, Some(e)));
			}
			_ => {
				return Err(e.into());
			}
		},
	}
}

Ok(ret)

最终得到的是一个包含处理后引入文件信息的数组

image.png

resolve_user_defined_entries 执行完返回处理完后的入口信息,我们再回到scan方法的后续流程代码中


let ModuleLoaderOutput {
	module_table,
	entry_points,
	symbols,
	runtime,
	warnings,
	errors,
	ast_table,
} = module_loader.fetch_all_modules(user_entries).await?;

看代码就应该知道,接下来要根据入口信息请求所有的模块了。

照例,我们先show一下fetch_all_modules的代码

pub async fn fetch_all_modules(
	mut self,
	user_defined_entries: Vec<(Option<String>, ResolvedRequestInfo)>,
) -> anyhow::Result<ModuleLoaderOutput> {
	// 处理异常情况
	if self.input_options.input.is_empty() {
		return Err(anyhow::format_err!("You must supply options.input to rolldown"));
	}
	
	let mut errors = vec![];
	let mut all_warnings: Vec<BuildError> = Vec::new();

	// 下面两个都是为了重新分配空间用的,input的数量n加上runtime文件,为 n + 1
	self
		.intermediate_normal_modules
		.modules
		.reserve(user_defined_entries.len() + 1 /* runtime */);
	self
		.intermediate_normal_modules
		.ast_table
		.reserve(user_defined_entries.len() + 1 /* runtime */);
}

接下里是fetch_all_modules方法中比较核心的实现

let mut entry_points = user_defined_entries
	.into_iter()
	.map(|(name, info)| EntryPoint {
		name,
		id: self.try_spawn_new_task(&info, true).expect_normal(),
		kind: EntryPointKind::UserDefined,
	})
	.inspect(|e| {
		user_defined_entry_ids.insert(e.id);
	})
	.collect::<Vec<_>>();

重点看其中的try_spawn_new_task方法

fn try_spawn_new_task(  
  &mut self,  
  info: &ResolvedRequestInfo,  
  is_user_defined_entry: bool,  
) -> ModuleId {
  // 判断是否处理过对应的文件
  match self.visited.entry(Arc::<str>::clone(&info.path.path)) {
	// 处理过就返回对应的id 
    std::collections::hash_map::Entry::Occupied(visited) => *visited.get(),  
    std::collections::hash_map::Entry::Vacant(not_visited) => {
	  // external 模块的处理逻辑(无需特殊处理)
      if info.is_external {  
        let id = self.external_modules.len_idx();  
        not_visited.insert(id.into());  
        let ext = ExternalModule::new(id, info.path.path.to_string());  
        self.external_modules.push(ext);  
        id.into()  
      } else {  
		// 普通模块的处理逻辑(无需特殊处理)
		// 创建空数据,并返回id
        let id = self.intermediate_normal_modules.alloc_module_id(&mut self.symbols);  
        // 缓存id,防止重复处理
        not_visited.insert(id.into());  
        // 需要处理模块计数 +1
        self.remaining += 1;  
        let module_path = info.path.clone();  

		// 下面代码是不是似曾相识,其实跟runtime代码逻辑一样,创建一个任务,然后异步执行
        let task = NormalModuleTask::new(  
          Arc::clone(&self.shared_context),  
          id,  
          module_path,  
          info.module_type,  
          is_user_defined_entry,  
        );   
        tokio::spawn(task.run());  
        id.into()  
      }  
    }  
  }  
}

这个方法就是专门用来处理对应引入文件用的,是个通用的方法,所有的后续import、ruquire引入文件都要走这个方法。

接下来再看看 run 方法里面做了什么

pub async fn run(mut self) {  
  match self.run_inner().await {  
    Ok(()) => {  
      if !self.errors.is_empty() {  
        self.ctx.tx.send(Msg::BuildErrors(self.errors)).await.expect("Send should not fail");  
      }  
    }  
    Err(err) => {  
      self.ctx.tx.send(Msg::Panics(err)).await.expect("Send should not fail");  
    }  
  }  
}

下集再看run_inner方法,因为看代码已经走到了load和transform的钩子了。

简单总结一下,这部分代码做了啥。

image.png