rolldown_plugin_utils 1.2.8

Shared utilities for Rolldown plugins
Documentation
use std::{path::Path, sync::Arc};

use arcstr::ArcStr;
use memchr::memmem;
use rolldown_common::{EmittedAsset, StrOrBytes};
use rolldown_plugin::{
  HookRenderChunkArgs, HookRenderChunkOutput, HookTransformOutputMap, PluginContext,
};
use rolldown_std_utils::relative_path_as_js_specifier;
use string_wizard::{MagicString, SourceMapOptions};

pub async fn emit_asset(
  ctx: &PluginContext,
  clean_id: &str,
  read_error: impl FnOnce(std::io::Error) -> anyhow::Error,
) -> anyhow::Result<ArcStr> {
  let path = Path::new(clean_id);
  let bytes = tokio::fs::read(clean_id).await.map_err(read_error)?;
  let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("asset").to_string();
  let original_file_name =
    path.strip_prefix(ctx.cwd()).unwrap_or(path).to_string_lossy().into_owned();

  let reference_id = ctx
    .emit_file_async(EmittedAsset {
      name: Some(file_name),
      original_file_name: Some(original_file_name),
      source: StrOrBytes::Bytes(bytes),
      ..Default::default()
    })
    .await?;

  Ok(reference_id)
}

pub fn rewrite_emitted_asset_references(
  ctx: &PluginContext,
  args: &HookRenderChunkArgs<'_>,
  prefix: &str,
) -> Option<HookRenderChunkOutput> {
  if !args.code.contains(prefix) {
    return None;
  }

  let chunk_filename = &args.chunk.filename;
  let code = args.code.as_str();
  let mut magic_string = MagicString::new(code);
  let mut changed = false;
  let finder = memmem::find_iter(code.as_bytes(), prefix.as_bytes());

  for abs_pos in finder {
    let after_prefix = abs_pos + prefix.len();
    let rest = &code[after_prefix..];
    let ref_end = rest.find(['"', '\'']).unwrap_or(rest.len());
    let ref_id = &rest[..ref_end];

    if ref_id.is_empty() {
      continue;
    }

    let Ok(asset_filename) = ctx.get_file_name(ref_id) else {
      continue;
    };
    let relative = compute_relative_path(chunk_filename, &asset_filename);
    let end = after_prefix + ref_end;

    #[expect(clippy::cast_possible_truncation)]
    if magic_string.update(abs_pos as u32, end as u32, relative).is_ok() {
      changed = true;
    }
  }

  if changed {
    Some(HookRenderChunkOutput {
      code: magic_string.to_string(),
      map: HookTransformOutputMap::from_if_enabled(args.options.sourcemap.is_some(), || {
        magic_string.source_map(SourceMapOptions {
          hires: string_wizard::Hires::Boundary,
          include_content: false,
          source: Arc::from(args.chunk.filename.as_str()),
        })
      }),
    })
  } else {
    None
  }
}

fn compute_relative_path(chunk_filename: &str, asset_filename: &str) -> String {
  let chunk_dir = Path::new(chunk_filename).parent().unwrap_or(Path::new(""));

  relative_path_as_js_specifier(asset_filename, chunk_dir)
}