use crate::Runtime;
use crate::plugins::{FobPlugin, PluginPhase};
use crate::runtime::BundlerRuntime;
use anyhow::Context;
use rolldown_common::{ModuleType, ResolvedExternal};
use rolldown_plugin::{
HookLoadArgs, HookLoadOutput, HookLoadReturn, HookResolveIdArgs, HookResolveIdOutput,
HookResolveIdReturn, Plugin, PluginContext,
};
use std::borrow::Cow;
use std::path::Path;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct RuntimeFilePlugin {
runtime: Arc<BundlerRuntime>,
}
impl RuntimeFilePlugin {
pub fn new(runtime: Arc<BundlerRuntime>) -> Self {
Self { runtime }
}
}
impl Plugin for RuntimeFilePlugin {
fn name(&self) -> Cow<'static, str> {
"fob-runtime-files".into()
}
fn register_hook_usage(&self) -> rolldown_plugin::HookUsage {
use rolldown_plugin::HookUsage;
HookUsage::ResolveId | HookUsage::Load
}
fn resolve_id(
&self,
_ctx: &PluginContext,
args: &HookResolveIdArgs,
) -> impl std::future::Future<Output = HookResolveIdReturn> + Send {
let specifier = args.specifier.to_string();
let runtime = Arc::clone(&self.runtime);
async move {
let path = Path::new(&specifier);
if runtime.has_virtual_file(path) {
return Ok(Some(HookResolveIdOutput {
id: specifier.into(),
external: Some(ResolvedExternal::Bool(false)),
..Default::default()
}));
}
for ext in &[".tsx", ".ts", ".jsx", ".js"] {
let with_ext = format!("{}{}", specifier, ext);
let path = Path::new(&with_ext);
if runtime.has_virtual_file(path) {
return Ok(Some(HookResolveIdOutput {
id: with_ext.into(),
external: Some(ResolvedExternal::Bool(false)),
..Default::default()
}));
}
}
Ok(None)
}
}
fn load(
&self,
_ctx: &PluginContext,
args: &HookLoadArgs<'_>,
) -> impl std::future::Future<Output = HookLoadReturn> + Send {
let id = args.id.to_string();
let runtime = Arc::clone(&self.runtime);
async move {
let path = Path::new(&id);
if !runtime.has_virtual_file(path) {
return Ok(None);
}
if id.ends_with(".mdx") {
return Ok(None);
}
let content = Runtime::read_file(&*runtime, path)
.await
.with_context(|| format!("Failed to read virtual file: {}", id))?;
let source = String::from_utf8(content)
.with_context(|| format!("Virtual file {} contains invalid UTF-8", id))?;
Ok(Some(HookLoadOutput {
code: source.into(),
module_type: Some(infer_module_type(&id)),
..Default::default()
}))
}
}
}
impl FobPlugin for RuntimeFilePlugin {
fn phase(&self) -> PluginPhase {
PluginPhase::Virtual
}
}
fn infer_module_type(id: &str) -> ModuleType {
match Path::new(id).extension().and_then(|e| e.to_str()) {
Some("tsx") => ModuleType::Tsx,
Some("ts") => ModuleType::Ts,
Some("jsx") => ModuleType::Jsx,
Some("mdx") => ModuleType::Jsx, Some("css") => ModuleType::Css,
Some("json") => ModuleType::Json,
Some("js") | Some("mjs") | Some("cjs") => ModuleType::Js,
_ => ModuleType::Js, }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_infer_module_type() {
assert!(matches!(infer_module_type("file.js"), ModuleType::Js));
assert!(matches!(infer_module_type("file.jsx"), ModuleType::Jsx));
assert!(matches!(infer_module_type("file.ts"), ModuleType::Ts));
assert!(matches!(infer_module_type("file.tsx"), ModuleType::Tsx));
assert!(matches!(infer_module_type("file.mdx"), ModuleType::Jsx));
assert!(matches!(infer_module_type("file.css"), ModuleType::Css));
assert!(matches!(infer_module_type("file.json"), ModuleType::Json));
}
}