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