posthog_cli/utils/
sourcemaps.rs1use anyhow::{anyhow, bail, Context, Ok, Result};
2use core::str;
3use posthog_symbol_data::{write_symbol_data, SourceAndMap};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::{
7 collections::HashMap,
8 path::{Path, PathBuf},
9};
10use tracing::{info, warn};
11use walkdir::WalkDir;
12
13pub struct Source {
14 path: PathBuf,
15 pub content: String,
16}
17
18impl Source {
19 pub fn get_sourcemap_path(&self) -> PathBuf {
20 let mut path = self.path.clone();
22 match path.extension() {
23 Some(ext) => path.set_extension(format!("{}.map", ext.to_string_lossy())),
24 None => path.set_extension("map"),
25 };
26 path
27 }
28
29 pub fn add_chunk_id(&mut self, chunk_id: String) {
30 self.prepend(&format!(r#"!function(){{try{{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{{}},n=(new e.Error).stack;n&&(e._posthogChunkIds=e._posthogChunkIds||{{}},e._posthogChunkIds[n]="{}")}}catch(e){{}}}}();"#, chunk_id));
31 self.append(&format!(r#"//# chunkId={}"#, chunk_id));
32 }
33
34 pub fn read(path: &PathBuf) -> Result<Source> {
35 let content = std::fs::read_to_string(path)
36 .map_err(|_| anyhow!("Failed to read source file: {}", path.display()))?;
37 Ok(Source {
38 path: path.clone(),
39 content,
40 })
41 }
42
43 pub fn write(&self) -> Result<()> {
44 std::fs::write(&self.path, &self.content)?;
45 Ok(())
46 }
47
48 pub fn prepend(&mut self, prefix: &str) {
49 self.content.insert_str(0, prefix);
50 }
51
52 pub fn append(&mut self, suffix: &str) {
53 self.content.push_str(suffix);
54 }
55}
56
57#[derive(Debug, Serialize, Deserialize)]
58pub struct SourceMapContent {
59 chunk_id: Option<String>,
60 #[serde(flatten)]
61 fields: HashMap<String, Value>,
62}
63
64#[derive(Debug)]
65pub struct SourceMap {
66 pub path: PathBuf,
67 content: SourceMapContent,
68}
69
70impl SourceMap {
71 pub fn add_chunk_id(&mut self, chunk_id: String) -> Result<()> {
72 if self.content.chunk_id.is_some() {
73 bail!("Sourcemap has already been processed");
74 }
75 self.content.chunk_id = Some(chunk_id);
76 Ok(())
77 }
78
79 pub fn read(path: &PathBuf) -> Result<SourceMap> {
80 let content = serde_json::from_slice(&std::fs::read(path)?)?;
81 Ok(SourceMap {
82 path: path.clone(),
83 content,
84 })
85 }
86
87 pub fn write(&self) -> Result<()> {
88 std::fs::write(&self.path, self.to_string()?)?;
89 Ok(())
90 }
91
92 pub fn chunk_id(&self) -> Option<String> {
93 self.content.chunk_id.clone()
94 }
95
96 pub fn to_string(&self) -> Result<String> {
97 serde_json::to_string(&self.content)
98 .map_err(|e| anyhow!("Failed to serialize sourcemap content: {}", e))
99 }
100}
101
102pub struct SourcePair {
103 pub source: Source,
104 pub sourcemap: SourceMap,
105}
106
107pub struct ChunkUpload {
108 pub chunk_id: String,
109 pub data: Vec<u8>,
110}
111
112impl SourcePair {
113 pub fn add_chunk_id(&mut self, chunk_id: String) -> Result<()> {
114 self.source.add_chunk_id(chunk_id.clone());
115 self.sourcemap.add_chunk_id(chunk_id)?;
116 Ok(())
117 }
118
119 pub fn write(&self) -> Result<()> {
120 self.source.write()?;
121 self.sourcemap.write()?;
122 Ok(())
123 }
124
125 pub fn chunk_id(&self) -> Option<String> {
126 self.sourcemap.chunk_id()
127 }
128
129 pub fn into_chunk_upload(self) -> Result<ChunkUpload> {
130 let chunk_id = self
131 .chunk_id()
132 .ok_or_else(|| anyhow!("Chunk ID not found"))?;
133 let sourcemap_content = self
134 .sourcemap
135 .to_string()
136 .context("Failed to serialize sourcemap")?;
137 let data = SourceAndMap {
138 minified_source: self.source.content,
139 sourcemap: sourcemap_content,
140 };
141 let data = write_symbol_data(data)?;
142 Ok(ChunkUpload { chunk_id, data })
143 }
144}
145
146pub fn read_pairs(directory: &PathBuf) -> Result<Vec<SourcePair>> {
147 if !directory.exists() {
149 bail!("Directory does not exist");
150 }
151
152 let mut pairs = Vec::new();
153 for entry in WalkDir::new(directory).into_iter().filter_map(|e| e.ok()) {
154 let entry_path = entry.path().canonicalize()?;
155 info!("Processing file: {}", entry_path.display());
156 if is_javascript_file(&entry_path) {
157 let source = Source::read(&entry_path)?;
158 let sourcemap_path = source.get_sourcemap_path();
159 if sourcemap_path.exists() {
160 let sourcemap = SourceMap::read(&sourcemap_path)?;
161 pairs.push(SourcePair { source, sourcemap });
162 } else {
163 warn!("No sourcemap file found for file {}", entry_path.display());
164 }
165 }
166 }
167 Ok(pairs)
168}
169
170fn is_javascript_file(path: &Path) -> bool {
171 path.extension()
172 .map_or(false, |ext| ext == "js" || ext == "mjs" || ext == "cjs")
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use anyhow::{Context, Result};
179 use std::fs::File;
180 use std::io::Write;
181 use tempfile::{tempdir, TempDir};
182 use test_log::test;
183 use tracing::info;
184
185 fn create_pair(dir: &TempDir, path: &str, pair_name: &str, extension: &str) -> Result<()> {
186 let sub_path = dir.path().join(path);
187 if !sub_path.exists() {
188 std::fs::create_dir_all(&sub_path)?;
189 }
190 let js_path = sub_path.join(format!("{}.{}", pair_name, extension));
191 info!("Creating file: {:?}", js_path);
192 let mut file = File::create(&js_path).context("Failed to create file")?;
193 let map_path = sub_path.join(format!("{}.{}.{}", pair_name, extension, "map"));
194 let mut map_file = File::create(&map_path).context("Failed to create map")?;
195 writeln!(file, "console.log('hello');").context("Failed to write to file")?;
196 writeln!(map_file, "{{}}").context("Failed to write to file")?;
197 Ok(())
198 }
199
200 fn setup_test_directory() -> Result<TempDir> {
201 let dir = tempdir()?;
202 create_pair(&dir, "", "regular", "js")?;
203 create_pair(&dir, "assets", "module", "mjs")?;
204 create_pair(&dir, "assets/sub", "common", "cjs")?;
205 Ok(dir)
206 }
207
208 #[test]
209 fn test_tempdir_creation() {
210 let dist_dir = setup_test_directory().unwrap();
211 assert!(dist_dir.path().exists());
212 let dist_dir_path = dist_dir.path().to_path_buf();
213 let pairs = read_pairs(&dist_dir_path).expect("Failed to read pairs");
214 assert_eq!(pairs.len(), 3);
215 }
216}