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.sourcemap.has_release_id()
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!("skip [ignored]: {}", entry_path.display());
114 continue;
115 }
116
117 let source = MinifiedSourceFile::load(&entry_path)?;
118 let sourcemap_path = source.get_sourcemap_path(prefix)?;
119
120 let Some(path) = sourcemap_path else {
121 warn!("skip [no sourcemap]: {}", entry_path.display());
122 continue;
123 };
124
125 info!("new pair: {}", entry_path.display());
126 let sourcemap = SourceMapFile::load(&path).context(format!("reading {path:?}"))?;
127 pairs.push(SourcePair { source, sourcemap });
128 }
129
130 info!("found {} pairs", pairs.len());
131
132 Ok(pairs)
133}
134
135impl TryInto<SymbolSetUpload> for SourcePair {
136 type Error = anyhow::Error;
137
138 fn try_into(self) -> Result<SymbolSetUpload> {
139 let chunk_id = self
140 .get_chunk_id()
141 .ok_or_else(|| anyhow!("Chunk ID not found"))?;
142 let source_content = self.source.inner.content;
143 let sourcemap_content = serde_json::to_string(&self.sourcemap.inner.content)?;
144 let data = SourceAndMap {
145 minified_source: source_content,
146 sourcemap: sourcemap_content,
147 };
148
149 let data = write_symbol_data(data)?;
150
151 Ok(SymbolSetUpload {
152 chunk_id,
153 data,
154 release_id: self.sourcemap.get_release_id(),
155 })
156 }
157}