posthog_cli/utils/
sourcemaps.rs1use anyhow::{anyhow, bail, Ok, Result};
2use core::str;
3use magic_string::{GenerateDecodedMapOptions, MagicString};
4use posthog_symbol_data::{write_symbol_data, SourceAndMap};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use sourcemap::SourceMap;
8use std::collections::BTreeMap;
9use std::{
10 collections::HashMap,
11 path::{Path, PathBuf},
12};
13use tracing::{info, warn};
14use walkdir::WalkDir;
15
16use super::constant::{CHUNKID_COMMENT_PREFIX, CHUNKID_PLACEHOLDER, CODE_SNIPPET_TEMPLATE};
17
18pub struct SourceFile {
19 pub path: PathBuf,
20 pub content: String,
21}
22
23#[derive(Debug, Serialize, Deserialize)]
24pub struct SourceMapChunkId {
25 chunk_id: Option<String>,
26 #[serde(flatten)]
27 fields: BTreeMap<String, Value>,
28}
29
30impl SourceFile {
31 pub fn new(path: PathBuf, content: String) -> Self {
32 SourceFile { path, content }
33 }
34
35 pub fn load(path: &PathBuf) -> Result<Self> {
36 let content = std::fs::read_to_string(path)?;
37 Ok(SourceFile::new(path.clone(), content))
38 }
39
40 pub fn save(&self, dest: Option<PathBuf>) -> Result<()> {
41 let final_path = dest.unwrap_or(self.path.clone());
42 std::fs::write(&final_path, &self.content)?;
43 Ok(())
44 }
45}
46
47#[derive(Debug, Serialize, Deserialize)]
48pub struct SourceMapContent {
49 chunk_id: Option<String>,
50 #[serde(flatten)]
51 fields: HashMap<String, Value>,
52}
53
54pub struct SourcePair {
55 pub chunk_id: Option<String>,
56
57 pub source: SourceFile,
58 pub sourcemap: SourceFile,
59}
60
61pub struct ChunkUpload {
62 pub chunk_id: String,
63 pub data: Vec<u8>,
64}
65
66impl SourcePair {
67 pub fn set_chunk_id(&mut self, chunk_id: String) -> Result<()> {
68 if self.chunk_id.is_some() {
69 return Err(anyhow!("Chunk ID already set"));
70 }
71 let (new_source_content, source_adjustment) = {
72 let source_content = &self.source.content;
74 let mut magic_source = MagicString::new(source_content);
75 let code_snippet = CODE_SNIPPET_TEMPLATE.replace(CHUNKID_PLACEHOLDER, &chunk_id);
76 magic_source
77 .prepend(&code_snippet)
78 .map_err(|err| anyhow!("Failed to prepend code snippet: {}", err))?;
79 let chunk_comment = CHUNKID_COMMENT_PREFIX.replace(CHUNKID_PLACEHOLDER, &chunk_id);
80 magic_source
81 .append(&chunk_comment)
82 .map_err(|err| anyhow!("Failed to append chunk comment: {}", err))?;
83 let adjustment = magic_source
84 .generate_map(GenerateDecodedMapOptions {
85 include_content: true,
86 ..Default::default()
87 })
88 .map_err(|err| anyhow!("Failed to generate source map: {}", err))?;
89 let adjustment_sourcemap = SourceMap::from_slice(
90 adjustment
91 .to_string()
92 .map_err(|err| anyhow!("Failed to serialize source map: {}", err))?
93 .as_bytes(),
94 )
95 .map_err(|err| anyhow!("Failed to parse adjustment sourcemap: {}", err))?;
96 (magic_source.to_string(), adjustment_sourcemap)
97 };
98
99 let new_sourcemap = {
100 let mut original_sourcemap =
102 SourceMap::from_slice(self.sourcemap.content.as_bytes())
103 .map_err(|err| anyhow!("Failed to parse sourcemap: {}", err))?;
104 original_sourcemap.adjust_mappings(&source_adjustment);
105
106 let mut new_sourcemap_bytes = Vec::new();
107 original_sourcemap.to_writer(&mut new_sourcemap_bytes)?;
108
109 let mut sourcemap_chunk: SourceMapChunkId =
110 serde_json::from_slice(&new_sourcemap_bytes)?;
111 sourcemap_chunk.chunk_id = Some(chunk_id.clone());
112 sourcemap_chunk
113 };
114
115 self.chunk_id = Some(chunk_id.clone());
116 self.source.content = new_source_content;
117 self.sourcemap.content = serde_json::to_string(&new_sourcemap)?;
118 Ok(())
119 }
120
121 pub fn save(&self) -> Result<()> {
122 self.source.save(None)?;
123 self.sourcemap.save(None)?;
124 Ok(())
125 }
126
127 pub fn into_chunk_upload(self) -> Result<ChunkUpload> {
128 let chunk_id = self.chunk_id.ok_or_else(|| anyhow!("Chunk ID not found"))?;
129 let source_content = self.source.content;
130 let sourcemap_content = self.sourcemap.content;
131 let data = SourceAndMap {
132 minified_source: source_content,
133 sourcemap: sourcemap_content,
134 };
135 let data = write_symbol_data(data)?;
136 Ok(ChunkUpload { chunk_id, data })
137 }
138}
139
140pub fn read_pairs(directory: &PathBuf) -> Result<Vec<SourcePair>> {
141 if !directory.exists() {
143 bail!("Directory does not exist");
144 }
145
146 let mut pairs = Vec::new();
147 for entry in WalkDir::new(directory).into_iter().filter_map(|e| e.ok()) {
148 let entry_path = entry.path().canonicalize()?;
149 info!("Processing file: {}", entry_path.display());
150 if is_javascript_file(&entry_path) {
151 let source = SourceFile::load(&entry_path)?;
152 let sourcemap_path = guess_sourcemap_path(&source.path);
153 if sourcemap_path.exists() {
154 let sourcemap = SourceFile::load(&sourcemap_path)?;
155 let chunk_id = get_chunk_id(&sourcemap);
156 pairs.push(SourcePair {
157 chunk_id,
158 source,
159 sourcemap,
160 });
161 } else {
162 warn!("No sourcemap file found for file {}", entry_path.display());
163 }
164 }
165 }
166 Ok(pairs)
167}
168
169pub fn get_chunk_id(sourcemap: &SourceFile) -> Option<String> {
170 #[derive(Deserialize)]
171 struct SourceChunkId {
172 chunk_id: String,
173 }
174 serde_json::from_str(&sourcemap.content)
175 .map(|chunk_id: SourceChunkId| chunk_id.chunk_id)
176 .ok()
177}
178
179pub fn guess_sourcemap_path(path: &Path) -> PathBuf {
180 let mut sourcemap_path = path.to_path_buf();
182 match path.extension() {
183 Some(ext) => sourcemap_path.set_extension(format!("{}.map", ext.to_string_lossy())),
184 None => sourcemap_path.set_extension("map"),
185 };
186 sourcemap_path
187}
188
189fn is_javascript_file(path: &Path) -> bool {
190 path.extension()
191 .map_or(false, |ext| ext == "js" || ext == "mjs" || ext == "cjs")
192}