rolldown_plugin_asset_module/
lib.rs1use std::{borrow::Cow, path::Path};
2
3use rolldown_common::{ModuleType, side_effects::HookSideEffects};
4use rolldown_plugin::{
5 HookLoadArgs, HookLoadOutput, HookLoadReturn, HookRenderChunkArgs, HookRenderChunkReturn,
6 HookUsage, Plugin, PluginHookMeta, PluginOrder, SharedLoadPluginContext,
7};
8use rolldown_plugin_utils::{emit_asset, rewrite_emitted_asset_references};
9use rolldown_utils::url::clean_url;
10use rustc_hash::FxHashSet;
11
12const PREFIX: &str = "__ROLLDOWN_ASSET__#";
13
14#[derive(Debug)]
15pub struct AssetModulePlugin {
16 asset_extensions: FxHashSet<String>,
17}
18
19impl AssetModulePlugin {
20 pub fn new(module_types: &rustc_hash::FxHashMap<Cow<'static, str>, ModuleType>) -> Self {
21 let mut asset_extensions = FxHashSet::default();
22 for (ext, module_type) in module_types {
23 if matches!(module_type, ModuleType::Asset) {
24 let ext = ext.strip_prefix('.').unwrap_or(ext);
25 asset_extensions.insert(ext.to_string());
26 }
27 }
28 Self { asset_extensions }
29 }
30}
31
32impl Plugin for AssetModulePlugin {
33 fn name(&self) -> Cow<'static, str> {
34 Cow::Borrowed("builtin:asset-module")
35 }
36
37 fn register_hook_usage(&self) -> HookUsage {
38 HookUsage::Load | HookUsage::RenderChunk
39 }
40
41 fn load_meta(&self) -> Option<PluginHookMeta> {
42 Some(PluginHookMeta { order: Some(PluginOrder::Post) })
44 }
45
46 fn load(
47 &self,
48 ctx: SharedLoadPluginContext,
49 args: &HookLoadArgs<'_>,
50 ) -> impl std::future::Future<Output = HookLoadReturn> + Send {
51 self.load_impl(ctx, args)
52 }
53
54 fn render_chunk_meta(&self) -> Option<PluginHookMeta> {
55 Some(PluginHookMeta { order: Some(PluginOrder::Pre) })
57 }
58
59 async fn render_chunk(
60 &self,
61 ctx: &rolldown_plugin::PluginContext,
62 args: &HookRenderChunkArgs<'_>,
63 ) -> HookRenderChunkReturn {
64 Ok(rewrite_emitted_asset_references(ctx, args, PREFIX))
65 }
66}
67
68impl AssetModulePlugin {
69 async fn load_impl(
70 &self,
71 ctx: SharedLoadPluginContext,
72 args: &HookLoadArgs<'_>,
73 ) -> HookLoadReturn {
74 let clean_id = clean_url(args.id);
82 let is_asset = args.asserted_module_type.is_some_and(|ty| matches!(ty, ModuleType::Asset))
83 || (args.attributes.module_type().is_none() && self.is_asset_by_extension(clean_id));
84
85 if !is_asset {
86 return Ok(None);
87 }
88
89 let reference_id = emit_asset(&ctx, clean_id, |e| {
90 anyhow::anyhow!("Failed to read asset module {clean_id}: {e}")
91 })
92 .await?;
93 ctx.associate_module_with_file_ref(args.id, &reference_id);
94 ctx.add_watch_file(clean_id);
96
97 let code = format!("module.exports = \"{PREFIX}{reference_id}\"");
101
102 Ok(Some(HookLoadOutput {
103 code: code.into(),
104 module_type: Some(ModuleType::Js),
105 side_effects: Some(HookSideEffects::False),
108 ..Default::default()
109 }))
110 }
111
112 fn is_asset_by_extension(&self, id: &str) -> bool {
113 if self.asset_extensions.is_empty() {
114 return false;
115 }
116 Path::new(id)
117 .extension()
118 .and_then(|e| e.to_str())
119 .is_some_and(|ext| self.asset_extensions.contains(ext))
120 }
121}