wakaru 1.14.0

Public Rust API for the Wakaru JavaScript decompiler
Documentation
//! Experimental standalone Vue 3 SFC recovery from compiled render modules.
//!
//! [`recover`] operates on its owned [`Source`] and parses it as an
//! independent operation; it is not covered by the root operations'
//! prepared-AST no-reparse invariant. There is deliberately no
//! framework-specific `vue::decompile` composition operation — end-to-end
//! component recovery belongs to the root operations (see the future
//! framework recovery policy in `docs/public-api.md`).

use std::fmt;
use std::sync::Arc;

use crate::error::{Error, ErrorKind, Result};
use crate::Source;

/// Resolves an import specifier to that module's source text, letting
/// recovery inspect sibling modules. Closures implement this trait.
pub trait ImportResolver: Send + Sync {
    fn resolve(&self, specifier: &str) -> Option<String>;
}

impl<F> ImportResolver for F
where
    F: Fn(&str) -> Option<String> + Send + Sync,
{
    fn resolve(&self, specifier: &str) -> Option<String> {
        self(specifier)
    }
}

#[derive(Clone, Default)]
pub struct RecoveryOptions {
    preferred_component_name: Option<String>,
    import_resolver: Option<Arc<dyn ImportResolver>>,
}

impl fmt::Debug for RecoveryOptions {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("RecoveryOptions")
            .field("preferred_component_name", &self.preferred_component_name)
            .field("has_import_resolver", &self.import_resolver.is_some())
            .finish()
    }
}

impl RecoveryOptions {
    pub fn preferred_component_name(&self) -> Option<&str> {
        self.preferred_component_name.as_deref()
    }

    pub fn with_preferred_component_name(mut self, name: impl Into<String>) -> Self {
        self.preferred_component_name = Some(name.into());
        self
    }

    pub fn with_import_resolver(mut self, resolver: impl ImportResolver + 'static) -> Self {
        self.import_resolver = Some(Arc::new(resolver));
        self
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RecoveredSfc {
    pub name: Option<String>,
    pub source: String,
}

/// Recover best-effort Vue 3 SFCs from one compiled module.
///
/// Plural by contract: one compiled module can contain multiple recoverable
/// components. An empty vector means no SFC was recovered — non-Vue input is
/// not an error.
pub fn recover(input: Source, options: RecoveryOptions) -> Result<Vec<RecoveredSfc>> {
    let input = input.into_parts();
    if input.source_map.is_some() {
        return Err(Error::new(
            ErrorKind::InvalidOptions,
            Some(input.filename),
            anyhow::anyhow!("standalone Vue recovery does not accept an input source map"),
        ));
    }

    let resolver = options.import_resolver;
    let mut core_options = wakaru_core::VueSfcRecoveryOptions::default();
    if let Some(name) = options.preferred_component_name.as_deref() {
        core_options = core_options.with_preferred_component_name(name);
    }
    if let Some(resolver) = resolver {
        core_options =
            core_options.with_import_resolver(move |specifier| resolver.resolve(specifier));
    }

    wakaru_core::recover_vue_sfcs_from_js(&input.code, core_options)
        .map(|recovered| {
            recovered
                .into_iter()
                .map(|sfc| RecoveredSfc {
                    name: sfc.name,
                    source: sfc.sfc.print(),
                })
                .collect()
        })
        .map_err(|error| Error::new(ErrorKind::Parse, Some(input.filename), error))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn plain_non_vue_module_returns_no_artifacts() {
        let recovered = recover(
            Source::new("plain.js", "export const value = 1;"),
            RecoveryOptions::default(),
        )
        .expect("valid JavaScript should recover successfully");
        assert!(recovered.is_empty());
    }

    #[test]
    fn recovery_options_own_the_name_and_resolver() {
        let name = String::from("OwnedComponent");
        let options = RecoveryOptions::default()
            .with_preferred_component_name(name)
            .with_import_resolver(|_: &str| None);
        assert_eq!(options.preferred_component_name(), Some("OwnedComponent"));
    }
}