source-map-tauri 0.3.0

Static Tauri app scanner that emits Meilisearch-ready NDJSON
Documentation
use std::{collections::BTreeSet, path::Path};

use anyhow::Result;
use regex::Regex;
use serde_json::{Map, Value};

use crate::{
    config::{normalize_path, ResolvedConfig},
    discovery::RepoDiscovery,
    ids::document_id,
    model::ArtifactDoc,
    security::apply_artifact_security,
};

fn line_number(text: &str, offset: usize) -> u32 {
    text[..offset].bytes().filter(|byte| *byte == b'\n').count() as u32 + 1
}

fn base_artifact(
    config: &ResolvedConfig,
    path: &Path,
    kind: &str,
    name: &str,
    line: u32,
) -> ArtifactDoc {
    let source_path = normalize_path(&config.root, path);
    let mut doc = ArtifactDoc {
        id: document_id(
            &config.repo,
            kind,
            Some(&source_path),
            Some(line),
            Some(name),
        ),
        repo: config.repo.clone(),
        kind: kind.to_owned(),
        side: Some("frontend".to_owned()),
        language: crate::frontend::language_for_path(path),
        name: Some(name.to_owned()),
        display_name: Some(name.to_owned()),
        source_path: Some(source_path),
        line_start: Some(line),
        line_end: Some(line),
        column_start: None,
        column_end: None,
        package_name: None,
        comments: Vec::new(),
        tags: Vec::new(),
        related_symbols: Vec::new(),
        related_tests: Vec::new(),
        risk_level: "low".to_owned(),
        risk_reasons: Vec::new(),
        contains_phi: false,
        has_related_tests: false,
        updated_at: chrono::Utc::now().to_rfc3339(),
        data: Map::new(),
    };
    apply_artifact_security(&mut doc);
    doc
}

pub fn extract_components_and_hooks(
    config: &ResolvedConfig,
    path: &Path,
    text: &str,
    known_hooks: &BTreeSet<String>,
) -> Vec<ArtifactDoc> {
    let export_fn =
        Regex::new(r"(?m)^\s*export\s+function\s+([A-Za-z0-9_]+)").expect("valid regex");
    let hook_call = Regex::new(r"\b(use[A-Z][A-Za-z0-9_]*)\(").expect("valid regex");

    let mut docs = Vec::new();
    let mut component_names = Vec::new();

    for capture in export_fn.captures_iter(text) {
        let whole = capture.get(0).expect("match");
        let name = capture.get(1).expect("name").as_str();
        let line = line_number(text, whole.start());
        if name.starts_with("use") {
            let mut doc = base_artifact(config, path, "frontend_hook_def", name, line);
            let hook_kind = if text.contains("new Channel") || text.contains("Channel<") {
                "channel_stream"
            } else if text.contains("listen(") || text.contains("once(") {
                "event_subscription"
            } else if text.contains("invoke(") {
                "invoke_once"
            } else {
                "unknown"
            };
            doc.display_name = Some(format!("{name} hook"));
            doc.tags = vec!["custom hook".to_owned()];
            doc.data
                .insert("hook_kind".to_owned(), Value::String(hook_kind.to_owned()));
            doc.data.insert(
                "requires_cleanup".to_owned(),
                Value::Bool(text.contains("listen(") || text.contains("once(")),
            );
            doc.data.insert(
                "cleanup_present".to_owned(),
                Value::Bool(text.contains("return () =>") || text.contains("unlisten")),
            );
            apply_artifact_security(&mut doc);
            docs.push(doc);
        } else if name
            .chars()
            .next()
            .map(|item| item.is_uppercase())
            .unwrap_or(false)
        {
            let mut doc = base_artifact(config, path, "frontend_component", name, line);
            doc.display_name = Some(format!("{name} component"));
            doc.tags = vec!["component".to_owned()];
            doc.data
                .insert("component".to_owned(), Value::String(name.to_owned()));
            apply_artifact_security(&mut doc);
            component_names.push(name.to_owned());
            docs.push(doc);
        }
    }

    for capture in hook_call.captures_iter(text) {
        let whole = capture.get(0).expect("match");
        let name = capture.get(1).expect("name").as_str();
        if !known_hooks.contains(name) {
            continue;
        }
        if text[whole.start()..whole.end()].starts_with("function ") {
            continue;
        }
        let line = line_number(text, whole.start());
        let mut doc = base_artifact(config, path, "frontend_hook_use", name, line);
        if let Some(component) = component_names.first() {
            doc.data
                .insert("component".to_owned(), Value::String(component.clone()));
            doc.display_name = Some(format!("{component} uses {name}"));
        }
        doc.data
            .insert("hook_kind".to_owned(), Value::String("unknown".to_owned()));
        doc.data
            .insert("hook_def_name".to_owned(), Value::String(name.to_owned()));
        apply_artifact_security(&mut doc);
        docs.push(doc);
    }

    docs
}

pub fn discover_hook_names(discovery: &RepoDiscovery) -> Result<BTreeSet<String>> {
    let export_fn =
        Regex::new(r"(?m)^\s*export\s+function\s+(use[A-Z][A-Za-z0-9_]*)").expect("valid regex");
    let mut names = BTreeSet::new();

    for path in &discovery.frontend_files {
        let text = std::fs::read_to_string(path)?;
        for capture in export_fn.captures_iter(&text) {
            if let Some(name) = capture.get(1) {
                names.insert(name.as_str().to_owned());
            }
        }
    }

    Ok(names)
}