posthog_cli/sourcemaps/
source_pairs.rs1use std::path::PathBuf;
2
3use crate::{
4 api::symbol_sets::SymbolSetUpload,
5 sourcemaps::content::{MinifiedSourceFile, SourceMapFile},
6};
7use anyhow::{anyhow, bail, Context, Result};
8use globset::{Glob, GlobSetBuilder};
9use posthog_symbol_data::{write_symbol_data, SourceAndMap};
10use tracing::{info, warn};
11use walkdir::{DirEntry, WalkDir};
12
13pub struct SourcePair {
15 pub source: MinifiedSourceFile,
16 pub sourcemap: SourceMapFile,
17}
18
19impl SourcePair {
20 pub fn has_chunk_id(&self) -> bool {
21 self.get_chunk_id().is_some()
24 }
25
26 pub fn get_chunk_id(&self) -> Option<String> {
27 self.source.get_chunk_id()
28 }
29
30 pub fn has_release_id(&self) -> bool {
31 self.get_release_id().is_some()
32 }
33
34 pub fn get_release_id(&self) -> Option<String> {
35 self.sourcemap.get_release_id()
36 }
37
38 pub fn remove_chunk_id(&mut self, chunk_id: String) -> Result<()> {
39 if self.get_chunk_id().as_ref() != Some(&chunk_id) {
40 return Err(anyhow!("Chunk ID mismatch"));
41 }
42 let adjustment = self.source.remove_chunk_id(chunk_id)?;
43 self.sourcemap.apply_adjustment(adjustment)?;
44 self.sourcemap.set_chunk_id(None);
45 Ok(())
46 }
47
48 pub fn update_chunk_id(
49 &mut self,
50 previous_chunk_id: String,
51 new_chunk_id: String,
52 ) -> Result<()> {
53 self.remove_chunk_id(previous_chunk_id)?;
54 self.add_chunk_id(new_chunk_id)?;
55 Ok(())
56 }
57
58 pub fn add_chunk_id(&mut self, chunk_id: String) -> Result<()> {
59 if self.has_chunk_id() {
60 return Err(anyhow!("Chunk ID already set"));
61 }
62
63 let adjustment = self.source.set_chunk_id(&chunk_id)?;
64 if self.sourcemap.get_chunk_id().is_none() {
68 self.sourcemap.apply_adjustment(adjustment)?;
69 self.sourcemap.set_chunk_id(Some(chunk_id));
70 }
71 Ok(())
72 }
73
74 pub fn set_release_id(&mut self, release_id: Option<String>) {
75 self.sourcemap.set_release_id(release_id);
76 }
77
78 pub fn save(&self) -> Result<()> {
79 self.source.save()?;
80 self.sourcemap.save()?;
81 Ok(())
82 }
83}
84
85pub fn read_pairs(
86 directory: &PathBuf,
87 ignore_globs: &[String],
88 matcher: impl Fn(&DirEntry) -> bool,
89 prefix: &Option<String>,
90) -> Result<Vec<SourcePair>> {
91 if !directory.exists() {
93 bail!("Directory does not exist");
94 }
95
96 let mut builder = GlobSetBuilder::new();
97 for glob in ignore_globs {
98 builder.add(Glob::new(glob)?);
99 }
100 let set: globset::GlobSet = builder.build()?;
101
102 let mut pairs = Vec::new();
103
104 for entry_path in WalkDir::new(directory)
105 .into_iter()
106 .filter_map(|e| e.ok())
107 .filter(matcher)
108 .map(|e| e.path().canonicalize())
109 {
110 let entry_path = entry_path?;
111
112 if set.is_match(&entry_path) {
113 info!(
114 "Skipping because it matches an ignored glob: {}",
115 entry_path.display()
116 );
117 continue;
118 }
119
120 info!("Processing file: {}", entry_path.display());
121 let source = MinifiedSourceFile::load(&entry_path)?;
122 let sourcemap_path = source.get_sourcemap_path(prefix)?;
123
124 let Some(path) = sourcemap_path else {
125 warn!(
126 "No sourcemap file found for file {}, skipping",
127 entry_path.display()
128 );
129 continue;
130 };
131
132 let sourcemap = SourceMapFile::load(&path).context(format!("reading {path:?}"))?;
133 pairs.push(SourcePair { source, sourcemap });
134 }
135
136 Ok(pairs)
137}
138
139impl TryInto<SymbolSetUpload> for SourcePair {
140 type Error = anyhow::Error;
141
142 fn try_into(self) -> Result<SymbolSetUpload> {
143 let chunk_id = self
144 .get_chunk_id()
145 .ok_or_else(|| anyhow!("Chunk ID not found"))?;
146 let source_content = self.source.inner.content;
147 let sourcemap_content = serde_json::to_string(&self.sourcemap.inner.content)?;
148 let data = SourceAndMap {
149 minified_source: source_content,
150 sourcemap: sourcemap_content,
151 };
152
153 let data = write_symbol_data(data)?;
154
155 Ok(SymbolSetUpload {
156 chunk_id,
157 data,
158 release_id: self.sourcemap.get_release_id(),
159 })
160 }
161}