posthog_cli/sourcemaps/
inject.rs

1use anyhow::{anyhow, bail, Result};
2use std::path::{Path, PathBuf};
3use tracing::info;
4use walkdir::DirEntry;
5
6use crate::{
7    api::releases::{Release, ReleaseBuilder},
8    sourcemaps::source_pairs::{read_pairs, SourcePair},
9    utils::git::get_git_info,
10};
11
12#[derive(clap::Args)]
13pub struct InjectArgs {
14    /// The directory containing the bundled chunks
15    #[arg(short, long)]
16    pub directory: PathBuf,
17
18    /// If your bundler adds a public path prefix to sourcemap URLs,
19    /// we need to ignore it while searching for them
20    /// For use alongside e.g. esbuilds "publicPath" config setting.
21    #[arg(short, long)]
22    pub public_path_prefix: Option<String>,
23
24    /// One or more directory glob patterns to ignore
25    #[arg(short, long)]
26    pub ignore: Vec<String>,
27
28    /// The project name associated with the uploaded chunks. Required to have the uploaded chunks associated with
29    /// a specific release. We will try to auto-derive this from git information if not provided. Strongly recommended
30    /// to be set explicitly during release CD workflows
31    #[arg(long)]
32    pub project: Option<String>,
33
34    /// The version of the project - this can be a version number, semantic version, or a git commit hash. Required
35    /// to have the uploaded chunks associated with a specific release.
36    #[arg(long)]
37    pub version: Option<String>,
38}
39
40pub fn inject_impl(args: &InjectArgs, matcher: impl Fn(&DirEntry) -> bool) -> Result<()> {
41    let InjectArgs {
42        directory,
43        public_path_prefix,
44        ignore,
45        project,
46        version,
47    } = args;
48
49    let directory = directory.canonicalize().map_err(|e| {
50        anyhow!(
51            "Directory '{}' not found or inaccessible: {}",
52            directory.display(),
53            e
54        )
55    })?;
56
57    info!("Processing directory: {}", directory.display());
58    let mut pairs = read_pairs(&directory, ignore, matcher, public_path_prefix)?;
59    if pairs.is_empty() {
60        bail!("No source files found");
61    }
62    info!("Found {} pairs", pairs.len());
63
64    let created_release_id = get_release_for_pairs(&directory, project, version, &pairs)?
65        .as_ref()
66        .map(|r| r.id.to_string());
67
68    pairs = inject_pairs(pairs, created_release_id)?;
69
70    // Write the source and sourcemaps back to disk
71    for pair in &pairs {
72        pair.save()?;
73    }
74    info!("Finished processing directory");
75    Ok(())
76}
77
78pub fn inject_pairs(
79    mut pairs: Vec<SourcePair>,
80    created_release_id: Option<String>,
81) -> Result<Vec<SourcePair>> {
82    for pair in &mut pairs {
83        let current_release_id = pair.get_release_id();
84        // We only update release ids and chunk ids when the release id changed or is not present
85        if current_release_id != created_release_id || pair.get_chunk_id().is_none() {
86            pair.set_release_id(created_release_id.clone());
87
88            let chunk_id = uuid::Uuid::now_v7().to_string();
89            if let Some(previous_chunk_id) = pair.get_chunk_id() {
90                pair.update_chunk_id(previous_chunk_id, chunk_id)?;
91            } else {
92                pair.add_chunk_id(chunk_id)?;
93            }
94        }
95    }
96
97    Ok(pairs)
98}
99
100pub fn get_release_for_pairs<'a>(
101    directory: &Path,
102    project: &Option<String>,
103    version: &Option<String>,
104    pairs: impl IntoIterator<Item = &'a SourcePair>,
105) -> Result<Option<Release>> {
106    // We need to fetch or create a release if: the user specified one, any pair is missing one, or the user
107    // forced release overriding
108    let needs_release =
109        project.is_some() || version.is_some() || pairs.into_iter().any(|p| !p.has_release_id());
110
111    let mut created_release = None;
112    if needs_release {
113        let mut builder = get_git_info(Some(directory.to_path_buf()))?
114            .map(ReleaseBuilder::init_from_git)
115            .unwrap_or_default();
116
117        if let Some(project) = project {
118            builder.with_project(project);
119        }
120        if let Some(version) = version {
121            builder.with_version(version);
122        }
123
124        if builder.can_create() {
125            created_release = Some(builder.fetch_or_create()?);
126        }
127    }
128
129    Ok(created_release)
130}