1use std::mem::replace;
2
3use anyhow::Result;
4use bincode::{Decode, Encode};
5use turbo_rcstr::RcStr;
6use turbo_tasks::{FxIndexMap, NonLocalValue, ResolvedVc, ValueToString, ValueToStringRef, Vc};
7use turbo_tasks_fs::{FileContent, FileLine, FileLinesContent, FileSystemPath, rope::Rope};
8use turbopack_core::{
9 asset::{Asset, AssetContent},
10 file_source::FileSource,
11 output::OutputAsset,
12 source_map::{GenerateSourceMap, OriginalToken, SourceMap, Token},
13};
14
15use crate::compressed_size::compressed_size_bytes;
16
17#[derive(Clone, Debug, Eq, NonLocalValue, PartialEq, Encode, Decode)]
18pub struct ChunkPartRange {
19 pub line: u32,
20 pub start_column: u32,
21 pub end_column: u32,
22}
23
24#[derive(Clone, Debug, Eq, NonLocalValue, PartialEq, Encode, Decode)]
25pub struct ChunkPart {
26 pub source: RcStr,
27 pub real_size: u32,
28 pub unaccounted_size: u32,
29 pub lines: ResolvedVc<FileLinesContent>,
30 pub ranges: Vec<ChunkPartRange>,
31}
32
33impl ChunkPart {
34 pub async fn get_compressed_size(&self) -> Result<Option<u32>> {
35 let lines = &*self.lines.await?;
36 let FileLinesContent::Lines(lines) = lines else {
37 return Ok(None);
38 };
39
40 if self.ranges.is_empty() {
41 let mut all_content = String::new();
42 for line in lines {
43 all_content.push_str(&line.content);
44 }
45 Ok(Some(compressed_size_bytes(all_content)?))
46 } else {
47 let mut all_range_content = String::new();
48 for range in &self.ranges {
49 append_content_between(
50 range.line,
51 range.start_column,
52 range.line,
53 range.end_column,
54 lines,
55 &mut all_range_content,
56 );
57 }
58 Ok(Some(compressed_size_bytes(all_range_content)?))
59 }
60 }
61}
62
63pub async fn split_traced_file_into_parts(path: FileSystemPath) -> Result<Vec<ChunkPart>> {
64 let source = FileSource::new(path.clone());
65 let content = source.content().await?;
66 let AssetContent::File(file_content) = &*content else {
67 return Ok(vec![]);
68 };
69 let FileContent::Content(content) = &*file_content.await? else {
70 return Ok(vec![]);
71 };
72 let content = content.content();
73 let lines_vc = file_content.lines().to_resolved().await?;
74
75 self_mapped(path.to_string_ref().await?, content, lines_vc).await
76}
77
78pub async fn split_output_asset_into_parts(
79 asset: Vc<Box<dyn OutputAsset>>,
80) -> Result<Vec<ChunkPart>> {
81 let content = asset.content().await?;
82 let AssetContent::File(file_content) = &*content else {
83 return Ok(vec![]);
84 };
85 let FileContent::Content(content) = &*file_content.await? else {
86 return Ok(vec![]);
87 };
88 let content = content.content();
89 let lines_vc = file_content.lines().to_resolved().await?;
90
91 let Some(generate_source_map) =
92 ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(asset.to_resolved().await?)
93 else {
94 return self_mapped(asset.path().to_string().owned().await?, content, lines_vc).await;
95 };
96 let source_map = generate_source_map.generate_source_map().await?;
97 let Some(source_map) = source_map.as_content() else {
98 return self_mapped(asset.path().to_string().owned().await?, content, lines_vc).await;
99 };
100 let Some(source_map) = SourceMap::new_from_rope(source_map.content())? else {
101 return unaccounted(asset, content, lines_vc).await;
102 };
103
104 let lines = lines_vc.await?;
105 let FileLinesContent::Lines(lines) = &*lines else {
106 return unaccounted(asset, content, lines_vc).await;
107 };
108
109 fn end_of_mapping_column(
110 start_line: u32,
111 end_line: u32,
112 end_column: u32,
113 lines: &[FileLine],
114 ) -> u32 {
115 let start_line = start_line.min(lines.len() as u32 - 1);
116 let line_end = lines[start_line as usize].len() as u32;
117 if start_line == end_line {
118 end_column.min(line_end)
119 } else {
120 line_end
121 }
122 }
123
124 fn len_between(
125 start_line: u32,
126 start_column: u32,
127 end_line: u32,
128 end_column: u32,
129 lines: &[FileLine],
130 ) -> u32 {
131 let start_line = start_line.min(lines.len() as u32 - 1);
132 let end_line = end_line.min(lines.len() as u32 - 1);
133 if start_line == end_line {
134 return end_column.saturating_sub(start_column);
136 }
137 let mut len = lines[start_line as usize].len() as u32 - start_column + 1;
138 for line in &lines[start_line as usize + 1..end_line as usize] {
139 len += line.len() as u32 + 1;
140 }
141 len += end_column;
142 len
143 }
144
145 let mut chunk_parts = FxIndexMap::default();
146 fn add_chunk_part_range(
147 source: RcStr,
148 chunk_part_range: ChunkPartRange,
149 size: u32,
150 chunk_parts: &mut FxIndexMap<RcStr, ChunkPart>,
151 lines: ResolvedVc<FileLinesContent>,
152 ) {
153 let entry = chunk_parts
154 .entry(source)
155 .or_insert_with_key(|source| ChunkPart {
156 source: source.clone(),
157 real_size: 0,
158 unaccounted_size: 0,
159 ranges: vec![],
160 lines,
161 });
162 entry.real_size += size;
163 entry.ranges.push(chunk_part_range);
164 }
165
166 fn add_unaccounted_chunk_part(
167 source: RcStr,
168 unaccounted: u32,
169 chunk_parts: &mut FxIndexMap<RcStr, ChunkPart>,
170 lines: ResolvedVc<FileLinesContent>,
171 ) {
172 let entry = chunk_parts
173 .entry(source)
174 .or_insert_with_key(|source| ChunkPart {
175 source: source.clone(),
176 real_size: 0,
177 unaccounted_size: 0,
178 ranges: vec![],
179 lines,
180 });
181 entry.unaccounted_size += unaccounted;
182 }
183
184 fn end_current_mapping(
185 source: RcStr,
186 current_line: u32,
187 start_column: u32,
188 next_line: u32,
189 next_column: u32,
190 lines: &[FileLine],
191 chunk_parts: &mut FxIndexMap<RcStr, ChunkPart>,
192 lines_vc: ResolvedVc<FileLinesContent>,
193 ) -> State {
194 let mapping_end_column = end_of_mapping_column(current_line, next_line, next_column, lines);
195 let len = mapping_end_column.saturating_sub(start_column);
196 add_chunk_part_range(
197 source.clone(),
198 ChunkPartRange {
199 line: current_line,
200 start_column,
201 end_column: mapping_end_column,
202 },
203 len,
204 chunk_parts,
205 lines_vc,
206 );
207 State::AfterMapping {
208 source,
209 generated_line: current_line,
210 current_generated_column: mapping_end_column,
211 }
212 }
213
214 fn should_extend_mapping(
215 state: &State,
216 new_source: &RcStr,
217 new_line: u32,
218 new_column: u32,
219 ) -> bool {
220 if let State::InMapping {
221 source,
222 generated_line,
223 end_column,
224 ..
225 } = state
226 {
227 source == new_source && *generated_line == new_line && *end_column <= new_column
230 } else {
231 false
232 }
233 }
234
235 enum State {
236 StartOfFile,
237 InMapping {
238 source: RcStr,
239 generated_line: u32,
240 start_column: u32,
241 end_column: u32,
242 },
243 AfterMapping {
244 source: RcStr,
245 generated_line: u32,
246 current_generated_column: u32,
247 },
248 }
249
250 let mut state: State = State::StartOfFile;
251
252 for token in source_map.tokens() {
253 if let Token::Original(OriginalToken {
254 original_file,
255 generated_line,
256 generated_column,
257 ..
258 }) = token
259 {
260 if should_extend_mapping(&state, &original_file, generated_line, generated_column) {
262 if let State::InMapping {
264 source,
265 generated_line: current_line,
266 start_column,
267 ..
268 } = state
269 {
270 state = State::InMapping {
271 source,
272 generated_line: current_line,
273 start_column,
274 end_column: generated_column,
275 };
276 continue;
277 }
278 }
279
280 if let State::InMapping {
282 source,
283 generated_line: current_line,
284 start_column,
285 ..
286 } = state
287 {
288 state = end_current_mapping(
289 source,
290 current_line,
291 start_column,
292 generated_line,
293 generated_column,
294 lines,
295 &mut chunk_parts,
296 lines_vc,
297 );
298 }
299
300 match replace(
304 &mut state,
305 State::InMapping {
306 source: original_file.clone(),
307 generated_line,
308 start_column: generated_column,
309 end_column: generated_column,
310 },
311 ) {
312 State::InMapping { .. } => {
313 unreachable!();
314 }
315 State::AfterMapping {
316 source,
317 generated_line,
318 current_generated_column,
319 } => {
320 let len = len_between(
321 generated_line,
322 current_generated_column,
323 generated_line,
324 generated_column,
325 lines,
326 );
327 let half = len / 2;
328 add_unaccounted_chunk_part(source, half, &mut chunk_parts, lines_vc);
329 add_unaccounted_chunk_part(
330 original_file.clone(),
331 len - half,
332 &mut chunk_parts,
333 lines_vc,
334 );
335 }
336 State::StartOfFile => {
337 let len = len_between(0, 0, generated_line, generated_column, lines);
338 add_unaccounted_chunk_part(
339 original_file.clone(),
340 len,
341 &mut chunk_parts,
342 lines_vc,
343 );
344 }
345 }
346 }
347 }
348 let last_line = lines.len() as u32 - 1;
349 let last_column = lines[last_line as usize].len() as u32;
350
351 if let State::InMapping {
353 ref source,
354 generated_line,
355 start_column,
356 ..
357 } = state
358 {
359 state = end_current_mapping(
360 source.clone(),
361 generated_line,
362 start_column,
363 last_line,
364 last_column,
365 lines,
366 &mut chunk_parts,
367 lines_vc,
368 );
369 }
370
371 match state {
372 State::InMapping { .. } => {
373 unreachable!();
374 }
375 State::AfterMapping {
376 source,
377 generated_line,
378 current_generated_column,
379 } => {
380 let len = len_between(
381 generated_line,
382 current_generated_column,
383 last_line,
384 last_column,
385 lines,
386 );
387 add_unaccounted_chunk_part(source, len, &mut chunk_parts, lines_vc);
388 }
389 State::StartOfFile => {
390 return unaccounted(asset, content, lines_vc).await;
391 }
392 }
393
394 Ok(chunk_parts.into_values().collect())
395}
396
397pub async fn self_mapped(
398 path: RcStr,
399 content: &Rope,
400 lines: ResolvedVc<FileLinesContent>,
401) -> Result<Vec<ChunkPart>> {
402 let len = content.len().try_into().unwrap_or(u32::MAX);
403 Ok(vec![ChunkPart {
404 source: path,
405 real_size: len,
406 unaccounted_size: 0,
407 ranges: vec![],
408 lines,
409 }])
410}
411
412async fn unaccounted(
413 asset: Vc<Box<dyn OutputAsset>>,
414 content: &Rope,
415 lines: ResolvedVc<FileLinesContent>,
416) -> Result<Vec<ChunkPart>> {
417 let len = content.len().try_into().unwrap_or(u32::MAX);
418 Ok(vec![ChunkPart {
419 source: asset.path().to_string().owned().await?,
420 real_size: 0,
421 unaccounted_size: len,
422 ranges: vec![],
423 lines,
424 }])
425}
426
427fn append_content_between(
428 start_line: u32,
429 start_column: u32,
430 end_line: u32,
431 end_column: u32,
432 lines: &[FileLine],
433 out: &mut String,
434) {
435 let start_line = start_line.min(lines.len() as u32 - 1);
436 let end_line = end_line.min(lines.len() as u32 - 1);
437
438 let start_column = start_column.min(lines[start_line as usize].len() as u32);
439 let end_column = if start_line == end_line {
440 end_column.min(lines[start_line as usize].len() as u32)
441 } else {
442 lines[start_line as usize].len() as u32
443 };
444
445 if end_column <= start_column {
446 return;
447 }
448
449 out.extend(
450 lines[start_line as usize]
451 .content
452 .chars()
453 .skip(start_column as usize)
454 .take((end_column - start_column) as usize),
455 );
456
457 if start_line == end_line {
458 return;
459 }
460
461 for line in &lines[start_line as usize + 1..end_line as usize] {
462 out.push_str(&line.content);
463 }
464
465 out.extend(
466 lines[end_line as usize]
467 .content
468 .chars()
469 .take(end_column as usize),
470 );
471}