写给自己看的
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,
}))
这里一共分成了四种情况
- 如果是动态引入的就调用 resolveDynamicImport 钩子处理
- 否则就尝试调用 resolveId 钩子处理
- 如果引入的是 线上资源或形如data: 则判断为外部资源直接返回
- 上面都没有命中就走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)
最终得到的是一个包含处理后引入文件信息的数组
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的钩子了。
简单总结一下,这部分代码做了啥。