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