posthog_cli/sourcemaps/
inject.rs

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